view src/menu.cc @ 25:8eaf72e2041b

Reindent the source using GNU indent and "indent -i4 -bli0 -npcs -nprs -npsl". Fix the problems introduced afterwards.
author Matti Hamalainen <ccr@tnsp.org>
date Sat, 24 Sep 2011 14:16:04 +0300
parents 07c68836db71
children a68786b9c74b
line wrap: on
line source

/*
 *	menu.cc
 *	AYM 1998-12-01
 */


/*
This file is part of Yadex.

Yadex incorporates code from DEU 5.21 that was put in the public domain in
1994 by Raphaël Quinet and Brendon Wyber.

The rest of Yadex is Copyright © 1997-2003 André Majorel and others.

This program 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 2 of the License, or (at your option) any later
version.

This program 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
this program; if not, write to the Free Software Foundation, Inc., 59 Temple
Place, Suite 330, Boston, MA 02111-1307, USA.
*/


/*
These are the pop-up and pull-down menus.

Events that are not understood (E.G. [Left] and [Right])
are put back in the input buffer before the
function returns.
*/


#include "yadex.h"
#include "events.h"
#include "gfx.h"
#include "menu.h"
#include "menudata.h"


const char *MI_SEPARATION = "";

const unsigned char MIF_MACTIVE = 0x03;
const unsigned char MIF_MTICK = 0x0c;
const unsigned char MIF_SEPAR = 0x10;

const short VSPACE = 2;		// Pixels between items


/* 
 *	Menu_item - a menu item class
 */
class Menu_item
{
    public: Menu_item()
    {
	flags = MIF_NTICK | MIF_NACTIVE;
	tilde_key = YK_;
	shortcut_key = YK_;
	str = 0;
    }

    unsigned char flags;	// Logical-or of MIF_?ACTIVE MIF_?TICK MIF_SEPAR
    inpev_t tilde_key;
    inpev_t shortcut_key;
    short y;			// Top of item, relative to ly0. If there is a
    // separation, it is above y.
    const char *str;
    union
    {
	bool s;			// Static
	bool *v;		// Pointer to variable
	struct
	{
	    micb_t f;		// Pointer to callback function
	    micbarg_t a;	// Argument
	} f;
    }
    tick;
    union
    {
	bool s;			// Static
	bool *v;		// Pointer to variable
	struct
	{
	    micb_t f;		// Pointer to callback function
	    micbarg_t a;	// Argument
	} f;
    }
    active;
};


/*
 *	Menu_priv - Menu's private data
 */
class Menu_priv
{
    public:Menu_priv()
    {
	flags = 0;
	title = 0;
	title_len = 0;
	items_ks_len = 0;
	items_len = 0;
	items.clear();
	user_ox0 = -1;
	user_oy0 = -1;
	ox0 = -1;
	oy0 = -1;
	need_geom = true;
	visible = false;
	visible_disp = false;
	line = 0;
	line_disp = UINT_MAX;
	item_height = FONTH + VSPACE;
    }

    void vinit(Menu & m, const char *title, va_list argp);
    void cook();
    void draw();
    void draw_one_line(size_t line, bool highlighted);
    void geom();
    int process_event(const input_status_t * is);

    // Menu data
    unsigned char flags;	// MF_MENUDATA
    //          The strings come from a Menu_data.
    // MF_POPUP Used as popup (not pull-down) menu
    //          If set, the title is shown and
    //          button releases are treated
    //          differently.
    // MF_TICK  At least one item can be ticked
    //          (has the MIF_[SVF]TICK flag).
    // MF_SHORTCUT  At least one item has a key
    //          shortcut key.
    // MF_NUMS  Force the use of standard
    //          shortcuts [0-9a-zA-Z] even if
    //          index/key shortcuts exist.
    const char *title;		// NULL if no title
    size_t title_len;		// Length of <title> (or 0 if none)
    size_t items_ks_len;	// Length of the longest item counting key sc.
    size_t items_len;		// Length of the longest item not counting k.s.
    const Menu_data *menudata;
    std::vector < Menu_item > items;

    // Geometry as the user would like it to be
    int user_ox0;		// Top left corner. < 0 means centered.
    int user_oy0;		// Top left corner. < 0 means centered.

    // Geometry as it should be
    int ix0, iy0, ix1, iy1;	// Corners of inner edge of window
    int ox0, oy0, ox1, oy1;	// Corners of outer edge of window
    int width;			// Overall width of window
    int height;			// Overall height of window
    int ty0;			// Y-pos of title
    int ly0;			// Y-pos of first item

