/* X-Designer
** (c) 1992, 1993, 1994, 1995, 1996
** Imperial Software Technology (IST),
** 95 London Street,
** Reading,
** Berkshire,
** United Kingdom RG1 4QA.
**
** Telephone: +44 1734 587055
** Fax:       +44 1734 589005
** Email:     support@ist.co.uk
*/

#if !defined(lint) && !defined(NOSCCS)
static char *sccsid = {"@(#)dist/release/src/help/client/help.c	1.20"} ; /* 96/07/01 */
#endif /* lint && NOSCCS */

/*
** help.c:
**
** Interface to help viewer
**
** Giles Thomas 1996
*/

#include <stdio.h>
#include <stdlib.h>
#include "help.h"
#include "path.h"

/*
** help client library - the protocol
**
** The protocol used to connect to XDhelp is simple.  The calling application
** designates one of its widgets as a communication endpoint.  It calls 
** XDhelp() passing in this widget, and a topic.  XDhelp looks at this 
** widget's window's properties - if the property XDHELP_WIN is set on it, it 
** gets the value of this property.  This property is another window ID.  It 
** tries to set the property XDHELP_DEST on this other window to the help key.
** If it succeeds then it exits signalling success.
** 
** If the property is not set, or if the set of XDHELP_DEST fails, then either
** XDhelp has not been started or it has exited.  So, the calling application
** starts XDhelp, passing in the window ID of its widget.  It then waits for
** the property to be set, which XDhelp does when it has created a window to
** use for *its* endpoint.  When this happens, it sets XDHELP_DEST as above.
**
** If XDHelp() is called while the application is waiting for the property
** to be set, it does not try to start another XDhelp but simply changes
** what it will set the XDHELP_DEST to when one does start.  This part of
** the protocol might need changing for some applications, or for XDhelps
** which might crash before they have had a chance to let the calling
** application know of their existence.
**
** A final useful feature is the ability to specify that a function be called
** with a particular parameter when the help viewer starts up.  The function
** XDHelpAddStartedCallback( void *(fn)( XtPointer ), XtPointer  ) is used to 
** set this, and is can be cleared with XDHelpRemoveStartedCallback().  Later
** revisions might support multiple callbacks of this type.
*/

#define XDHELP_WIN  "XDHELP_WIN"
#define XDHELP_DEST "XDHELP_DEST"

#define COMMAND_TEMPLATE "%s -wid %u &"

/* Forward declarations of some functions used in the main routine */

static Window  GetDestWindow() ;
static Boolean SetKeyOnDest() ;
static int     StartXDHelp() ;
static void    AddXDHCallback() ;

/**
** State information for protocol 
*/
static Boolean  waitingForXDHelp = False ;
static char    *currentTopic = NULL ;


/**
** A user callback which can be used to check when the help viewer
** has appeared
*/
static void      (*startedCallback)() = NULL ;
static XtPointer   startedClosure     = NULL ;


/**
** Display some help, using widget /w/ as the widget for preliminary 
** data transfer if any is necessary.  Returns 1 if the
** connection succeeded, 0 if it failed and a wait will occur before
** the help is displayed.  Either way, the help will eventually be
** displayed so long as the viewer works.  If -1 is returned, an 
** error occured when trying to fork/exec the help viewer, or the 
** widget passed in for IPC was invalid.
*/
int
XDHelp( w, topic )
    Widget w ;
    char *topic ;
{
    Display * d;
    Window    win;

    Window destWindow ;

    if ( w == NULL )
	  return -1 ;

    /* Get display for later use */
    if ( XmIsGadget (w) ) 
    {
	d = XtDisplay ( XtParent (w) );
	win = XtWindow (XtParent (w) );
    }
    else 
    {
	d = XtDisplay (w);
	win = XtWindow (w);
    }
    /* Initialise if necessary */
    XDHelpGetAtoms( w ) ;

    /* Store away the topic so that we know where we should be... */
    if ( currentTopic != NULL )
        free( currentTopic ) ;
    currentTopic = strdup( topic ) ;

    /* If we're already waiting for XDhelp to start up then knowing
       where to go should be sufficient */
    if ( waitingForXDHelp )
	return 0 ;

    /* If we have a destination window specified... */
    if ( (destWindow = GetDestWindow( w )) != (Window) 0 )
	/* ...if we can set the property on it */
	if ( SetKeyOnDest( d, destWindow, topic ) )
	    return 1 ;

    /* At this point, either there was no property set on our window,
       or the XDhelp whose window ID was contained in the property 
       has disappeared.  Either way, if we're not meant to be quietting a
       help viewer, we need to start one.  We set the callback first, as 
       we have to select for the events before we start the program to
       ensure we do pick up any changes. */
    if ( !strcmp( topic, QUIT_HELP_SYSTEM_MSG ) )
	return 1 ;

    AddXDHCallback( w, topic ) ;
    return StartXDHelp( win ) ;
}

int
QuitHelpViewer( Widget w )
{
    XDHelp( w, QUIT_HELP_SYSTEM_MSG ) ;
}

void
XDHelpAddStartedCallback( fn, closure )
    void      (*fn)() ;
    XtPointer   closure ;
{
    startedCallback = fn ;
    startedClosure  = closure ;
}

void
XDHelpRemoveStartedCallback()  {
    startedCallback = NULL ;
    startedClosure  = NULL ;
}

