// $Id$  -*- C++ -*-
// Interactive Help Callbacks

// Copyright (C) 1998 Technische Universitaet Braunschweig, Germany.
// Copyright (C) 2001, 2004 Free Software Foundation, Inc.
// Written by Andreas Zeller <zeller@gnu.org>.
// 
// This file is part of DDD.
// 
// DDD is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public
// License as published by the Free Software Foundation; either
// version 3 of the License, or (at your option) any later version.
// 
// DDD is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public
// License along with DDD -- see the file COPYING.
// If not, see <http://www.gnu.org/licenses/>.
// 
// DDD is the data display debugger.
// For details, see the DDD World-Wide-Web page, 
// `http://www.gnu.org/software/ddd/',
// or send a mail to the DDD developers <ddd@gnu.org>.

// "DDD is for wimps."
//                  -- Robert Bresner <rbresner@olf.com>

char HelpCB_rcsid[] =
    "$Id$";

#include "config.h"

#include "Agent.h"
#include "ComboBox.h"
#include "DestroyCB.h"
#include "HelpCB.h"
#include "MakeMenu.h"
#include "SmartC.h"
#include "TimeOut.h"
#include "TextSetS.h"
#include "ddd.h"		// process_pending_events()
#include "findParent.h"
#include "isid.h"
#include "longName.h"
#include "toolbar.h"
#include "windows.h"		// set_scrolled_window_size()
#include "misc.h"		// min(), max()

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <limits.h>
#include <ctype.h>

#include <Xm/Xm.h>
#include <Xm/CascadeB.h>
#include <Xm/MainW.h>
#include <Xm/MessageB.h>
#include <Xm/SelectioB.h>
#include <Xm/RowColumn.h>
#include <Xm/List.h>
#include <Xm/Text.h>
#include <Xm/Label.h>
#include <Xm/Form.h>
#include <Xm/TextF.h>
#include <Xm/PushB.h>
#include <Xm/PanedW.h>
#include <Xm/MenuShell.h>
#include <Xm/ToggleB.h>

#include <X11/cursorfont.h>
#include <X11/StringDefs.h>
#include <X11/IntrinsicP.h>	// LessTif hacks
#include <X11/Shell.h>

// Misc DDD includes
#include "LessTifH.h"
#include "strclass.h"
#include "cook.h"
#include "events.h"
#include "exit.h"
#include "simpleMenu.h"
#include "verify.h"
#include "Delay.h"
#include "StringA.h"
#include "IntArray.h"
#include "wm.h"
#include "post.h"
#include "mydialogs.h"
#include "ArgField.h"


//-----------------------------------------------------------------------
// Resources
//-----------------------------------------------------------------------

// The help system supports five resources:
// helpString          - displayed in context-sensitive help.
// helpOnVersionString - displayed in `Help On Version'.
// tipString           - displayed in small windows when entering buttons
// documentationString - displayed in the status line
// helpShowTitle       - if set, include widget name in context-sensitive help

struct help_resource_values {
    XmString helpString;
    Boolean showTitle;
};

struct help_on_version_resource_values {
    XmString helpOnVersionString;
    Boolean showTitle;
};

struct tip_resource_values {
    XmString tipString;
};

struct doc_resource_values {
    XmString documentationString;
};

static XtResource help_subresources[] = {
    {
	XTRESSTR(XtNhelpString),
	XTRESSTR(XtCHelpString),
	XmRXmString,
	sizeof(XmString),
	XtOffsetOf(help_resource_values, helpString),
	XtRImmediate,
	XtPointer(0)
    },

    {
	XTRESSTR(XtNhelpShowTitle),
	XTRESSTR(XtCHelpShowTitle),
	XmRBoolean,
	sizeof(Boolean),
	XtOffsetOf(help_resource_values, showTitle),
	XtRImmediate,
	XtPointer(False)
    }
};

static XtResource help_on_version_subresources[] = {
    {
	XTRESSTR(XtNhelpOnVersionString),
	XTRESSTR(XtCHelpOnVersionString),
	XmRXmString,
	sizeof(XmString),
	XtOffsetOf(help_on_version_resource_values, helpOnVersionString),
	XtRImmediate,
	XtPointer(0)
    },

    {
	XTRESSTR(XtNhelpShowTitle),
	XTRESSTR(XtCHelpShowTitle),
	XmRBoolean,
	sizeof(Boolean),
	XtOffsetOf(help_on_version_resource_values, showTitle),
	XtRImmediate,
	XtPointer(False)
    }
};

static XtResource tip_subresources[] = {
    {
	XTRESSTR(XtNtipString),
	XTRESSTR(XtCTipString),
	XmRXmString,
	sizeof(XmString),
	XtOffsetOf(tip_resource_values, tipString), 
	XtRImmediate,
	XtPointer(0)
    }
};

static XtResource doc_subresources[] = {
    {
	XTRESSTR(XtNdocumentationString),
	XTRESSTR(XtCDocumentationString),
	XmRXmString,
	sizeof(XmString),
	XtOffsetOf(doc_resource_values, documentationString), 
	XtRImmediate,
	XtPointer(0)
    }
};

//-----------------------------------------------------------------------
// Data
//-----------------------------------------------------------------------

static Widget help_dialog = 0;
static Widget help_shell  = 0;

static MString NoHelpText(Widget widget);
static MString NoTipText(Widget widget, XEvent *event);
static MString NoDocumentationText(Widget widget, XEvent *event);
static void _MStringHelpCB(Widget widget, 
			   XtPointer client_data, 
			   XtPointer call_data,
			   bool help_on_help = false);

MString helpOnVersionExtraText;


//-----------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------

// A single `\n' means `no string'.  (An empty string causes the
// helpers to be called.)
static bool isNone(const MString& s)
{
    return s.isEmpty() && s.lineCount() > 1;
}

static MString get_help_string(Widget widget)
{
    // Get text
    help_resource_values values;
    XtGetApplicationResources(widget, &values, 
			      help_subresources, XtNumber(help_subresources), 
			      ArgList(0), 0);

    MString text(values.helpString, true);
    if ((text.isNull() || text.isEmpty()) && DefaultHelpText != 0)
	text = DefaultHelpText(widget);
    if (text.isNull())
	text = NoHelpText(widget);

    if (values.showTitle)
	text.prepend(rm("Help for ") + bf(cook(longName(widget)))
		     + rm(":") + cr() + cr());

    return text;
}

static MString get_help_on_version_string(Widget widget)
{
    // Get text
    help_on_version_resource_values values;
    XtGetApplicationResources(widget, &values, 
			      help_on_version_subresources, 
			      XtNumber(help_subresources), 
			      ArgList(0), 0);

    MString text(values.helpOnVersionString, true);
    if (text.isNull() || text.isEmpty())
	text = NoHelpText(widget);

    if (values.showTitle)
	text.prepend(rm("Help on version for ") + bf(cook(longName(widget)))
		     + rm(":") + cr() + cr());

    return text;
}

static MString _get_tip_string(Widget widget, XEvent *event)
{
    if (XmIsText(widget))
    {
	if (DefaultTipText != 0)
	    return DefaultTipText(widget, event);
	return NoTipText(widget, event);
    }

    // Get text
    tip_resource_values values;
    XtGetApplicationResources(widget, &values, 
			      tip_subresources, XtNumber(tip_subresources), 
			      ArgList(0), 0);

    MString text(values.tipString, true);
    if (text.isNull() || isNone(text))
	return NoTipText(widget, event);

    if (text.isEmpty())
    {
	if (DefaultTipText != 0)
	    return DefaultTipText(widget, event);
	return NoTipText(widget, event);
    }

    return text;
}

static MString prepend_label_name(Widget widget, XEvent *event, 
				  MString (*get_string)(Widget, XEvent *))
{
    MString text = get_string(widget, event);

#if 0				// Experimental
    if (XtIsSubclass(widget, xmLabelWidgetClass))
    {
	unsigned char label_type = XmSTRING;
	XtVaGetValues(widget, XmNlabelType, &label_type, XtPointer(0));
	if (label_type == XmPIXMAP)
	{
	    XmString label = 0;
	    XtVaGetValues(widget, XmNlabelString, &label, XtPointer(0));
	    if (label != 0)
	    {
		text = MString(label, true) + rm(": ") + text;
		XmStringFree(label);
	    }
	}
    }
#endif

    return text;
}

inline MString get_tip_string(Widget widget, XEvent *event)
{
    return prepend_label_name(widget, event, _get_tip_string);
}


static MString _get_documentation_string(Widget widget, XEvent *event)
{
    if (XmIsText(widget))
    {
	if (DefaultDocumentationText != 0)
	    return DefaultDocumentationText(widget, event);
	return NoDocumentationText(widget, event);
    }

    // Get text
    doc_resource_values values;
    XtGetApplicationResources(widget, &values, 
			      doc_subresources, XtNumber(doc_subresources), 
			      ArgList(0), 0);

    MString text(values.documentationString, true);
    if (text.isNull())
	return get_tip_string(widget, event);

    if (isNone(text))
	return NoDocumentationText(widget, event);

    if (text.isEmpty())
    {
	if (DefaultDocumentationText != 0)
	    return DefaultDocumentationText(widget, event);
	return NoDocumentationText(widget, event);
    }

    return text;
}

inline MString get_documentation_string(Widget widget, XEvent *event)
{
    return prepend_label_name(widget, event, _get_documentation_string);
}