    // Geometry as it is displayed
    int ox0_disp;
    int oy0_disp;
    int width_disp;
    int height_disp;

    // Status
    bool need_geom;		// Need to call geom() again
    bool visible;		// Should the menu be visible ?
    bool visible_disp;		// Is it really visible ?
    size_t line;		// Which line should be highlighted ?
    size_t line_disp;		// Which line is actually highlighted ?
    inpev_t _last_shortcut_key;	// Shortcut key for last selected item.

    // Misc
    unsigned short item_height;
};


/* First subscript :  0 = normal, 1 = greyed out
   Second subscript : 0 = normal, 1 = highlighted */
const colour_pair_t menu_colour[2][2] = {
    {
     {WINBG, WINFG},		// Normal entry
     {WINBG_HL, WINFG_HL}	// Normal entry, highlighted
     },
    {
     {WINBG, WINFG_DIM},	// Greyed out entry
     {WINBG_HL, WINFG_DIM_HL}	// Greyed out entry, highlighted
     }
};

const unsigned char MF_POPUP = 0x01;
const unsigned char MF_TICK = 0x02;
const unsigned char MF_TILDE = 0x04;
const unsigned char MF_SHORTCUT = 0x08;
const unsigned char MF_NUMS = 0x10;
const unsigned char MF_MENUDATA = 0x20;


/*
 *	Menu::Menu - ctor from arguments in variable number
 *
 *	Another ctor for Menu, suited to the creation of pull
 *	down menus.
 *
 *	Expects the title of the menu followed by repeats of
 *	either of the following sets of arguments, in any order :
 *
 *	1) Menu item
 *
 *	  A menu item is made of 3 or more arguments :
 *
 *	    const char *
 *			A NUL-terminated string that will be
 *			used to display the item. The first
 *			occurrence of a tilde ("~") in the
 *			string indicates that the following
 *			character will be used as a local
 *			shortcut for this item.
 *
 *			That string must remain valid and should
 *			not change during the lifetime of the
 *			Menu object.
 *
 *	    inpev_t	The global shortcut key associated to
 *			this item. That key will be displayed on
 *			the right of the menu.
 *
 *	    <option> ...
 *			Zero or more options.
 *
 *	    (unsigned char) 0
 *			The end of the item.
 *
 *	  Where <option> is any of the following pairs of
 *	  arguments :
 *
 *	    (unsigned char) MIF_STICK
 *	    bool ticked
 *			Indicates that this item can be ticked.
 *			If <ticked> is true, the item will be
 *			initially ticked. To change the state of
 *			the item, call set_ticked().
 *
 *	    (unsigned char) MIF_VTICK
 *	    bool *ticked
 *			Indicates that this item can be ticked.
 *			The argument is a pointer to a boolean
 *			variable. Whenever this item is
 *			displayed, the variable is read and the
 *			item is shown as ticked or unticked
 *			depending on whether the function
 *			returned true or false.
 *
 *	    (unsigned char) MIF_FTICK
 *	    boolcb_t ticked
 *	    void *arg
 *			Indicates that this item can be ticked.
 *			The argument is a pointer to a function
 *			taking only one argument (<arg>) and
 *			returning bool.  Whenever this item is
 *			displayed, the function is called and
 *			the item is shown as ticked or unticked
 *			depending on whether the function
 *			returned true or false.
 *
 *	    (unsigned char) MIF_SACTIVE
 *	    bool active
 *			Indicates that this item can be greyed
 *			out. If <active> is false the item
 *			will be initially greyed out. To change
 *			the state of the item, call
 *			set_active().
 *
 *	    (unsigned char) MIF_VACTIVE
 *	    bool *active
 *			Indicates that this item can be greyed
 *			out. The argument is a pointer to a
 *			boolean variable. Whenever this item is
 *			displayed, the variable is read and the
 *			item is shown as greyed out or not
 *			depending on whether the function
 *			returned false or true.
 *
 *	    (unsigned char) MIF_FACTIVE
 *	    boolcb_t active
 *			Indicates that this item can be greyed
 *			out. The argument is a pointer to a
 *			function taking only one argument
 *			(<arg>) and returning bool. Whenever
 *			this item is displayed, the function is
 *			called and the item is shown as greyed
 *			out or not depending on whether the
 *			function returned false or true.
 *
 *	2) Separation
 *
 *	  A separation is made of exactly 1 argument :
 *
 *	    (const char *) MI_SEPARATION
 *
 *	Pass a NULL pointer after the last group of arguments.
 *
 *	For a given item, the options MIF_STICK, MIF_VTICK and
 *	MIF_FTICK are mutually exclusive. For a given item, the
 *	options MIF_SACTIVE, MIF_VACTIVE and MIF_FACTIVE are
 *	mutually exclusive.
 *
 *	A MI_SEPARATION argument at the end of the list (i.e.
 *	not followed by an item) is ignored.
 *
 *	The ticked and greyed out states can change at any time
 *	but will not be reflected until the menu is redrawn from
 *	scratch.
 */