/*
** Atoms cached for efficiency
*/
Atom helpWin  = 0 ;
Atom helpDest = 0 ;

/*
** Routine to set up the atoms
*/
void
XDHelpGetAtoms( w )
    Widget w ;
{
    Display * d;
    
    if ( XmIsGadget (w) ) 
    {
	d = XtDisplay ( XtParent (w) );
    }
    else 
    {
	d = XtDisplay ( w );
    }

    if ( helpWin == 0 )
	helpWin = XInternAtom( d, XDHELP_WIN, False ) ;
    if ( helpDest == 0 )
	helpDest = XInternAtom( d, XDHELP_DEST, False ) ;
}

/*
** See if a property XDHELP_WIN is set on parameter /w/ and return it coerced
** to a Window if it is
*/
static Window
GetDestWindow( w )
    Widget w ;
{
    int  propGetResult ;
    Atom actualType ;
    int  actualFormat ;
    long nItems,
	 bytesAfter ;

    unsigned char *property ;

    Display * d;
    Window    win;

    /* Get display for later use */
    if ( XmIsGadget (w) ) 
    {
	d = XtDisplay ( XtParent (w) );
	win = XtWindow (XtParent (w) );
    }
    else 
    {
	d = XtDisplay (w);
	win = XtWindow (w);
    }
    /* Get the property */
    propGetResult = XGetWindowProperty( d,
					win,
					helpWin,
					0L,
					1L,
					(Bool) False,
					XA_WINDOW,
					&actualType,
					&actualFormat,
					&nItems,
					&bytesAfter,
					&property ) ;

    /* If we couldn't get the property... */
    if ( propGetResult != Success || actualType != XA_WINDOW )
	return (Window) 0 ;

    return *(Window *)property ;
}

static int (*oldErrorHandler)() ;
static Display *errorDisplay ;
static Boolean  haveHadBadWinError ;

static int
XDHErrorHandler( display, error )
    Display     *display ;
    XErrorEvent *error ;
{
    if ( (display = errorDisplay) && (error->error_code == BadWindow) ) {
	haveHadBadWinError = True ;
    } else
	oldErrorHandler( display, error ) ;
}

/*
** Try to set the property XDHELP_DEST on /window/ to /value/.  Return True
** on success, False on failure
*/
static Boolean
SetKeyOnDest( display, window, value )
    Display *display ;
    Window   window ;
    char    *value ;
{
    /* The only way we can detect that the window doesn't exist is to pick
       up X errors.  As these are called outside the scope of this function
       we must store away some information */
    oldErrorHandler = XSetErrorHandler( XDHErrorHandler ) ;
    errorDisplay = display ;
    haveHadBadWinError = False ;

    /* Now we change the property */
    XChangeProperty( display, window, helpDest, XA_STRING, sizeof( char ) * 8,
		     PropModeReplace, value, strlen( value ) + 1 ) ;
    
    /* The X error, if there is going to be one, will only happen if we 
       synchonise with the server.  One of these calls will call the
       error handler above if there was a problem in the XChangeProperty */
    XFlush( display ) ;
    XSync( display, False ) ;

    /* The error handler will have been called by this point, so we
       should reinstate the old one */
    (void) XSetErrorHandler( oldErrorHandler ) ;

    /* Now, if the error handler has been called due to the problem we 
       were expecting, haveHadBadWinError is True */
    return !haveHadBadWinError ;
}

/*
** Start up XDhelp, telling it that the window it needs to announce itself
** on is /window/.  Return 0 on success, -1 on failure.
*/
static int
StartXDHelp( window )
    Window window ;
{
    char *command = (char *) XtMalloc( strlen( COMMAND_TEMPLATE ) 
				       + strlen( _HELPPATH ) + 64 ) ;

    /* Set our state */
    waitingForXDHelp = True ;

    /* Create and execute the command */
    sprintf( command, COMMAND_TEMPLATE, _HELPPATH, (int) window ) ;
    return system( command ) == 0 ? 0 : -1 ;
}

static void XDHStartedCallback() ;

/*
** Add a callback on /widget/ to send XDhelp to the current topic when 
** the property XDHELP_WIN is set on /widget/.
*/
static void
AddXDHCallback( widget )
    Widget widget ;
{
    if ( XmIsGadget( widget ) )
	widget = XtParent( widget );


    XtAddEventHandler( widget, PropertyChangeMask, False, 
		       XDHStartedCallback, NULL ) ;
}

/*
** Callback to send XDhelp to a stored-away topic, currentTopic, when a
** property is set
** NB. we don't pass in the topic in the client data because it might have
** changed in between one run and the next.
*/
static void
XDHStartedCallback( w, client_data, event, ctd )
    Widget     w ;
    XtPointer  client_data ;
    XEvent    *event ;
    Boolean   *ctd ;
{
    /* Check that this *is* the event we were expecting */
    if ( event->type != PropertyNotify
      || event->xproperty.atom != helpWin ) {
	*ctd = True ;
	return ;
    }

    /* XDHelp must have started up (even if it has now gone down again) so
       we're not waiting any more */
    waitingForXDHelp = False ;

    /* Just go through the normal process; XDHelp might have disappeared
       immediately after telling us it was alive. */
    XDHelp( w, currentTopic ) ;

    if ( startedCallback != NULL ) 
	startedCallback( startedClosure ) ;
}