static bool call_tracking_help(XtPointer call_data, bool key_only = false)
{
    XmAnyCallbackStruct *cbs = (XmAnyCallbackStruct *)call_data;

    if (cbs == 0)
	return key_only;

    if (cbs->event == 0)
	return key_only;

    if (cbs->event->type != KeyPress && cbs->event->type != KeyRelease)
	return key_only;

    if ((cbs->event->xkey.state & ShiftMask) == 0)
	return false;

    return true;
}

static void nop1(Widget) {}
void (*PostHelpOnItemHook)(Widget) = nop1;


//-----------------------------------------------------------------------
// Functions
//-----------------------------------------------------------------------

void HelpOnHelpCB(Widget widget, XtPointer client_data, XtPointer call_data)
{
    if (help_dialog == 0)
    {
	// Make sure help dialog is created
	ImmediateHelpCB(widget, client_data, call_data);
    }

    // Get help on the help dialog
    MString text = get_help_string(help_dialog);
    _MStringHelpCB(widget, XtPointer(text.xmstring()), call_data, true);

    PostHelpOnItemHook(help_dialog);
}

void ImmediateHelpCB(Widget widget, XtPointer client_data, XtPointer call_data)
{
    if (widget == 0)
	return;

    if (call_tracking_help(call_data))
    {
	HelpOnContextCB(widget, client_data, call_data);
	return;
    }

    Delay delay;

    // Get help on this widget
    MString text = get_help_string(widget);
    MStringHelpCB(widget, XtPointer(text.xmstring()), call_data);
    PostHelpOnItemHook(widget);
}

void HelpOnThisCB(Widget widget, XtPointer client_data, XtPointer call_data)
{
    if (widget == 0)
	return;

    if (call_tracking_help(call_data))
    {
	HelpOnContextCB(widget, client_data, call_data);
	return;
    }

    Delay delay;

    Widget w = (Widget)client_data;
	
    // Get help on this widget
    MString text = get_help_string(w);
    MStringHelpCB(widget, XtPointer(text.xmstring()), call_data);
    PostHelpOnItemHook(w);
}

void HelpOnWindowCB(Widget widget, XtPointer client_data, XtPointer call_data)
{
    if (call_tracking_help(call_data))
    {
	HelpOnContextCB(widget, client_data, call_data);
	return;
    }

    Delay delay;

    // Get help on the shell window
    Widget shell = findTopLevelShellParent(widget);

    MString text = get_help_string(shell);
    MStringHelpCB(widget, XtPointer(text.xmstring()), call_data);
    PostHelpOnItemHook(shell);
}

void HelpOnVersionCB(Widget widget, XtPointer client_data, XtPointer call_data)
{
    if (call_tracking_help(call_data))
    {
	HelpOnContextCB(widget, client_data, call_data);
	return;
    }

    Delay delay;

    // Get a shell window
    Widget shell = findTopLevelShellParent(widget);

    MString text = get_help_on_version_string(shell);
    text += helpOnVersionExtraText;

    _MStringHelpCB(widget, XtPointer(text.xmstring()), call_data, false);

    // PostHelpOnItemHook(shell);
}

static void HelpDestroyCB(Widget, XtPointer client_data, XtPointer)
{
    Widget old_dialog = Widget(client_data);
    if (old_dialog == help_dialog)
    {
	help_dialog = 0;
	help_shell  = 0;
    }
}


// Default help, tip, and documentation strings
static MString NoHelpText(Widget widget)
{
    MString text = "No help available for \"";
    text += cook(longName(widget));
    text += "\"";

    return text;
}

static MString NoTipText(Widget, XEvent *)
{
    return MString(0, true);	// Empty string
}

static MString NoDocumentationText(Widget, XEvent *)
{
    return MString(0, true);	// Empty string
}

static XmTextPosition NoTextPosOfEvent(Widget, XEvent *)
{
    return XmTextPosition(-1);
}

MString (*DefaultHelpText)(Widget)                    = NoHelpText;
MString (*DefaultTipText)(Widget, XEvent *)           = NoTipText;
MString (*DefaultDocumentationText)(Widget, XEvent *) = NoDocumentationText;
XmTextPosition (*TextPosOfEvent)(Widget, XEvent *)    = NoTextPosOfEvent;


void (*DisplayDocumentation)(const MString&) = 0;

void StringHelpCB(Widget widget, XtPointer client_data, XtPointer call_data)
{
    MString text = (String)client_data;

    MStringHelpCB(widget, text.xmstring(), call_data);
}

void MStringHelpCB(Widget widget, XtPointer client_data, XtPointer call_data)
{
    _MStringHelpCB(widget, client_data, call_data);
}

static void _MStringHelpCB(Widget widget, 
			   XtPointer client_data, 
			   XtPointer,
			   bool help_on_help)
{
    XmString text = (XmString)client_data;

    Arg args[10];
    Cardinal arg = 0;
    XtSetArg(args[arg], XmNmessageString, text); arg++;

    Widget shell = findTopLevelShellParent(widget);
    if (shell == 0)
	shell = widget;

    if (help_dialog && (shell != help_shell))
    {
	DestroyWhenIdle(help_dialog);
	help_dialog = 0;
    }

    help_shell  = shell;

    if (help_dialog == 0)
    {
	// Build help_dialog
	XtSetArg(args[arg], XmNdeleteResponse, XmUNMAP); arg++;

	help_dialog = 
	    verify(XmCreateInformationDialog(shell, 
					     XMST("help"), args, arg));
	Delay::register_shell(help_dialog);
	XtAddCallback(help_dialog, XmNhelpCallback,
		      HelpOnHelpCB, 0);
	XtAddCallback(help_dialog, XtNdestroyCallback, 
		      HelpDestroyCB, XtPointer(help_dialog));

	XtUnmanageChild(XmMessageBoxGetChild(help_dialog, 
					     XmDIALOG_CANCEL_BUTTON));
    }
    else
    {
	// Setup text for existing dialog
	XtSetValues(help_dialog, args, arg);
    }

    // If this is a recursive call, disable the help button
    Widget help_button = 
	XmMessageBoxGetChild(help_dialog, XmDIALOG_HELP_BUTTON);
    set_sensitive(help_button, !help_on_help);

    // Popup help_dialog
    manage_and_raise(help_dialog);
}


static void HelpIndexCB(Widget widget, XtPointer client_data, 
			XtPointer call_data)
{
    Delay delay;

    XmListCallbackStruct *cbs = (XmListCallbackStruct *)call_data;
    Widget help_man = Widget(client_data);
    int index = cbs->item_position;

    void *userData = 0;
    XtVaGetValues(widget, XmNuserData, &userData, XtPointer(0));
    XmTextPosition *positions = (XmTextPosition *)userData;
    if (positions == 0)
	return;			// Not yet set

    XmTextPosition pos = positions[index - 1];

    XmTextSetInsertionPosition(help_man, pos);
    XmTextSetTopCharacter(help_man, pos);
    XmTextShowPosition(help_man, pos);
}


// Activate the button given in CLIENT_DATA
static void ActivateCB(Widget, XtPointer client_data, 
		       XtPointer call_data)
{
    XmAnyCallbackStruct *cbs = (XmAnyCallbackStruct *)call_data;
    
    Widget button = Widget(client_data);
    XtCallActionProc(button, "ArmAndActivate", cbs->event, (String *)0, 0);
}

struct FindInfo {
    Widget key;			// The text field holding the search key 
    Widget text;		// The text to be searched
};

static bool lock_update_arg = false;

static void FindCB(Widget w, XtPointer client_data, XtPointer call_data,
		   bool forward)
{
    Delay delay;

    XmAnyCallbackStruct *cbs = (XmAnyCallbackStruct *)call_data;

    FindInfo *fi = (FindInfo *)client_data;

    String key_s = XmTextFieldGetString(fi->key);
    string key(key_s);
    XtFree(key_s);

    static StringArray find_keys;

    int next_occurrence = -1;
    if (!key.empty())
    {
	find_keys += key;
	smart_sort(find_keys);
	uniq(find_keys);
	ComboBoxSetList(fi->key, find_keys);

	String text_s = XmTextGetString(fi->text);
	string text(text_s);
	XtFree(text_s);

	// If no uppercase letter is in KEY, make TEXT lowercase
	if (key == downcase(key))
	    text.downcase();

	XmTextPosition cursor = XmTextGetInsertionPosition(fi->text);

	if (forward)
	{
	    next_occurrence = text.index(key, cursor);
	    if (next_occurrence < 0)
		next_occurrence = text.index(key); // Wrap around
	}
	else
	{
	    next_occurrence = text.index(key, cursor - text.length() - 1);
	    if (next_occurrence < 0)
		next_occurrence = text.index(key, -1); // Wrap around
	}
    }

    if (next_occurrence < 0)
    {
	// FIXME: This should be in some status line.  Unfortunately,
	// we don't have any...
	post_warning(quote(key) + " not found.", "manual_find_error", w);
    }
    else
    {
	// LessTif 0.79 sometimes returns 0 in cbs->event.  Handle this.
	Time tm;
	if (cbs->event != 0)
	    tm = time(cbs->event);
	else
	    tm = XtLastTimestampProcessed(XtDisplay(fi->text));

	lock_update_arg = true;

	TextSetSelection(fi->text,
			 next_occurrence,
			 next_occurrence + key.length(),
			 tm);
	if (forward)
	{
	    XmTextSetInsertionPosition(fi->text, 
				       next_occurrence + key.length());
	}
	else
	{
	    XmTextSetInsertionPosition(fi->text, next_occurrence);
	}

	lock_update_arg = false;
    }
}