Menu::Menu(const char *title, ...)
{
    priv = new Menu_priv;
    va_list argp;
    va_start(argp, title);
    priv->vinit(*this, title, argp);
    va_end(argp);
    priv->cook();
}


/*
 *	Menu::Menu - ctor from an argument list
 *
 *	This is the same thing as Menu::Menu (const char
 *	*title, ...) except that it expects a pointer on the
 *	list of arguments.
 */
Menu::Menu(const char *title, va_list argp)
{
    priv = new Menu_priv;
    priv->vinit(*this, title, argp);
    priv->cook();
}


/*
 *	Menu::Menu - ctor from a list
 *
 *	Another ctor for Menu, suited to the creation of the
 *	menu from a list.
 */
Menu::Menu(const char *title,
	   al_llist_t * list, const char *(*getstr) (void *))
{
    priv = new Menu_priv;
    set_title(title);
    size_t nitems = y_min(al_lcount(list), 100);
    priv->items.resize(nitems);
    size_t line;
    for (al_lrewind(list), line = 0;
	 !al_leol(list) && line < nitems; al_lstep(list), line++)
	priv->items[line].str = getstr(al_lptr(list));
    priv->cook();
}


/*
 *	Menu::Menu - ctor from a Menu_data
 */
Menu::Menu(const char *title, const Menu_data & menudata)
{
    priv = new Menu_priv;
    priv->menudata = &menudata;
    priv->flags |= MF_MENUDATA;
    set_title(title);
    size_t nitems = menudata.nitems();
    priv->items.resize(nitems);
    priv->cook();
}


/*
 *	Menu::~Menu - dtor
 */
Menu::~Menu()
{
    delete priv;
}


/*
 *	Menu_priv::vinit - initialize the menu from an argument list
 */
void Menu_priv::vinit(Menu & m, const char *title, va_list argp)
{
    bool tick = false;

    m.set_title(title);

    while (items.size() < 100)
    {
	Menu_item i;

	const char *str = va_arg(argp, const char *);
	while (str == MI_SEPARATION)
	{
	    i.flags |= MIF_SEPAR;
	    str = va_arg(argp, const char *);
	}
	if (str == 0)
	    break;

	i.str = str;
	i.shortcut_key = (inpev_t) va_arg(argp, int);
	unsigned char flag;
	while ((flag = (unsigned char) va_arg(argp, int)) != 0)
	{
	    if (flag == MIF_SACTIVE)
	    {
		i.flags = (i.flags & ~MIF_MACTIVE) | MIF_SACTIVE;
		i.active.s = (bool) va_arg(argp, int);
	    }
	    else if (flag == MIF_VACTIVE)
	    {
		i.flags = (i.flags & ~MIF_MACTIVE) | MIF_VACTIVE;
		i.active.v = va_arg(argp, bool *);
	    }
	    else if (flag == MIF_FACTIVE)
	    {
		i.flags = (i.flags & ~MIF_MACTIVE) | MIF_FACTIVE;
		i.active.f.f = va_arg(argp, micb_t);
		i.active.f.a = va_arg(argp, micbarg_t);
	    }
	    else if (flag == MIF_STICK)
	    {
		tick = true;
		i.flags = (i.flags & ~MIF_MTICK) | MIF_STICK;
		i.tick.s = (bool) va_arg(argp, int);
	    }
	    else if (flag == MIF_VTICK)
	    {
		tick = true;
		i.flags = (i.flags & ~MIF_MTICK) | MIF_VTICK;
		i.tick.v = va_arg(argp, bool *);
	    }
	    else if (flag == MIF_FTICK)
	    {
		tick = true;
		i.flags = (i.flags & ~MIF_MTICK) | MIF_FTICK;
		i.tick.f.f = va_arg(argp, micb_t);
		i.tick.f.a = va_arg(argp, micbarg_t);
	    }
	    else if (flag != 0)
	    {
		nf_bug("Menu::ctor: flag %d", (int) flag);
	    }
	}
	items.push_back(i);
    }

    if (tick)
	flags |= MF_TICK;
}