// Find the next occurrence of the string contained in the widget 
// given in CLIENT_DATA
static void FindForwardCB(Widget w, XtPointer client_data, XtPointer call_data)
{
    FindCB(w, client_data, call_data, true);
}

// Find the previous occurrence of the string contained in the widget 
// given in CLIENT_DATA
static void FindBackwardCB(Widget w, XtPointer client_data, 
			   XtPointer call_data)
{
    FindCB(w, client_data, call_data, false);
}

// Highlight current section after cursor motion
static void HighlightSectionCB(Widget, XtPointer client_data, 
			       XtPointer call_data)
{
    XmTextVerifyCallbackStruct *cbs = (XmTextVerifyCallbackStruct *)call_data;
    Widget list = Widget(client_data);

    XmTextPosition cursor = cbs->newInsert;

    void *userData = 0;
    XtVaGetValues(list, XmNuserData, &userData, XtPointer(0));
    XmTextPosition *positions = (XmTextPosition *)userData;
    if (positions == 0)
	return;			// Not yet set

    int pos = 0;
    while (positions[pos] <= cursor)
	pos++;

    ListSetAndSelectPos(list, pos);
}

static void SetSelectionCB(Widget w, XtPointer client_data, 
			   XtPointer call_data)
{
    if (lock_update_arg)
	return;

    ArgField *arg_field = (ArgField *)client_data;

    string selection = "";
    String _selection = XmTextGetSelection(w);
    if (_selection != 0)
    {
	selection = _selection;
	XtFree(_selection);
    }
    else
    {
	// No selection - get word at cursor
	XmTextVerifyCallbackStruct *cbs = 
	    (XmTextVerifyCallbackStruct *)call_data;
	XmTextPosition cursor = cbs->newInsert;
	String text = XmTextGetString(w);
	XmTextPosition startpos, endpos;
	startpos = endpos = cursor;

	while (startpos > 0 && isid(text[startpos - 1]))
	    startpos--;
	while (text[endpos] != '\0' && isid(text[endpos]))
	    endpos++;

	if (endpos > startpos)
	    selection = string(text + startpos, endpos - startpos);
    }

    if (!selection.empty())
    {
	selection.downcase();
	while (selection.contains('\n'))
	    selection = selection.after('\n');
	arg_field->set_string(selection);
    }
}

// Return true iff TEXT contains a manual header line at pos
static bool has_header(char *text, unsigned pos)
{
    if (text[pos] != '\0'
	&& !isspace(text[pos])
	&& (pos == 0 || text[pos - 1] == '\n')
	&& text[pos + 1] != '\b')
    {
	// Non-highlighted character in column 1
	return true;
    }

    return false;
}

// Note: assumes `man' or `info' format.
void ManualStringHelpCB(Widget widget, XtPointer client_data, 
			XtPointer)
{
    static MString null(0, true);
    string text(STATIC_CAST(char *,client_data));

    ManualStringHelpCB(widget, null, text);
}


// Close action from menu
static void CloseCB(Widget w, XtPointer, XtPointer)
{
    Widget shell = findTopLevelShellParent(w);
    DestroyWhenIdle(shell);
}

// Create an empty dialog within a top-level-shell
// FIXME: This should be part of core DDD!
static Widget create_text_dialog(Widget parent, const _XtString name, 
				 Arg *args, int arg, Widget& bar)
{
    Widget shell = verify(XtCreateWidget(name, topLevelShellWidgetClass,
					 parent, args, arg));

    arg = 0;
    Widget w = XmCreateMainWindow(shell, XMST(name), args, arg);
    XtManageChild(w);

    MMDesc file_menu[] = 
    {
	{ "close", MMPush, { CloseCB, XtPointer(shell) }, 0, 0, 0, 0 },
	{ "exit",  MMPush, { DDDExitCB, XtPointer(EXIT_SUCCESS) }, 0, 0, 0, 0},
	MMEnd
    };

    MMDesc menubar[] = 
    {
	{ "file",     MMMenu, MMNoCB, file_menu, 0, 0, 0 },
	{ "edit",     MMMenu, MMNoCB, simple_edit_menu, 0, 0, 0 },
	{ "help",     MMMenu | MMHelp, MMNoCB, simple_help_menu, 0, 0, 0 },
	MMEnd
    };

    bar = MMcreateMenuBar(w, "menubar", menubar);

    // Don't add a callback to `Edit' menu
    menubar[1].items = 0;

    MMaddCallbacks(menubar);
    MMaddHelpCallback(menubar, ImmediateHelpCB);

    menubar[1].items = simple_edit_menu;

    Delay::register_shell(shell);
    InstallButtonTips(shell);

    return w;
}

static void DeleteFindInfoCB(Widget, XtPointer client_data, XtPointer)
{
    FindInfo *fi = (FindInfo *)client_data;
    delete fi;
}

static int max_width(const char *text)
{
    int max_width = 0;
    int width     = 0;

    while (*text != '\0')
    {
	switch (*text++)
	{
	case '\b':
	    if (width > 0)
		width--;
	    break;

	case '\n':
	    max_width = max(max_width, width);
	    width = 0;
	    break;

	default:
	    width++;
	}
    }

    return max_width;
}

static void ToggleIndexCB(Widget w, XtPointer client_data, XtPointer)
{
    Widget child = Widget(client_data);

    if (XmToggleButtonGetState(w))
	manage_paned_child(child);
    else
	XtUnmanageChild(child);
}

// Return manual
void ManualStringHelpCB(Widget widget, const MString& title,
			const string& unformatted_text)
{
    // Delay delay;

    // Build manual dialog
    Widget toplevel = findTheTopLevelShell(widget);
    if (toplevel == 0)
	return;

    // Format text
    string the_text(unformatted_text);

    // For efficiency reasons, we access all data in-place via the TEXT ptr.
    char *text = CONST_CAST(char *,the_text.chars());

    // Set i > 0 if TEXT contains more than one newline
    int i = the_text.index('\n');
    if (i >= 0)
	i = the_text.index('\n', i + 1);

    bool manual = !the_text.contains("File:", 0) && i > 0;
    bool info   =  the_text.contains("File:", 0) && i > 0;

    int len = the_text.length();

    if (manual)
    {
	// Manual page: strip manual headers and footers
	int source = 0;
	int target = 0;

	for (;;)
	{
	    if (target % 100 == 0)
		process_pending_events();

	    while (has_header(text, source))
	    {
		// Header line: strip line and surrounding blanks.
		// At least using GNU nroff, each header line
		// has three blank lines before and two afterwards.
		if (target > 0)
		    target--;
		if (target >= 0 && text[target] == '\n')
		    target--;
		if (target >= 0 && text[target] == '\n')
		    target--;
		if (target >= 0 && text[target] == '\n')
		    target--;
		if (target >= 0 && text[target] == '\n')
		    target--;
		target++;
		if (target > 0)
		    text[target++] = '\n';

		while (text[source] != '\n')
		    source++;
		if (text[source] == '\n')
		    source++;
		if (text[source] == '\n')
		    source++;
		if (text[source] == '\n')
		    source++;
	    }
	    if (text[source] == '\0')
		break;

	    text[target++] = text[source++];
	}

	text[target] = '\0';
	len = target;
	while (target < int(the_text.length()))
	    text[target++] = '\0';
    }
    else if (info)
    {
	// Info file: strip menus
	int source = 0;
	int target = 0;

	while (text[source] != '\0')
	{
	    if (target % 100 == 0)
		process_pending_events();

	    while (text[source] == '*' && 
		   text[source + 1] == ' ' && 
		   (source == 0 || text[source - 1] == '\n'))
	    {
		// Skip menu item
		while (text[source++] != '\n')
		    ;
	    }

	    text[target++] = text[source++];
	}

	text[target] = '\0';
	len = target;
	while (target < int(the_text.length()))
	    text[target++] = '\0';
    }

    if (info || manual)
    {
	// Info and manual: kill multiple empty lines
	int source = 0;
	int target = 0;

	while (text[source] != '\0')
	{
	    if (target % 100 == 0)
		process_pending_events();

	    if (text[source] == '\n')
	    {
		while (text[source] != '\0'
		       && text[source + 1] == '\n'
		       && text[source + 2] == '\n')
		    source++;
	    }
	    text[target++] = text[source++];
	}
	text[target] = '\0';
	len = target;
	while (target < int(the_text.length()))
	    text[target++] = '\0';
    }

    // Find titles
    StringArray titles;
    IntArray positions;

    if (info)
    {
	// Info file
	int source = 0;
	while (source >= 0)
	{
	    process_pending_events();

	    assert(the_text.contains("File: ", source));

	    // Fetch title below `File: ' line
	    int start_of_title = the_text.index("\n\n", source) + 2;
	    int end_of_title   = the_text.index("\n", start_of_title);
	    string title = 
		the_text.at(start_of_title, end_of_title - start_of_title);

	    // Fetch indent level, using the underline characters
	    int start_of_underline = the_text.index('\n', start_of_title) + 1;
	    static string underlines = "*=-.";
	    int indent = underlines.index(the_text[start_of_underline]);
	    if (indent < 0)
		indent = underlines.length();
	    if (indent == 0)
		title.upcase();

	    // Add title and position
	    titles += replicate(' ', indent * 2) + title;
	    positions += source;

	    // Strip `File: ' line
	    the_text.del(source, start_of_title - source);
	    
	    // Find next `File: ' line
	    source = the_text.index("File: ", source);
	}
	text = CONST_CAST(char *,the_text.chars());
	len = the_text.length();
    }



    Arg args[15];
    Cardinal arg = 0;
    Widget menubar;
    XtSetArg(args[arg], XmNdeleteResponse, XmDESTROY); arg++;
    Widget text_dialog = create_text_dialog(toplevel, "manual_help", 
					    args, arg, menubar);

    arg = 0;
    XtSetArg(args[arg], XmNmarginWidth,        0); arg++;
    XtSetArg(args[arg], XmNmarginHeight,       0); arg++;
    XtSetArg(args[arg], XmNborderWidth,        0); arg++;
    XtSetArg(args[arg], XmNhighlightThickness, 0); arg++;
    Widget form = verify(XmCreateForm(text_dialog, XMST("form"), args, arg));

    arg = 0;
    XtSetArg(args[arg], XmNmarginWidth,      0);                 arg++;
    XtSetArg(args[arg], XmNmarginHeight,     0);                 arg++;
    XtSetArg(args[arg], XmNborderWidth,      0);                 arg++;
    XtSetArg(args[arg], XmNallowResize,      True);              arg++;
    XtSetArg(args[arg], XmNtopAttachment,    XmATTACH_FORM);     arg++;
    XtSetArg(args[arg], XmNbottomAttachment, XmATTACH_FORM);     arg++;
    XtSetArg(args[arg], XmNleftAttachment,   XmATTACH_FORM);     arg++;
    XtSetArg(args[arg], XmNrightAttachment,  XmATTACH_FORM);     arg++;
    Widget area = verify(XmCreatePanedWindow(form, 
					     XMST("help_area"), args, arg));
    XtManageChild(area);

    FindInfo *fi = new FindInfo;
    XtAddCallback(text_dialog, XmNdestroyCallback,
		  DeleteFindInfoCB, XtPointer(fi));

    MMDesc items [] = 
    {
	{ "findBackward", MMPush, 
	  { FindBackwardCB, XtPointer(fi) }, 0, 0, 0, 0},
	{ "findForward",  MMPush, 
	  { FindForwardCB, XtPointer(fi) }, 0, 0, 0, 0},
	MMEnd
    };

    Widget arg_label;
    ArgField *arg_field;
    Widget toolbar = create_toolbar(area, "toolbar", items, 0, arg_label,
				    arg_field, XmPIXMAP);
    fi->key = arg_field->text();
    XtAddCallback(arg_label, XmNactivateCallback, 
		  ClearTextFieldCB, fi->key);
    MMaddCallbacks(simple_edit_menu, XtPointer(fi->key));

    arg = 0;
    Widget help_index = 
	verify(XmCreateScrolledList(area, XMST("index"), args, arg));
    XtManageChild(help_index);
    if (lesstif_version < 90)
	set_scrolled_window_size(help_index);

    Widget view_index;
    MMDesc manual_menu[] = 
    {
	{ "findForward",  MMPush, 
	  { FindForwardCB, XtPointer(fi) }, 0, 0, 0, 0 },
	{ "findBackward", MMPush, 
	  { FindBackwardCB, XtPointer(fi) }, 0, 0, 0, 0 },
	MMSep,
	{ "viewIndex",    MMToggle, { ToggleIndexCB, 
				      XtPointer(XtParent(help_index)) }, 
	  0, &view_index, 0, 0 },
	MMEnd
    };

    MMDesc more_menubar[] = 
    {
	// This is called `source' such that we can re-use the Find
	// specs from the DDD `Source' meny
	{ "source", MMMenu, MMNoCB, manual_menu, 0, 0, 0 },
	MMEnd
    };

    MMaddItems(menubar, more_menubar);
    MMaddCallbacks(more_menubar);
    XtVaSetValues(view_index, XmNset, True, XtPointer(0));

    int columns = max_width(text);
    columns = min(max(columns, 40), 80) + 1;

    arg = 0;
    XtSetArg(args[arg], XmNcolumns,  columns);           arg++;
    XtSetArg(args[arg], XmNeditable, False);             arg++;
    XtSetArg(args[arg], XmNeditMode, XmMULTI_LINE_EDIT); arg++;
    XtSetArg(args[arg], XmNvalue,    "");                arg++;
    Widget help_man = 
	verify(XmCreateScrolledText(area, XMST("text"), args, arg));
    XtManageChild(help_man);
    if (lesstif_version < 90)
	set_scrolled_window_size(help_man);
    fi->text = help_man;

    XtWidgetGeometry size;
    size.request_mode = CWHeight;
    XtQueryGeometry(toolbar, (XtWidgetGeometry *)0, &size);
    XtVaSetValues(toolbar,
		  XmNpaneMaximum, size.height,
		  XmNpaneMinimum, size.height,
		  XtPointer(0));

    XtAddCallback(help_index, XmNsingleSelectionCallback,
		  HelpIndexCB, XtPointer(help_man));
    XtAddCallback(help_index, XmNmultipleSelectionCallback,
		  HelpIndexCB, XtPointer(help_man));
    XtAddCallback(help_index, XmNbrowseSelectionCallback,
		  HelpIndexCB, XtPointer(help_man));
    XtAddCallback(help_index, XmNdefaultActionCallback,
		  HelpIndexCB, XtPointer(help_man));
    XtAddCallback(text_dialog, XmNhelpCallback,
		  ImmediateHelpCB, XtPointer(help_man));

    XtAddCallback(help_man, XmNmotionVerifyCallback,
		  HighlightSectionCB, XtPointer(help_index));
    XtAddCallback(help_man, XmNmotionVerifyCallback,
		  SetSelectionCB, XtPointer(arg_field));

    XtAddCallback(fi->key, XmNactivateCallback, ActivateCB,
		  XtPointer(items[1].widget));

    XtVaSetValues(text_dialog, XmNdefaultButton, Widget(0), XtPointer(0));
    
    XtManageChild(form);
    InstallButtonTips(text_dialog);

    // Set title
    if (!title.isNull())
	wm_set_name(XtParent(text_dialog), title.str(), title.str());

    if (manual)
    {
	// Manual page: handle underlines

	bool *underlined    = new bool[len];
	bool *doublestriked = new bool[len];
	for (i = 0; i < len; i++)
	    underlined[i] = doublestriked[i] = false;

	int source = 0;
	int target = 0;

	while (text[source] != '\0')
	{
	    if (target % 100 == 0)
		process_pending_events();

	    char c = text[target++] = text[source++];

	    if (c == '\b' && target >= 2)
	    {
		target -= 2;
		if (text[target] == '_')
		    underlined[target] = true;
		if (text[target] == text[source])
		    doublestriked[target] = true;
	    }
	}
	text[target] = '\0';
	len = target;
	while (target < int(the_text.length()))
	    text[target++] = '\0';

	// Set text
	XtVaSetValues(help_man, XmNvalue, text, XtPointer(0));

	// Set highlighting
	XmTextSetHighlight(help_man, 0, XmTextGetLastPosition(help_man), 
			   XmHIGHLIGHT_NORMAL);

	XmTextPosition underlining    = 0;
	XmTextPosition doublestriking = 0;
	for (i = 0; i < len; i++)
	{
	    if (i % 100 == 0)
		process_pending_events();

	    if (doublestriked[i] && !doublestriking)
	    {
		doublestriking = i;
	    }
	    else if (!doublestriked[i] && doublestriking)
	    {
		// There is no bold face in XmText, normal selection
		// clutters the screen and secondary selection is already used.
		// So what shall we do here?  Use the Motif 2.0 CSText?
#if 0
		XmTextSetHighlight(help_man, doublestriking, i, 
				   XmHIGHLIGHT_SELECTED);
		process_pending_events();
#endif
		doublestriking = 0;
	    }

	    if (underlined[i] && !underlining)
	    {
		underlining = i;
	    }
	    else if (!underlined[i] && underlining)
	    {
		XmTextSetHighlight(help_man, underlining, i, 
				   XmHIGHLIGHT_SECONDARY_SELECTED);
		underlining = 0;
	    }
	}

	delete[] underlined;
	delete[] doublestriked;
    }

    if (manual)
    {
	// Set titles for manual page
	int start_of_line = 0;

	for (int source = 0; source < len; source++)
	{
	    if (source % 100 == 0)
		process_pending_events();

	    if (text[source] == '\n')
	    {
		if (source - start_of_line > 3)
		{
		    // Check manual title
		    bool is_title = false;
		
		    if (text[start_of_line] == ' ' &&
			text[start_of_line + 1] == ' ' &&
			text[start_of_line + 2] != '\0' &&
			text[start_of_line + 3] != ' ')
			is_title = true; // .SS title
		    else if (text[start_of_line] != ' '
			     && source - start_of_line < 60)
			is_title = true; // .SH title

		    if (is_title)
		    {
			titles += 
			    the_text.at(start_of_line, source - start_of_line);
			positions += start_of_line;
		    }
		}

		start_of_line = source + 1;
	    }
	}
    }
    else
    {
	// Info file, or something else: titles are already set

	// Set text
	XtVaSetValues(help_man, XmNvalue, text, XtPointer(0));

	// No highlighting
	XmTextSetHighlight(help_man, 0, XmTextGetLastPosition(help_man), 
			   XmHIGHLIGHT_NORMAL);
    }

    process_pending_events();

    // Set titles in selection list
    XmTextPosition *xmpositions = new XmTextPosition[titles.size() + 1];
    for (i = 0; i < titles.size(); i++)
	xmpositions[i] = positions[i];
    xmpositions[i] = INT_MAX;

    XmStringTable xmtitles = new XmString[titles.size()];

    for (i = 0; i < titles.size(); i++)
    {
	xmtitles[i] = 
	    XmStringCreateLtoR(XMST(titles[i].chars()),
			       titles[i].contains(' ', 0) ?
			       XMST(CHARSET_RM) :
			       XMST(CHARSET_BF));
    }

    XtVaSetValues(help_index,
		  XmNtopItemPosition,   1,
		  XmNselectedItemCount, 0,
		  XmNitems,             xmtitles,
		  XmNuserData,          xmpositions,
		  XmNitemCount,         titles.size(),
		  XtPointer(0));

    for (i = 0; i < titles.size(); i++)
	XmStringFree(xmtitles[i]);
    delete[] xmtitles;

    process_pending_events();

    // Enable Text Window
    Delay::register_shell(XtParent(text_dialog));
    InstallButtonTips(XtParent(text_dialog));
    manage_and_raise(text_dialog);
}