/*
 *	Menu_priv::cook - parse the menu item strings
 *
 *	Compute items_len, items_ks_len and prepare the cooked
 *	tilde shortcuts.
 */
void Menu_priv::cook()
{
    items_len = 0;
    items_ks_len = 0;
    short y = 0;

    for (size_t line = 0; line < items.size(); line++)
    {
	Menu_item & i = items[line];
	if (i.shortcut_key != YK_)
	    flags |= MF_SHORTCUT;
	if ((i.flags & MIF_MTICK) != MIF_NTICK)
	    flags |= MF_TICK;
	if (i.flags & MIF_SEPAR)
	    y += 2 * (NARROW_VSPACING + NARROW_BORDER);
	i.y = y;
	y += item_height;
	size_t len = 0;
	i.tilde_key = YK_;
	const char *str = (flags & MF_MENUDATA) ? (*menudata)[line] : i.str;
	for (const char *p = str; *p != '\0'; p++)
	    if (p[0] == '~' && p[1] != '\0' && i.tilde_key == YK_)
	    {
		i.tilde_key = tolower(p[1]);
		flags |= MF_TILDE;
	    }
	    else
		len++;
	size_t len_ks = len;
	if (i.shortcut_key != YK_)
	    len_ks += strlen(key_to_string(i.shortcut_key)) + 2;
	if (len > items_len)
	    items_len = len;
	if (len_ks > items_ks_len)
	    items_ks_len = len_ks;
    }

    if (flags & MF_TICK)
    {
	items_len += 2;		// Tick mark
	items_ks_len += 2;
    }
    if (flags & MF_SHORTCUT)
    {
	items_len += 4;		// Space between strings and shortcut
	items_ks_len += 4;
    }
    if (!(flags & MF_TILDE) && !(flags & MF_SHORTCUT))
	flags |= MF_NUMS;
    if (flags & MF_NUMS)
    {
	items_len += 4;		// [1-9a-zA-Z] prefix
	items_ks_len += 4;
    }
}


/*
 *	Menu::set_title - set the title
 *
 *	Set the title of the menu (it's ignored unless the menu
 *	is set in popup mode).
 *
 *	Bug: changing the title does not take effect until the
 *	next display from scratch.
 */
void Menu::set_title(const char *title)
{
    priv->title = title;
    size_t title_len = title ? strlen(title) : 0;

    /* If the length of the title has changed,
       force geom() to be called again. */
    if (title_len != priv->title_len)
	priv->need_geom = true;

    priv->title_len = title_len;
}


/*
 *	Menu::set_coords - position or reposition the menu window.
 *
 *	(<x0>,<y0>) is the top left corner.
 *	If <x0> is < 0, the window is horizontally centred.
 *	If <y0> is < 0, the window is vertically centred.
 */
void Menu::set_coords(int x0, int y0)
{
    if (x0 != priv->ox0 || y0 != priv->oy0)
	priv->need_geom = true;	// Force geom() to be called

    priv->user_ox0 = x0;
    priv->user_oy0 = y0;
}


/*
 *	Menu::set_item_no - set the current line
 *
 *	The current line number is set to <item_no>. The first
 *	line bears number 0.
 */
void Menu::set_item_no(int item_no)
{
    priv->line = item_no;
}


/*
 *	Menu::set_popup - set the popup flag
 *
 *	If <popup> is true, the popup flag is set. If <popup> is
 *	false, the popup flag is cleared.
 */
void Menu::set_popup(bool popup)
{
    if (popup != ! !(priv->flags & MF_POPUP))
	priv->need_geom = true;	// Force geom() to be called
    if (popup)
	priv->flags |= MF_POPUP;
    else
	priv->flags &= ~MF_POPUP;
}


/*
 *	Menu::set_force_numbers - set the force_numbers flags
 *
 *	If <force_numbers> is true, the force_numbers flag is
 *	set. If <force_numbers> is false, the force_numbers flag
 *	is cleared.
 *
 *	The effect of the <force_numbers> flag is to disable key
 *	shortcuts and tilde shortcuts and to add automatic
 *	numbering of items ([1-9a-zA-Z]).
 *
 *	If none of the items has a tilde or key shortcut,
 *	<force_numbers> is automatically set. Otherwise, is it
 *	off by default.
 */