void TextHelpCB(Widget widget, XtPointer client_data, XtPointer)
{
    // Delay delay;

    string name = "text_help";

    // If CLIENT_DATA starts with @FOO@, use FOO as dialog name
    String text = (String)client_data;
    if (text[0] == '@')
    {
	name = text + 1;
	name = name.before('@');
	text += name.length() + 2;
    }

    // Build help_text
    Widget toplevel = findTheTopLevelShell(widget);
    if (toplevel == 0)
	return;

    Arg args[15];
    Cardinal arg = 0;
    Widget menubar;
    XtSetArg(args[arg], XmNdeleteResponse, XmDESTROY); arg++;
    Widget text_dialog = create_text_dialog(toplevel, name.chars(), args, arg, 
					    menubar);

    arg = 0;
    Widget form = verify(XmCreateForm(text_dialog, XMST("form"), args, arg));

    arg = 0;
    XtSetArg(args[arg], XmNmarginWidth,      0);                 arg++;
    XtSetArg(args[arg], XmNmarginHeight,     0);                 arg++;
    XtSetArg(args[arg], XmNborderWidth,      0);                 arg++;
    XtSetArg(args[arg], XmNallowResize,      True);              arg++;
    XtSetArg(args[arg], XmNtopAttachment,    XmATTACH_FORM);     arg++;
    XtSetArg(args[arg], XmNbottomAttachment, XmATTACH_FORM);     arg++;
    XtSetArg(args[arg], XmNleftAttachment,   XmATTACH_FORM);     arg++;
    XtSetArg(args[arg], XmNrightAttachment,  XmATTACH_FORM);     arg++;
    Widget area = verify(
	XmCreatePanedWindow(form, XMST("help_area"), args, arg));
    XtManageChild(area);

    FindInfo *fi = new FindInfo;
    XtAddCallback(text_dialog, XmNdestroyCallback,
		  DeleteFindInfoCB, XtPointer(fi));

    MMDesc items [] = 
    {
	{ "findBackward", MMPush, 
	  { FindBackwardCB, XtPointer(fi) }, 0, 0, 0, 0 },
	{ "findForward",  MMPush, 
	  { FindForwardCB, XtPointer(fi) }, 0, 0, 0, 0 },
	MMEnd
    };

    Widget arg_label;
    ArgField *arg_field;
    Widget toolbar = create_toolbar(area, "toolbar", items, 0, arg_label,
				    arg_field, XmPIXMAP);
    fi->key = arg_field->text();
    XtAddCallback(arg_label, XmNactivateCallback, 
		  ClearTextFieldCB, fi->key);
    MMaddCallbacks(simple_edit_menu, XtPointer(fi->key));

    MMDesc manual_menu[] = 
    {
	{ "findForward",  MMPush, 
	  { FindForwardCB, XtPointer(fi) }, 0, 0, 0, 0},
	{ "findBackward", MMPush,
	  { FindBackwardCB, XtPointer(fi) }, 0, 0, 0, 0},
	MMEnd
    };

    MMDesc more_menubar[] = 
    {
	// This is called `source' such that we can re-use the Find
	// specs from the DDD `Source' meny
	{ "source", MMMenu, MMNoCB, manual_menu, 0, 0, 0 },
	MMEnd
    };

    MMaddItems(menubar, more_menubar);
    MMaddCallbacks(more_menubar);

    int columns = max_width(text);
    columns = min(max(columns, 40), 80) + 1;

    arg = 0;
    XtSetArg(args[arg], XmNcolumns,          columns);           arg++;
    XtSetArg(args[arg], XmNeditable,         False);             arg++;
    XtSetArg(args[arg], XmNeditMode,         XmMULTI_LINE_EDIT); arg++;
    XtSetArg(args[arg], XmNvalue,            text); arg++;
    Widget help_text = verify(
	XmCreateScrolledText(area, XMST("text"), args, arg));
    XtManageChild(help_text);
    if (lesstif_version < 90)
	set_scrolled_window_size(help_text);
    fi->text = help_text;

    XtWidgetGeometry size;
    size.request_mode = CWHeight;
    XtQueryGeometry(toolbar, (XtWidgetGeometry *)0, &size);
    XtVaSetValues(toolbar,
		  XmNpaneMaximum, size.height,
		  XmNpaneMinimum, size.height,
		  XtPointer(0));

    XtAddCallback(text_dialog, XmNhelpCallback, ImmediateHelpCB, XtPointer(0));

    XtAddCallback(help_text, XmNmotionVerifyCallback,
		  SetSelectionCB, XtPointer(arg_field));

    XtAddCallback(fi->key, XmNactivateCallback, ActivateCB,
		  XtPointer(items[1].widget));

    XtManageChild(form);
    InstallButtonTips(text_dialog);

    // Enable Text Window
    XtRealizeWidget(XtParent(text_dialog));
    Delay::register_shell(XtParent(text_dialog));
    InstallButtonTips(XtParent(text_dialog));
    XtPopup(XtParent(text_dialog), XtGrabNone);
    manage_and_raise(text_dialog);
}


//-----------------------------------------------------------------------------
// Context-sensitive help
//-----------------------------------------------------------------------------

// Return the widget related to the mouse event EV
static Widget EventToWidget(Widget widget, XEvent *ev)
{
    // If the button was clicked outside of this programs windows, the
    // widget that grabbed the pointer will get the event.  So, we
    // check the bounds of the widget against the coordinates of the
    // event.  If they're outside, we return 0.  Otherwise we
    // return the widget in which the event occured.

    switch (ev->type)
    {
    case KeyPress:
    case KeyRelease:
    case ButtonPress:
    case ButtonRelease:
	break;

    default:
	return 0;		// No window
    }

    Position x, y;
    Dimension width, height;
    XtVaGetValues(widget, XmNx, &x, XmNy, &y,
		  XmNwidth, &width, XmNheight, &height,
		  XtPointer(0));

    if (ev->xbutton.window == XtWindow(widget) &&
	(ev->xbutton.x < x ||
	 ev->xbutton.y < y ||
	 ev->xbutton.x > x + width ||
	 ev->xbutton.y > y + height))
    {
	return 0;
    }
    else
    {
	return XtWindowToWidget(XtDisplay(widget),
				ev->xbutton.window);
    }
}


// In LessTif, XmTrackingEvent() is somewhat broken - it returns on
// KeyRelease events, and it does not return the event.  Here's an
// improved implementation.
static Widget
TrackingEvent(Widget widget, Cursor cursor,
	      Boolean confine_to, XEvent *event_return)
{
    Window confine_to_this;
    XEvent ev;
    Boolean key_pressed = False;
    Time time;

    if (confine_to)
    {
	confine_to_this = XtWindow(widget);
    }
    else
    {
	confine_to_this = None;
    }

    time = XtLastTimestampProcessed(XtDisplay(widget));
    if (XtGrabPointer(widget, True,
		      ButtonReleaseMask | ButtonPressMask,
		      GrabModeAsync, GrabModeAsync,
		      confine_to_this, cursor, time) != GrabSuccess)
    {
	std::cerr << "TrackingEvent: Could not grab pointer\n";
	return 0;
    }

    while (True)
    {
	XtAppNextEvent(XtWidgetToApplicationContext(widget), &ev);
	time = XtLastTimestampProcessed(XtDisplay(widget));

	if (ev.type == KeyPress)
	{
	    // Avoid exiting upon releasing the key that caused
	    // XmTrackingEvent() to be invoked
	    key_pressed = True;
	}
	else if ((ev.type == KeyRelease && key_pressed) ||
		 (ev.type == ButtonRelease && ev.xbutton.button == 1))
	{
	    if (event_return != 0)
		*event_return = ev;

	    XtUngrabPointer(widget, time);

	    return EventToWidget(widget, &ev);
	}
    }
}

// Hook before help on context
static void nop2(Widget, XtPointer, XtPointer) {}
void (*PreHelpOnContextHook)(Widget w, XtPointer client_data, 
			     XtPointer call_data) = nop2;

void HelpOnContextCB(Widget widget, XtPointer client_data, XtPointer call_data)
{
    Widget item = 0;
    Widget toplevel = findTopLevelShellParent(widget);

    if (toplevel == 0)
    {
	HelpOnWindowCB(widget, client_data, call_data);
	return;
    }
	   
    PreHelpOnContextHook(widget, client_data, call_data);

    static Cursor cursor = 
	XCreateFontCursor(XtDisplay(toplevel), XC_question_arrow);

    XEvent ev;
#if XmVersion < 1002
    // No XmTrackingEvent() in Motif 1.1
    item = TrackingEvent(toplevel, cursor, False, &ev);
#else
    if (lesstif_version <= 84)
	item = TrackingEvent(toplevel, cursor, False, &ev);
    else
	item = XmTrackingEvent(toplevel, cursor, False, &ev);
#endif

    if (item != 0)
	ImmediateHelpCB(item, client_data, 0);
    else
	ImmediateHelpCB(toplevel, client_data, 0);

    // Some Motif versions get confused if this function is invoked
    // via a menu accelerator; the keyboard remains grabbed. Hence, we
    // ungrab it explicitly.
    XtUngrabKeyboard(toplevel,
		     XtLastTimestampProcessed(XtDisplay(widget)));
}


// Return the child widget (or gadget) EX/EY is in, starting with WIDGET.
static Widget GetWidgetAt(Widget w, int ex, int ey)
{
    if (w == 0)
	return 0;

    bool once_again = true;
    while (once_again)
    {
	WidgetList children  = 0;
	Cardinal numChildren = 0;

	if (XtIsComposite(w))
	{
	    XtVaGetValues(w, XmNchildren, &children,
			  XmNnumChildren, &numChildren, XtPointer(0));
	}

	once_again = false;

	// Look for position within children.
	// In a form, we may have multiple children overlapping each
	// other.  Hence, search later children first, as they will be
	// on top of earlier ones.
	for (int m = int(numChildren) - 1; m >= 0; m--)
	{
	    Widget child = children[m];
	    if (XtIsRectObj(child) && XtIsManaged(child))
	    {
		Dimension cx, cy, cw, ch;
		XtVaGetValues(child,
			      XmNx, &cx, XmNy, &cy,
			      XmNwidth, &cw, XmNheight, &ch,
			      XtPointer(0));

		if (cx <= ex && ex <= (cx + cw) &&
		    cy <= ey && ey <= (cy + ch))
		{
		    // Position is within CHILD - restart search
		    once_again = true;
		    w = child;
		    ex -= cx;
		    ey -= cy;
		    break;
		}
	    }
	}
    }

    return w;
}

// Returns the widget the pointer is over, or 0 if it cannot be determined.
static Widget GetWidgetAt(XKeyEvent *e)
{
    return GetWidgetAt(XtWindowToWidget(e->display, e->window), e->x, e->y);
}

void HelpOnItemCB(Widget widget, XtPointer client_data, XtPointer call_data)
{
    Widget toplevel = findTheTopLevelShell(widget);

    if (call_tracking_help(call_data, true) || toplevel == 0)
    {
	HelpOnContextCB(widget, client_data, call_data);
	return;
    }

    Delay delay;		// Finding the widget may take time

    XmAnyCallbackStruct *cbs = (XmAnyCallbackStruct *)call_data;

    Widget item = GetWidgetAt(&cbs->event->xkey);
    if (item == 0)
	item = toplevel;

    ImmediateHelpCB(item, client_data, 0);
}


//-----------------------------------------------------------------------------
// Button tips
//-----------------------------------------------------------------------------

// True iff button tips are enabled.
static bool button_tips_enabled       = true;

// True iff text tips are enabled.
static bool text_tips_enabled         = true;

// True iff button docs are enabled.
static bool button_docs_enabled       = true;

// True iff text docs are enabled.
static bool text_docs_enabled         = true;

// The shell containing the tip label.
static Widget tip_shell               = 0;

// The tip label.
static Widget tip_label               = 0;

// The tip row; a RowColumn widget surrounding the label.
static Widget tip_row                 = 0;

// True if the button tip shell is raised.
static bool tip_popped_up             = false;

// The timer used for the delay until the tip is raised.
static XtIntervalId raise_tip_timer   = 0;

// The timer used for the delay until the tip is cleared.
static XtIntervalId clear_tip_timer   = 0;

// The timer used for the delay until the documentation is shown.
static XtIntervalId raise_doc_timer   = 0;

// The timer used for the delay until the documentation is cleared.
static XtIntervalId clear_doc_timer   = 0;

// Delay times (in ms)
int help_button_tip_delay = 750;  // Delay before raising button tip
int help_value_tip_delay  = 750;  // Delay before raising value tip
int help_button_doc_delay = 0;    // Delay before showing button doc
int help_value_doc_delay  = 0;    // Delay before showing value doc
int help_clear_doc_delay  = 1000; // Delay before clearing doc
int help_clear_tip_delay  = 50;   // Delay before clearing tip


// Helper: cancel the timer given in CLIENT_DATA
static void CancelTimer(Widget, XtPointer client_data, XtPointer)
{
    XtIntervalId timer = XtIntervalId(client_data);
    XtRemoveTimeOut(timer);
}

// Helper: cancel RAISE_DOC_TIMER
static void CancelRaiseDoc(Widget = 0, XtPointer = 0, XtPointer = 0)
{
    if (raise_doc_timer)
    {
	XtRemoveTimeOut(raise_doc_timer);
	raise_doc_timer = 0;
    }
}

// Helper: cancel RAISE_TIP_TIMER
static void CancelRaiseTip(Widget = 0, XtPointer = 0, XtPointer = 0)
{
    if (raise_tip_timer)
    {
	XtRemoveTimeOut(raise_tip_timer);
	raise_tip_timer = 0;
    }
}

// Event information passed through timeouts, etc.
struct TipInfo {
    XEvent event;		// The event structure
    Widget widget;		// The widget the event occurred in
};