void Menu::set_force_numbers(bool force_numbers)
{
    if (force_numbers != ! !(priv->flags & MF_NUMS))
	priv->need_geom = true;	// Force geom() to be called.
    if (force_numbers)
	priv->flags |= MF_NUMS;
    else
	priv->flags &= ~MF_NUMS;
}


/*
 *	Menu::set_visible - set the visible flag
 *
 *	If <visible> is true, the visible flag is set. If
 *	<visible> is false, the visible flag is cleared.
 */
void Menu::set_visible(bool visible)
{
    priv->visible = visible;
}


/*
 *	Menu::set_ticked - tick or untick a menu item
 *
 *	If <ticked> is true, item number <item_no> is ticked. If
 *	<ticked> is false, item number <item_no> is unticked.
 *	If the menu item was not created with the MIF_STICK
 *	option, emit a warning and return without doing
 *	anything.
 */
void Menu::set_ticked(size_t item_no, bool ticked)
{
    if (item_no >= priv->items.size())
    {
	nf_bug("Menu::set_ticked: item_no %lu", (unsigned long) item_no);
	return;
    }
    Menu_item & i = priv->items[item_no];
    if ((i.flags & MIF_MTICK) != MIF_STICK)
    {
	nf_bug("Menu::set_ticked: flags %02X", i.flags);
	return;
    }
    i.tick.s = ticked;
}


/*
 *	Menu::set_active - grey-out or ungrey-out a menu
 *
 *	If <active> is false, item number <item_no> becomes
 *	greyed out. If <active> is true, item number <item_no>
 *	ceases to be greyed out. If the item was not created
 *	with with the MIF_SACTIVE option, emit a warning and
 *	return without doing anything.
 */
void Menu::set_active(size_t item_no, bool active)
{
    if (item_no >= priv->items.size())
    {
	nf_bug("Menu::set_active: item_no %lu", (unsigned long) item_no);
	return;
    }
    Menu_item & i = priv->items[item_no];
    if ((i.flags & MIF_MACTIVE) != MIF_SACTIVE)
    {
	nf_bug("Menu::set_active: flags %02Xh", i.flags);
	return;
    }
    i.active.s = active;
}


/*
 *	Menu_priv::geom - recalculate the screen coordinates etc.
 */
void Menu_priv::geom()
{
    size_t width_chars = 0;
    if (title && (flags & MF_POPUP))
	width_chars = y_max(width_chars, title_len);
    if (flags & MF_NUMS)
	width_chars = y_max(width_chars, items_len + 4);
    else
	width_chars = y_max(width_chars, items_ks_len);
    int title_height = title && (flags & MF_POPUP) ? (int) (1.5 * FONTH) : 0;

    width = 2 * BOX_BORDER + 2 * WIDE_HSPACING + width_chars * FONTW;
    height = 2 * BOX_BORDER + 2 * WIDE_VSPACING + title_height
	+ items.back().y + item_height;

    if (user_ox0 < 0)
	ox0 = (ScrMaxX - width) / 2;
    else
	ox0 = user_ox0;
    ix0 = ox0 + BOX_BORDER;
    ix1 = ix0 + 2 * WIDE_HSPACING + width_chars * FONTW - 1;
    ox1 = ix1 + BOX_BORDER;
    if (ox1 > ScrMaxX)
    {
	int overlap = ox1 - ScrMaxX;
	ox0 -= overlap;
	ix0 -= overlap;
	ix1 -= overlap;
	ox1 -= overlap;
    }

    if (user_oy0 < 0)
	oy0 = (ScrMaxY - height) / 2;
    else
	oy0 = user_oy0;
    iy0 = oy0 + BOX_BORDER;
    ty0 = iy0 + FONTH / 2;	// Title of menu
    ly0 = ty0 + title_height;	// First item of menu

    oy1 = oy0 + height - 1;
    iy1 = oy1 - BOX_BORDER;

    need_geom = false;
}


/*
 *	Menu::process_event - process an input event
 *
 *	Process event in *<is>.
 *
 *	Return one of the following :
 *	- MEN_CANCEL: user pressed [Esc] or clicked outside
 *	  the menu. The caller should delete the menu.
 *	- MEN_INVALID: we didn't understand the event so we put it
 *	  back in the input buffer.
 *	- MEN_OTHER: we understood the event and processed it.
 *	- the number of the item that was validated.
 */
int Menu::process_event(const input_status_t * is)
{
    return priv->process_event(is);
}


int Menu_priv::process_event(const input_status_t * is)
{
    size_t mouse_line;
    char status;

    if ((int) is->x < ix0 || (int) is->x > ix1 || (int) is->y < ly0)
	mouse_line = items.size();
    else
    {
	for (mouse_line = 0; mouse_line < items.size(); mouse_line++)
	    if ((int) is->y >= ly0 + items[mouse_line].y
		&& (int) is->y < ly0 + items[mouse_line].y + item_height)
		break;
    }

    status = 'i';

    // Clicking left button on an item: validate it.
    if (is->key == YE_BUTL_PRESS && mouse_line < items.size())
    {
	line = mouse_line;	// Useless ?
	status = 'v';
    }

    // Moving over the box sets current line.
    else if (is->key == YE_MOTION && mouse_line < items.size())
    {
	line = mouse_line;
	status = 'o';
    }

    /* Releasing the button while standing on an item: has a
       different effect depending on whether we're in pull-down or
       pop-up mode.

       In pull-down mode, the button was normally last pressed on
       the menu bar or on an item of this menu. So the current
       item is selected upon button release.

       In pop-up mode, if the button was pressed, it was most
       likely to exit a submenu (cf. the "thing type" menu) so we
       ignore the event. */
    else if (is->key == YE_BUTL_RELEASE
	     && mouse_line < items.size() && !(flags & MF_POPUP))
	status = 'v';

    // [Enter], [Return]: accept selection
    else if (is->key == YK_RETURN)
	status = 'v';

    // [Esc]: cancel
    else if (is->key == YK_ESC)
	status = 'c';

    // [Up]: select previous line
    else if (is->key == YK_UP)
    {
	if (line > 0)
	    line--;
	else
	    line = items.size() - 1;
	status = 'o';
    }

    // [Down]: select next line
    else if (is->key == YK_DOWN)
    {
	if (line < items.size() - 1)
	    line++;
	else
	    line = 0;
	status = 'o';
    }

    // [Home]: select first line
    else if (is->key == YK_HOME)
    {
	line = 0;
	status = 'o';
    }

    // [End]: select last line
    else if (is->key == YK_END)
    {
	line = items.size() - 1;
	status = 'o';
    }

    // [Pgup]: select line - 5
    else if (is->key == YK_PU)
    {
	if (line >= 5)
	    line -= 5;
	else
	    line = 0;
	status = 'o';
    }

    // [Pgdn]: select line + 5
    else if (is->key == YK_PD)
    {
	if (line + 5 < items.size())
	    line += 5;
	else
	    line = items.size() - 1;
	status = 'o';
    }

    // [1]-[9]: select items 0 through 8
    else if ((flags & MF_NUMS)
	     && is->key < YK_ && within(dectoi(is->key), 1, items.size()))
    {
	line = dectoi(is->key) - 1;
	status = 'o';
	send_event(YK_RETURN);
    }

    // [a]-[z]: select items 9 through 34
    else if ((flags & MF_NUMS)
	     && is->key < YK_
	     && islower(is->key) && within(b36toi(is->key), 10, items.size()))
    {
	line = b36toi(is->key) - 1;
	status = 'o';
	send_event(YK_RETURN);
    }

    // [A]-[Z]: select items 35 through 60
    else if ((flags & MF_NUMS)
	     && is->key < YK_
	     && isupper(is->key)
	     && within(b36toi(is->key) + 26, 36, items.size()))
    {
	line = b36toi(is->key) + 25;
	status = 'o';
	send_event(YK_RETURN);
    }

    // A shortcut ?
    else
    {
	/* First, check the list of tilde shortcuts
	   (only if is->key is a regular key) */
	if ((flags & MF_TILDE)
	    && !(flags & MF_NUMS) && is->key == (unsigned char) is->key)
	{
	    for (size_t n = 0; n < items.size(); n++)
		if (items[n].tilde_key != YK_
		    && items[n].tilde_key == tolower(is->key))
		{
		    line = n;
		    status = 'o';
		    send_event(YK_RETURN);
		    break;
		}
	}
	/* If no tilde shortcut matched, check the list of shortcut
	   keys. It's important to do the tilde shortcuts first so
	   that you can override a shortcut key (normally global)
	   with a tilde shortcut (normally local). */
	if (status == 'i' && (flags & MF_SHORTCUT) && !(flags & MF_NUMS))
	{
	    for (size_t n = 0; n < items.size(); n++)
		if (items[n].shortcut_key != YK_
		    && items[n].shortcut_key == is->key)
		{
		    line = n;
		    status = 'o';
		    send_event(YK_RETURN);
		    break;
		}
	}
    }

    // See last_shortcut_key()
    if (status == 'v')
	_last_shortcut_key =
	    (flags & MF_SHORTCUT) ? items[line].shortcut_key : 0;

    /* Return
       - the item# if validated,
       - MEN_CANCEL if cancelled,
       - MEN_OTHER or MEN_INVALID if neither. */
    if (status == 'v')
	return (int) line;
    else if (status == 'c')
	return MEN_CANCEL;
    else if (status == 'o')
	return MEN_OTHER;
    else if (status == 'i')
	return MEN_INVALID;
    else
    {
	// Can't happen
	fatal_error("Menu::process_event: bad status %02Xh", status);
	return 0;		// To please the compiler
    }
}