// Raise button tip near the widget given in CLIENT_DATA
static void PopupTip(XtPointer client_data, XtIntervalId *timer)
{
    (void) timer;
    assert(*timer == raise_tip_timer);
    raise_tip_timer = 0;

    TipInfo *ti = (TipInfo *)client_data;
    Widget& w = ti->widget;

    if (w == 0)
	return;

    XtRemoveCallback(w, XmNdestroyCallback, CancelRaiseTip, 0);

    MString tip = get_tip_string(w, &ti->event);
    if (tip.isNull() || isNone(tip) || tip.isEmpty())
	return;

    if (!XtIsRealized(w))
	return;

    if (tip_shell == 0)
    {
	Arg args[10];
	int arg;

	arg = 0;
	XtSetArg(args[arg], XmNallowShellResize, true);             arg++;
	XtSetArg(args[arg], XmNx, WidthOfScreen(XtScreen(w)) + 1);  arg++;
	XtSetArg(args[arg], XmNy, HeightOfScreen(XtScreen(w)) + 1); arg++;
	XtSetArg(args[arg], XmNwidth,            10);               arg++;
	XtSetArg(args[arg], XmNheight,           10);               arg++;
	tip_shell = verify(XmCreateMenuShell(findTheTopLevelShell(w),
					     XMST("tipShell"), args, arg));

	arg = 0;
	XtSetArg(args[arg], XmNmarginWidth, 0);                     arg++;
	XtSetArg(args[arg], XmNmarginHeight, 0);                    arg++;
	XtSetArg(args[arg], XmNresizeWidth, True);                  arg++;
	XtSetArg(args[arg], XmNresizeHeight, True);                 arg++;
	XtSetArg(args[arg], XmNborderWidth, 0);                     arg++;
	XtSetArg(args[arg], XmNshadowThickness, 0);                 arg++;
	tip_row = verify(
	    XmCreateRowColumn(tip_shell, XMST("tipRow"), args, arg));
	XtManageChild(tip_row);

	arg = 0;
	XtSetArg(args[arg], XmNlabelString, tip.xmstring());        arg++;
	XtSetArg(args[arg], XmNrecomputeSize, true);                arg++;
	XtSetArg(args[arg], XmNalignment, XmALIGNMENT_BEGINNING);   arg++;
	tip_label = XmCreateLabel(tip_row, XMST("tipLabel"), args, arg);
	XtManageChild(tip_label);

	// Simple hack to ensure shell is realized
	XtPopup(tip_shell, XtGrabNone);
	XtPopdown(tip_shell);
    }

    if (lesstif_version <= 87)
    {
	// LessTif 0.87 and earlier fails to resize the shell properly
	// - the border width is zero.  Use this hack instead.
	XmFontList font_list;
	XtVaGetValues(tip_label, XmNfontList, &font_list, XtPointer(0));
    
	Dimension tip_width  = tip.width(font_list)  + 6;
	Dimension tip_height = tip.height(font_list) + 6;

	XtResizeWidget(tip_label, tip_width, tip_height, 0);
	XtResizeWidget(tip_row,   tip_width, tip_height, 0);
	XtResizeWidget(tip_shell, tip_width, tip_height, 1);
    }

    XtVaSetValues(tip_label, XmNlabelString, tip.xmstring(), XtPointer(0));

    // Find a possible place for the tip.  Consider the alignment of
    // the parent composite as well as the distance to the screen edge.

    //            TopLeft TopRight
    // LeftTop    XXXXXXXXXXXXXXXX RightTop
    //            XXXXXXXXXXXXXXXX
    // LeftBottom XXXXXXXXXXXXXXXX RightBottom
    //         BottomLeft BottomRight

    enum Placement { LeftTop, RightTop,
		     LeftBottom, RightBottom,
		     BottomLeft, BottomRight,
		     TopLeft, TopRight };

    static Placement last_placement = BottomRight;
    static Widget last_parent = 0;

    Widget parent = XtParent(w);
    if (parent != last_parent || XmIsText(w))
	last_placement = BottomRight;

    // Use 18 runs to find out the best position:
    // Run 0: try last placement in same alignment
    // Run 1-8: try at various sides of W.  Don't hide other widgets
    //          and don't move tip off the screen.
    // Run 9-17: same as 0-8, but don't care for hiding other widgets.
    for (int run = 0; run < 18; run++)
    {
	Placement placement = last_placement;
	switch (run % 9)
	{
	case 0: placement = last_placement; break;
	case 1: placement = BottomRight; break;
	case 2: placement = RightBottom; break;
	case 3: placement = RightTop;    break;
	case 4: placement = TopRight;    break;
	case 5: placement = BottomLeft;  break;
	case 6: placement = LeftBottom;  break;
	case 7: placement = LeftTop;     break;
	case 8: placement = TopLeft;     break;
	}

	bool ok = false;

	if (XmIsRowColumn(parent))
	{
	    // We're part of a button box: if vertically aligned, place
	    // tip on the right; otherwise, place it at the bottom.
	    unsigned char orientation = XmHORIZONTAL;
	    XtVaGetValues(parent, XmNorientation, &orientation, XtPointer(0));

	    switch (placement)
	    {
	    case BottomLeft:
	    case BottomRight:
	    case TopLeft:
	    case TopRight:
		if (orientation == XmHORIZONTAL)
		    ok = true;
		break;

	    case LeftBottom:
	    case LeftTop:
	    case RightBottom:
	    case RightTop:
		if (orientation == XmVERTICAL)
		    ok = true;
		break;
	    }
	}
	else if (XmIsForm(parent))
	{
	    // We're part of a form: try to place the tip beyond a form
	    // boundary.

	    int fraction_base = 100;
	    XtVaGetValues(parent, 
			  XmNfractionBase, &fraction_base,
			  XtPointer(0));
	    if (fraction_base == 100)
	    {
		// Simple form
		ok = true;
	    }
	    else
	    {
		// Command tool or likewise
		unsigned char left_attachment   = XmATTACH_NONE;
		unsigned char right_attachment  = XmATTACH_NONE;
		unsigned char top_attachment    = XmATTACH_NONE;
		unsigned char bottom_attachment = XmATTACH_NONE;

		XtVaGetValues(w, 
			      XmNleftAttachment,   &left_attachment,
			      XmNrightAttachment,  &right_attachment,
			      XmNtopAttachment,    &top_attachment,
			      XmNbottomAttachment, &bottom_attachment,
			      XtPointer(0));

		int left_position   = 0;
		int right_position  = 0;
		int top_position    = 0;
		int bottom_position = 0;

		XtVaGetValues(w, 
			      XmNleftPosition,   &left_position,
			      XmNrightPosition,  &right_position,
			      XmNtopPosition,    &top_position,
			      XmNbottomPosition, &bottom_position,
			      XtPointer(0));

		switch (placement)
		{
		case BottomLeft:
		case BottomRight:
		    if (bottom_attachment == XmATTACH_POSITION
			&& bottom_position >= fraction_base)
			ok = true;
		    if (bottom_attachment == XmATTACH_FORM
			|| top_attachment == XmATTACH_OPPOSITE_FORM)
			ok = true;
		    break;

		case RightBottom:
		case RightTop:
		    if (right_attachment == XmATTACH_POSITION
			&& right_position >= fraction_base)
			ok = true;
		    if (right_attachment == XmATTACH_FORM
			|| left_attachment == XmATTACH_OPPOSITE_FORM)
			ok = true;
		    break;

		case TopLeft:
		case TopRight:
		    if (top_attachment == XmATTACH_POSITION
			&& top_position == 0)
			ok = true;
		    if (top_attachment == XmATTACH_FORM
			|| bottom_attachment == XmATTACH_OPPOSITE_FORM)
			ok = true;
		    break;

		case LeftBottom:
		case LeftTop:
		    if (left_attachment == XmATTACH_POSITION
			&& left_position == 0)
			ok = true;
		    if (left_attachment == XmATTACH_FORM
			|| right_attachment == XmATTACH_OPPOSITE_FORM)
			ok = true;
		    break;
		}
	    }
	}
	else
	{
	    // Any other alignment
	    ok = true;
	}

	if (!ok && run <= 8)
	    continue;

	// Don't move tip off the screen
	XWindowAttributes w_attributes;
	XGetWindowAttributes(XtDisplay(w), XtWindow(w), &w_attributes);

	if (XmIsText(w))
	{
	    w_attributes.width  = 0;
	    w_attributes.height = 0;
	}

	XtWidgetGeometry tip_geometry;
	tip_geometry.request_mode = CWHeight | CWWidth;
	XtQueryGeometry(tip_shell, (XtWidgetGeometry *)0, &tip_geometry);

	int x_offset = 5;
	int y_offset = 5;

	int dx = 0;
	switch (placement)
	{
	case LeftBottom:
	case LeftTop:
	    dx = -(tip_geometry.width + x_offset);
	    break;
	case RightBottom:
	case RightTop:
	    dx = w_attributes.width + x_offset;
	    break;
	case BottomLeft:
	case TopLeft:
	    dx = w_attributes.width / 2 - tip_geometry.width;
	    break;
	case BottomRight:
	case TopRight:
	    dx = w_attributes.width / 2;
	    break;
	}

	int dy = 0;
	switch (placement)
	{
	case LeftBottom:
	case RightBottom:
	    dy = w_attributes.height / 2;
	    break;
	case LeftTop:
	case RightTop:
	    dy = w_attributes.height / 2 - tip_geometry.height;
	    break;
	case BottomLeft:
	case BottomRight:
	    dy = w_attributes.height + y_offset;
	    break;
	case TopLeft:
	case TopRight:
	    dy = -(tip_geometry.height + y_offset);
	    break;
	}

	if (XmIsText(w))
	{
	    BoxPoint pos = point(&ti->event);
	    dx += pos[X];
	    dy += pos[Y];
	}

	// Don't move tip off the screen
	Window w_child;
	int x, y;
	XTranslateCoordinates(XtDisplay(w), XtWindow(w), w_attributes.root,
			      dx, dy, &x, &y, &w_child);
	if (x < 0)
	    continue;
	if (y < 0)
	    continue;
	if (x + tip_geometry.width >= WidthOfScreen(XtScreen(w)))
	    continue;
	if (y + tip_geometry.height >= HeightOfScreen(XtScreen(w)))
	    continue;

	// Move tip to X, Y...
	XtVaSetValues(tip_shell,
		      XmNx, x,
		      XmNy, y,
		      XtPointer(0));
	
	// and pop it up.
	XtPopup(tip_shell, XtGrabNone);
	tip_popped_up = true;
	last_placement = placement;
	last_parent    = parent;

	return;
    }
}

// Show the documentation for the widget given in CLIENT_DATA
static void ShowDocumentation(XtPointer client_data, XtIntervalId *timer)
{
    (void) timer;
    assert(*timer == raise_doc_timer);
    raise_doc_timer = 0;

    TipInfo *ti = (TipInfo *)client_data;
    XtRemoveCallback(ti->widget, XmNdestroyCallback, CancelRaiseDoc, 0);

    if (DisplayDocumentation != 0 
	&& (XmIsText(ti->widget) ? text_docs_enabled : button_docs_enabled))
    {
	// Display documentation
	MString doc = get_documentation_string(ti->widget, &ti->event);
	DisplayDocumentation(doc);
    }
}

// Clear the documentation
static void ClearDocumentationNow(Widget w);

static void CancelClearDocumentation(Widget w, XtPointer, XtPointer)
{
    if (clear_doc_timer == 0)
	return;

    XtRemoveTimeOut(clear_doc_timer);
    clear_doc_timer = 0;
    ClearDocumentationNow(w);
}

static void ClearDocumentationNow(Widget w)
{
    if (DisplayDocumentation != 0 
	&& (XmIsText(w) ? text_docs_enabled : button_docs_enabled))
    {
	// Clear documentation
	static MString empty(0, true);
	DisplayDocumentation(empty);
    }
}

static void ClearDocumentation(XtPointer client_data, XtIntervalId *timer)
{
    (void) timer;
    assert(*timer == clear_doc_timer);
    clear_doc_timer = 0;

    TipInfo *ti = (TipInfo *)client_data;

    XtRemoveCallback(ti->widget, XmNdestroyCallback, 
		     CancelClearDocumentation, 0);
    ClearDocumentationNow(ti->widget);
}

// Clear tips and documentation
static void ClearTip(Widget w, XEvent *event)
{
    CancelRaiseTip();
    CancelRaiseDoc();

    if (tip_popped_up)
    {
	XtPopdown(tip_shell);
	tip_popped_up = false;
    }

    if (clear_doc_timer)
    {
	XtRemoveTimeOut(clear_doc_timer);
	clear_doc_timer = 0;
    }

    if (DisplayDocumentation != 0 
	&& (XmIsText(w) ? text_docs_enabled : button_docs_enabled))
    {
	// We don't clear the documentation immediately, since the
	// user might be moving over to another button, and we don't
	// want flashing documentation strings.

	static TipInfo ti;
	ti.event  = *event;
	ti.widget = w;

	clear_doc_timer =
	    XtAppAddTimeOut(XtWidgetToApplicationContext(w),
			    help_clear_doc_delay, 
			    ClearDocumentation, XtPointer(&ti));

	// Should the button be destroyed beforehand, cancel timeout
	XtRemoveCallback(w, XmNdestroyCallback, CancelClearDocumentation, 0);
	XtAddCallback   (w, XmNdestroyCallback, CancelClearDocumentation, 0);
    }
}

// Raise tips and documentation
static void RaiseTip(Widget w, XEvent *event)
{
    if (DisplayDocumentation != 0
	&& (XmIsText(w) ? text_docs_enabled : button_docs_enabled))
    {
	// No need to clear the documentation
	if (clear_doc_timer)
	{
	    XtRemoveTimeOut(clear_doc_timer);
	    clear_doc_timer = 0;
	}

	static TipInfo ti;
	ti.event  = *event;
	ti.widget = w;

	int doc_delay = 
	    XmIsText(w) ? help_value_doc_delay : help_button_doc_delay;

	CancelRaiseDoc();

	raise_doc_timer =
	    XtAppAddTimeOut(XtWidgetToApplicationContext(w),
			    doc_delay,
			    ShowDocumentation, XtPointer(&ti));

	// Should W be destroyed beforehand, cancel timeout
	XtAddCallback(w, XmNdestroyCallback, CancelRaiseDoc, 0);
    }

    if (XmIsText(w) ? text_tips_enabled : button_tips_enabled)
    {
	static TipInfo ti;
	ti.event  = *event;
	ti.widget = w;

	int tip_delay = 
	    XmIsText(w) ? help_value_tip_delay : help_button_tip_delay;

	CancelRaiseTip();

	raise_tip_timer = 
	    XtAppAddTimeOut(XtWidgetToApplicationContext(w),
			    tip_delay,
			    PopupTip, XtPointer(&ti));

	// Should W be destroyed beforehand, cancel timeout
	XtAddCallback(w, XmNdestroyCallback, CancelRaiseTip, 0);
    }
}

static void DoClearTip(XtPointer client_data, XtIntervalId *timer)
{
    (void) timer;
    assert(*timer == clear_tip_timer);
    clear_tip_timer = 0;

    TipInfo& ti = *((TipInfo *)client_data);
    ClearTip(ti.widget, &ti.event);
}


// Widget W has been entered or left.  Handle event.
static void HandleTipEvent(Widget w,
			   XtPointer /* client_data */,
			   XEvent *event, 
			   Boolean * /* continue_to_dispatch */)
{
    static Widget last_left_widget = 0;

    switch (event->type)
    {
    case EnterNotify:
    {
	if (clear_tip_timer != 0)
	{
	    XtRemoveTimeOut(clear_tip_timer);
	    clear_tip_timer  = 0;

	    if (w != last_left_widget)
	    {
		// Entered other widget within HELP_CLEAR_TIP_DELAY.
		ClearTip(w, event);
		last_left_widget = 0;
	    }
	    else
	    {
		// Re-entered same widget -- ignore.
		last_left_widget = 0;
		break;
	    }
	}

	if (!XmIsText(w))
	{
	    // Clear and re-raise tip
	    ClearTip(w, event);
	    RaiseTip(w, event);
	}
    }
    break;

    case LeaveNotify:
    {
	last_left_widget = w;
	if (clear_tip_timer != 0)
	{
	    XtRemoveTimeOut(clear_tip_timer);
	    clear_tip_timer = 0;
	}

	// We don't clear the tip immediately, because the DDD ungrab
	// mechanism may cause the pointer to leave a button and
	// re-enter it immediately.
	static TipInfo ti;
	ti.event  = *event;
	ti.widget = w;

	clear_tip_timer = 
	    XtAppAddTimeOut(XtWidgetToApplicationContext(w),
			    help_clear_tip_delay, 
			    DoClearTip, &ti);
    }
    break;

    case KeyPress:
    case KeyRelease:
    case ButtonPress:
    case ButtonRelease:
	ClearTip(w, event);
	break;

    case MotionNotify:
	if (XmIsText(w))
	{
	    static Widget last_motion_widget           = 0;
	    static XmTextPosition last_motion_position = XmTextPosition(-1);
	    XmTextPosition pos = TextPosOfEvent(w, event);

	    if (w != last_motion_widget || pos != last_motion_position)
	    {
		last_motion_widget   = w;
		last_motion_position = pos;

		ClearTip(w, event);
		if (pos != XmTextPosition(-1))
		    RaiseTip(w, event);
	    }
	}
        break;
    }
}


// (Un)install toolbar tips for W
static void InstallButtonTipEvents(Widget w, bool install)
{
    // Install handler only if either `tipString' or
    // `documentationString' resource is specified
    tip_resource_values tip_values;
    XtGetApplicationResources(w, &tip_values, 
			      tip_subresources, XtNumber(tip_subresources), 
			      ArgList(0), 0);

    doc_resource_values doc_values;
    XtGetApplicationResources(w, &doc_values, 
			      doc_subresources, XtNumber(doc_subresources), 
			      ArgList(0), 0);

    if (tip_values.tipString != 0 || doc_values.documentationString != 0)
    {
	EventMask event_mask = 
	    EnterWindowMask | LeaveWindowMask | ButtonPress | ButtonRelease 
	    | KeyPress | KeyRelease;
	if (install)
	{
	    XtAddEventHandler(w, event_mask, False, 
			      HandleTipEvent, XtPointer(0));

	}
	else
	{
	    XtRemoveEventHandler(w, event_mask, False, 
				 HandleTipEvent, XtPointer(0));
	}
    }

    // CvtStringToXmString and friends should be responsible for
    // the memory management.
    // XmStringFree(tip_values.tipString);
    // tip_values.tipString = 0;
    // XmStringFree(doc_values.documentationString);
    // doc_values.documentationString = 0;
}


// (Un)install toolbar tips for W and all its descendants
static void InstallButtonTipsNow(Widget w, bool install)
{
    if (w == 0 || !XtIsWidget(w))
	return;

    // Install tips for this widget
    InstallButtonTipEvents(w, install);

    if (XtIsComposite(w))
    {
	// Traverse widget tree
	WidgetList children   = 0;
	Cardinal num_children = 0;

	XtVaGetValues(w,
		      XtNchildren, &children,
		      XtNnumChildren, &num_children,
		      XtPointer(0));

	if (children != 0)
	    for (int i = 0; i < int(num_children); i++)
		InstallButtonTipsNow(children[i], install);
    }

    if (XmIsCascadeButton(w))
    {
	// Traverse the menu associated with this button
	Widget subMenuId = 0;
	XtVaGetValues(w, XmNsubMenuId, &subMenuId, XtPointer(0));
	if (subMenuId != 0)
	    InstallButtonTipsNow(subMenuId, install);
    }

    if (XmIsRowColumn(w))
    {
	// Traverse the menu associated with this option menu
	unsigned char rowColumnType = XmWORK_AREA;
	Widget subMenuId = 0;
	XtVaGetValues(w, 
		      XmNsubMenuId, &subMenuId,
		      XmNrowColumnType, &rowColumnType,
		      XtPointer(0));
	if (rowColumnType == XmMENU_OPTION && subMenuId != 0)
	    InstallButtonTipsNow(subMenuId, install);
    }
}

// Callback funtion: install tips now.
static void InstallButtonTipsTimeOut(XtPointer client_data,
				     XtIntervalId *timer)
{
    Widget w = Widget(client_data);
    XtRemoveCallback(w, XmNdestroyCallback, 
		     CancelTimer, XtPointer(*timer));
    InstallButtonTipsNow(w, true);
}

// Callback funtion: uninstall tips now.
static void UnInstallButtonTipsTimeOut(XtPointer client_data, 
				       XtIntervalId *timer)
{
    Widget w = Widget(client_data);
    XtRemoveCallback(w, XmNdestroyCallback, 
		     CancelTimer, XtPointer(*timer));
    InstallButtonTipsNow(w, false);
}

// (Un)install tips for W and all its descendants.
// We do this as soon as we are back in the event loop, since we
// assume all children have been created until then.
void InstallButtonTips(Widget w, bool install)
{
    XtIntervalId timer;

    if (install)
	timer = XtAppAddTimeOut(XtWidgetToApplicationContext(w), 0,
				InstallButtonTipsTimeOut, XtPointer(w));
    else
	timer = XtAppAddTimeOut(XtWidgetToApplicationContext(w), 0,
				UnInstallButtonTipsTimeOut, XtPointer(w));

    // Should W be destroyed beforehand, cancel installation.
    XtAddCallback(w, XmNdestroyCallback, CancelTimer, XtPointer(timer));
}

// Enable or disable button tips
void EnableButtonTips(bool enable)
{
    button_tips_enabled = enable;
}

// Enable or disable button docs
void EnableButtonDocs(bool enable)
{
    button_docs_enabled = enable;
}



//-----------------------------------------------------------------------------
// Text tips.
//-----------------------------------------------------------------------------

// (Un)install text tips for W.
void InstallTextTips(Widget w, bool install)
{
    EventMask event_mask = EnterWindowMask | LeaveWindowMask 
	| ButtonPress | ButtonRelease | PointerMotionMask
	| KeyPress | KeyRelease;

    if (install)
    {
	XtAddEventHandler(w, event_mask, False, 
			  HandleTipEvent, XtPointer(0));

    }
    else
    {
	XtRemoveEventHandler(w, event_mask, False, 
			     HandleTipEvent, XtPointer(0));
    }
}

// Enable or disable text tips
void EnableTextTips(bool enable)
{
    text_tips_enabled = enable;
}

// Enable or disable text docs
void EnableTextDocs(bool enable)
{
    text_docs_enabled = enable;
}