/*
 *	Menu::last_shortcut_key - shortcut key for last selected item
 *
 *	Return the code of the shortcut key for the last
 *	selected item. This function shouldn't exist : it's just
 *	there because it helps editloop.cc. When real key
 *	bindings are implemented in editloop.cc,
 *	get_shortcut_key() should disappear.
 */
inpev_t Menu::last_shortcut_key()
{
    return priv->_last_shortcut_key;
}


/*
 *	Menu::draw - display the menu
 *
 *	If necessary, redraw everything from scratch. Else, if
 *	<line> has changed, refresh the highlighted line.
 */
void Menu::draw()
{
    priv->draw();
}


void Menu_priv::draw()
{
    bool from_scratch = false;

    if (need_geom)
	geom();

    // Do we need to redraw everything from scratch ?
    if (visible && !visible_disp
	|| ox0 != ox0_disp
	|| oy0 != oy0_disp || width != width_disp || height != height_disp)
	from_scratch = true;

    // Display the static part of the menu
    if (from_scratch)
    {
	DrawScreenBox3D(ox0, oy0, ox1, oy1);
	set_colour(WINTITLE);
	if ((flags & MF_POPUP) && title != 0)
	    DrawScreenString(ix0 + WIDE_HSPACING, ty0, title);

	for (size_t l = 0; l < items.size(); l++)
	{
	    set_colour(WINFG);
	    draw_one_line(l, false);
	}
	visible_disp = true;
	ox0_disp = ox0;
	oy0_disp = oy0;
	width_disp = width;
	height_disp = height;
    }

    // Display the "highlight" bar
    if (from_scratch || line != line_disp)
    {
	if (line_disp < items.size())
	    draw_one_line(line_disp, false);
	if (line < items.size())
	    draw_one_line(line, true);
	line_disp = line;
    }
}


/*
 *	Menu::draw_one_line - display just one line of a menu
 *
 *  	<line> is the number of the option to draw (0 = first
 *  	option). <highlighted> tells whether the option should
 *  	be drawn highlighted.
 */
void Menu_priv::draw_one_line(size_t line, bool highlighted)
{
    const Menu_item & i = items[line];
    int x = ix0 + FONTW;
    int y = ly0 + i.y;
    int text_y = y + VSPACE / 2;

    // Separation ?
    if (i.flags & MIF_SEPAR)
    {
	push_colour(WINBG_DARK);
	short groove_y = y - NARROW_VSPACING - 2 * NARROW_BORDER;
	DrawScreenLine(ix0, groove_y, ix1, groove_y);
	set_colour(WINBG_LIGHT);
	DrawScreenLine(ix0, groove_y + 1, ix1, groove_y + 1);
	pop_colour();
    }

    // Greyed out ?
    bool active = true;
    switch (i.flags & MIF_MACTIVE)
    {
    case MIF_NACTIVE:
	active = true;
	break;

    case MIF_SACTIVE:
	active = i.active.s;
	break;

    case MIF_VACTIVE:
	active = *i.active.v;
	break;

    case MIF_FACTIVE:
	active = i.active.f.f(i.active.f.a);
	break;

    default:
	nf_bug("Menu::draw_one_line: active %02Xh", i.flags);
	break;
    }
    set_colour(menu_colour[!active][highlighted].bg);
    DrawScreenBox(ix0, y, ix1, y + item_height - 1);
    set_colour(menu_colour[!active][highlighted].fg);

    // Tick mark if any
    if (flags & MF_TICK)
    {
	bool have_tick = false;
	bool ticked = false;
	switch (i.flags & MIF_MTICK)
	{
	case MIF_NTICK:
	    have_tick = false;
	    break;

	case MIF_STICK:
	    have_tick = true;
	    ticked = i.tick.s;
	    break;

	case MIF_VTICK:
	    have_tick = true;
	    ticked = *i.tick.v;
	    break;

	case MIF_FTICK:
	    have_tick = true;
	    ticked = i.tick.f.f(i.tick.f.a);
	    break;

	default:
	    nf_bug("Menu::draw_one_line: tick %02Xh", i.flags);
	    break;
	}
	if (have_tick)
	{
	    if (ticked)
	    {
		unsigned hside = FONTW * 4 / 5;
		unsigned vside = FONTH * 4 / 5;
		int x0 = x + (FONTW - hside) / 2;
		int y0 = y + (FONTH - vside) / 2;
		DrawScreenLine(x0, y0 + vside / 2, x0 + hside / 2,
			       y0 + vside - 1);
		DrawScreenLine(x0 + hside / 2, y0 + vside - 1, x0 + hside - 1,
			       y0);
	    }
	    else
	    {
		unsigned margin = FONTW / 5;
		DrawScreenLine(x + margin, y + FONTH / 2,
			       x + FONTW - 1 - margin, y + FONTH / 2);
	    }
	}
	x += 2 * FONTW;
    }

    // Automatic keys if any
    if (flags & MF_NUMS)
    {
	char c = '\0';
	if (line <= 8)
	    c = '1' + line;
	else if (line >= 9 && line < 9 + 26)
	    c = 'a' + line - 9;
	else if (line >= 9 + 26 && line < 9 + 26 + 26)
	    c = 'A' + line - (9 + 26);
	if (c != '\0')
	{
	    push_colour(highlighted ? WINLABEL_HL : WINLABEL);
	    DrawScreenString(x, text_y, "[ ]");
	    pop_colour();
	    DrawScreenChar(x + FONTW, text_y, c);
	    DrawScreenChar(x + FONTW, text_y + FONTU, '_');
	}
	x += 4 * FONTW;
    }

    // Text
    int tilde_index = -1;
    {
	const char *str = (flags & MF_MENUDATA) ? (*menudata)[line] : i.str;
	char *buf = new char[strlen(str) + 1];
	char *d = buf;
	for (const char *s = str; *s != '\0'; s++)
	{
	    if (*s == '~' && tilde_index < 0)
	    {
		tilde_index = s - str;
		continue;
	    }
	    *d++ = *s;
	}
	*d = '\0';
	DrawScreenString(x, text_y, buf);
	delete[]buf;
    }

    // Underscore the tilde shortcut if any
    if (!(flags & MF_NUMS) && tilde_index >= 0)
	DrawScreenString(x + tilde_index * FONTW, text_y + FONTU, "_");

    // Shortcut key if any
    if (!(flags & MF_NUMS) && i.shortcut_key != YK_)
    {
	const char *s = key_to_string(i.shortcut_key);
	DrawScreenString(ix1 + 1 - FONTW - strlen(s) * FONTW, text_y, s);
    }
}


/*
 *	WIDGET METHODS
 */


void Menu::undraw()
{
    ;				// I can't undraw myself
}


int Menu::can_undraw()
{
    return 0;			// I can't undraw myself
}


int Menu::need_to_clear()
{
    return !priv->visible && priv->visible_disp || priv->need_geom;
}


void Menu::clear()
{
    priv->visible_disp = false;
}


int Menu::req_width()
{
    if (priv->need_geom)
	priv->geom();
    return priv->width;
}


int Menu::req_height()
{
    if (priv->need_geom)
	priv->geom();
    return priv->height;
}


int Menu::get_x0()
{
    return priv->ox0_disp;
}


int Menu::get_y0()
{
    return priv->oy0_disp;
}


int Menu::get_x1()
{
    return priv->ox0_disp + priv->width_disp - 1;
}


int Menu::get_y1()
{
    return priv->oy0_disp + priv->height_disp - 1;
}