Files
nethack/win/curses/cursdial.c
2019-06-24 23:53:08 -04:00

1559 lines
49 KiB
C

/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/
/* NetHack 3.6 cursdial.c */
/* Copyright (c) Karl Garrison, 2010. */
/* NetHack may be freely redistributed. See license for details. */
#include "curses.h"
#include "hack.h"
#include "wincurs.h"
#include "cursdial.h"
#include "func_tab.h"
#include <ctype.h>
#if defined(FILENAME_CMP) && !defined(strcasecmp)
#define strcasecmp FILENAME_CMP
#endif
#if defined(STRNCMPI) && !defined(strncasecmp)
#define strncasecmp strncmpi
#endif
/*
* Note:
*
* Prototypes need to use the widened/unwidened type macros (CHAR_P, &c)
* in order to match fields of the window_procs struct (see winprocs.h).
* But for a standard-conforming compiler, we'll end up with the widened
* types necessary to match the mixed prototype/old-style function
* definition environment as used by nethack's core. Prototype
int func(CHAR_P);
* becomes
int func(int);
* after expansion, which matches the definition
int func(arg) char arg; { ... }
* according to the rules of the C standard. But the use of new-style
* definitions
int func(char arg) { ... }
* by the curses interface turns that into a conflict. No widening takes
* place so it ought to be 'int func(char);' instead. Unfortunately that
* would be incompatible for functions assigned to window_procs.
*
* So, the code here (also cursmain.c and cursinvt.c) is mis-using the
* widening macros for variable types
int func(CHAR_P arg) { ... }
* (no doubt modelling it after the C++ code in win/Qt where the option
* to switch the applicable definitions to old-style isn't available).
* Other alternatives aren't significantly better so just live with it.
* [Redoing the windowing interface to avoid narrow arguments would be
* better since that would fix Qt's usage too.]
*/
/* Dialog windows for curses interface */
/* Private declarations */
typedef struct nhmi {
winid wid; /* NetHack window id */
int glyph; /* Menu glyphs */
anything identifier; /* Value returned if item selected */
CHAR_P accelerator; /* Character used to select item from menu */
CHAR_P group_accel; /* Group accelerator for menu item, if any */
int attr; /* Text attributes for item */
const char *str; /* Text of menu item */
BOOLEAN_P presel; /* Whether menu item should be preselected */
boolean selected; /* Whether item is currently selected */
int page_num; /* Display page number for entry */
int line_num; /* Line number on page where entry begins */
int num_lines; /* Number of lines entry uses on page */
int count; /* Count for selected item */
struct nhmi *prev_item; /* Pointer to previous entry */
struct nhmi *next_item; /* Pointer to next entry */
} nhmenu_item;
typedef struct nhm {
winid wid; /* NetHack window id */
const char *prompt; /* Menu prompt text */
nhmenu_item *entries; /* Menu entries */
int num_entries; /* Number of menu entries */
int num_pages; /* Number of display pages for entry */
int height; /* Window height of menu */
int width; /* Window width of menu */
boolean reuse_accels; /* Non-unique accelerators per page */
boolean bottom_heavy; /* display multi-page menu starting at end */
struct nhm *prev_menu; /* Pointer to previous entry */
struct nhm *next_menu; /* Pointer to next entry */
} nhmenu;
typedef enum menu_op_type {
SELECT,
DESELECT,
INVERT
} menu_op;
static nhmenu_item *curs_new_menu_item(winid, const char *);
static void curs_pad_menu(nhmenu *, boolean);
static nhmenu *get_menu(winid wid);
static char menu_get_accel(boolean first);
static void menu_determine_pages(nhmenu *menu);
static boolean menu_is_multipage(nhmenu *menu, int width, int height);
static void menu_win_size(nhmenu *menu);
static void menu_display_page(nhmenu *menu, WINDOW * win, int page_num,
char *);
static int menu_get_selections(WINDOW * win, nhmenu *menu, int how);
static void menu_select_deselect(WINDOW * win, nhmenu_item *item,
menu_op operation, int);
static int menu_operation(WINDOW * win, nhmenu *menu, menu_op operation,
int page_num);
static void menu_clear_selections(nhmenu *menu);
static int menu_max_height(void);
static nhmenu *nhmenus = NULL; /* NetHack menu array */
/* maximum number of selector entries per page; if '$' is present
it isn't counted toward maximum, so actual max per page is 53 */
#define MAX_ACCEL_PER_PAGE 52 /* 'a'..'z' + 'A'..'Z' */
/* TODO? limit per page should be ignored for perm_invent, which might
have some '#' overflow entries and isn't used to select items */
/* Get a line of text from the player, such as asking for a character name
or a wish. Note: EDIT_GETLIN not supported for popup prompting. */
void
curses_line_input_dialog(const char *prompt, char *answer, int buffer)
{
int map_height, map_width, maxwidth, remaining_buf, winx, winy, count;
WINDOW *askwin, *bwin;
char *tmpstr;
int prompt_width = (int) strlen(prompt) + buffer + 1;
int prompt_height = 1;
int height = prompt_height;
char input[BUFSZ];
/* if messages were being suppressed for the remainder of the turn,
re-activate them now that input is being requested */
curses_got_input();
if (buffer > (int) sizeof input)
buffer = (int) sizeof input;
maxwidth = term_cols - 2;
if (iflags.window_inited) {
if (!iflags.wc_popup_dialog) {
curses_message_win_getline(prompt, answer, buffer);
return;
}
curses_get_window_size(MAP_WIN, &map_height, &map_width);
if ((prompt_width + 2) > map_width)
maxwidth = map_width - 2;
}
if (prompt_width > maxwidth) {
prompt_height = curses_num_lines(prompt, maxwidth);
height = prompt_height;
prompt_width = maxwidth;
tmpstr = curses_break_str(prompt, maxwidth, prompt_height);
remaining_buf = buffer - ((int) strlen(tmpstr) - 1);
if (remaining_buf > 0) {
height += (remaining_buf / prompt_width);
if ((remaining_buf % prompt_width) > 0) {
height++;
}
}
free(tmpstr);
}
if (iflags.window_inited) {
bwin = curses_create_window(prompt_width, height, UP);
wrefresh(bwin);
getbegyx(bwin, winy, winx);
askwin = newwin(height, prompt_width, winy + 1, winx + 1);
} else {
bwin = curses_create_window(prompt_width, height, CENTER);
wrefresh(bwin);
getbegyx(bwin, winy, winx);
askwin = newwin(height, prompt_width, winy + 1, winx + 1);
}
for (count = 0; count < prompt_height; count++) {
tmpstr = curses_break_str(prompt, maxwidth, count + 1);
if (count == (prompt_height - 1)) { /* Last line */
mvwprintw(askwin, count, 0, "%s ", tmpstr);
} else {
mvwaddstr(askwin, count, 0, tmpstr);
}
free(tmpstr);
}
echo();
curs_set(1);
wgetnstr(askwin, input, buffer - 1);
curs_set(0);
strcpy(answer, input);
werase(bwin);
delwin(bwin);
curses_destroy_win(askwin);
noecho();
}
/* Get a single character response from the player, such as a y/n prompt */
int
curses_character_input_dialog(const char *prompt, const char *choices,
CHAR_P def)
{
WINDOW *askwin = NULL;
#ifdef PDCURSES
WINDOW *message_window;
#endif
int answer, count, maxwidth, map_height, map_width;
char *linestr;
char askstr[BUFSZ + QBUFSZ];
char choicestr[QBUFSZ];
int prompt_width;
int prompt_height = 1;
boolean any_choice = FALSE;
boolean accept_count = FALSE;
/* if messages were being suppressed for the remainder of the turn,
re-activate them now that input is being requested */
curses_got_input();
if (g.invent || (g.moves > 1)) {
curses_get_window_size(MAP_WIN, &map_height, &map_width);
} else {
map_height = term_rows;
map_width = term_cols;
}
#ifdef PDCURSES
message_window = curses_get_nhwin(MESSAGE_WIN);
#endif
maxwidth = map_width - 2;
if (choices != NULL) {
for (count = 0; choices[count] != '\0'; count++) {
if (choices[count] == '#') { /* Accept a count */
accept_count = TRUE;
}
}
choicestr[0] = ' ';
choicestr[1] = '[';
for (count = 0; choices[count] != '\0'; count++) {
if (choices[count] == '\033') { /* Escape */
break;
}
choicestr[count + 2] = choices[count];
}
choicestr[count + 2] = ']';
if (((def >= 'A') && (def <= 'Z')) || ((def >= 'a') && (def <= 'z'))) {
choicestr[count + 3] = ' ';
choicestr[count + 4] = '(';
choicestr[count + 5] = def;
choicestr[count + 6] = ')';
choicestr[count + 7] = '\0';
} else { /* No usable default choice */
choicestr[count + 3] = '\0';
def = '\0'; /* Mark as no default */
}
strcpy(askstr, prompt);
strcat(askstr, choicestr);
} else {
strcpy(askstr, prompt);
any_choice = TRUE;
}
prompt_width = (int) strlen(askstr);
if ((prompt_width + 2) > maxwidth) {
prompt_height = curses_num_lines(askstr, maxwidth);
prompt_width = map_width - 2;
}
if (iflags.wc_popup_dialog /*|| curses_stupid_hack*/) {
askwin = curses_create_window(prompt_width, prompt_height, UP);
for (count = 0; count < prompt_height; count++) {
linestr = curses_break_str(askstr, maxwidth, count + 1);
mvwaddstr(askwin, count + 1, 1, linestr);
free(linestr);
}
wrefresh(askwin);
} else {
/* TODO: add SUPPRESS_HISTORY flag, then after getting a response,
append it and use put_msghistory() on combined prompt+answer */
custompline(OVERRIDE_MSGTYPE, "%s", askstr);
curs_set(1);
}
/*curses_stupid_hack = 0; */
while (1) {
#ifdef PDCURSES
answer = wgetch(message_window);
#else
answer = getch();
#endif
if (answer == ERR) {
answer = def;
break;
}
answer = curses_convert_keys(answer);
if (answer == KEY_ESC) {
if (choices == NULL) {
break;
}
answer = def;
for (count = 0; choices[count] != '\0'; count++) {
if (choices[count] == 'q') { /* q is preferred over n */
answer = 'q';
} else if ((choices[count] == 'n') && answer != 'q') {
answer = 'n';
}
}
break;
} else if ((answer == '\n') || (answer == '\r') || (answer == ' ')) {
if ((choices != NULL) && (def != '\0')) {
answer = def;
}
break;
}
if (digit(answer)) {
if (accept_count) {
if (answer != '0') {
yn_number = curses_get_count(answer - '0');
touchwin(askwin);
refresh();
}
answer = '#';
break;
}
}
if (any_choice) {
break;
}
if (choices != NULL && answer != '\0' && index(choices, answer))
break;
}
if (iflags.wc_popup_dialog) {
/* Kludge to make prompt visible after window is dismissed
when inputting a number */
if (digit(answer)) {
custompline(OVERRIDE_MSGTYPE, "%s", askstr);
curs_set(1);
}
curses_destroy_win(askwin);
} else {
curses_clear_unhighlight_message_window();
curs_set(0);
}
return answer;
}
/* Return an extended command from the user */
int
curses_ext_cmd()
{
int count, letter, prompt_width, startx, starty, winx, winy;
int messageh, messagew, maxlen = BUFSZ - 1;
int ret = -1;
char cur_choice[BUFSZ];
int matches = 0;
WINDOW *extwin = NULL, *extwin2 = NULL;
if (iflags.extmenu) {
return extcmd_via_menu();
}
startx = 0;
starty = 0;
if (iflags.wc_popup_dialog) { /* Prompt in popup window */
int x0, y0, w, h; /* bounding coords of popup */
extwin2 = curses_create_window(25, 1, UP);
wrefresh(extwin2);
/* create window inside window to prevent overwriting of border */
getbegyx(extwin2, y0, x0);
getmaxyx(extwin2, h, w);
extwin = newwin(1, w - 2, y0 + 1, x0 + 1);
if (w - 4 < maxlen) maxlen = w - 4;
} else {
curses_get_window_xy(MESSAGE_WIN, &winx, &winy);
curses_get_window_size(MESSAGE_WIN, &messageh, &messagew);
if (curses_window_has_border(MESSAGE_WIN)) {
winx++;
winy++;
}
winy += messageh - 1;
extwin = newwin(1, messagew - 2, winy, winx);
if (messagew - 4 < maxlen)
maxlen = messagew - 4;
custompline(OVERRIDE_MSGTYPE, "#");
}
cur_choice[0] = '\0';
while (1) {
wmove(extwin, starty, startx);
waddstr(extwin, "# ");
wmove(extwin, starty, startx + 2);
waddstr(extwin, cur_choice);
wmove(extwin, starty, (int) strlen(cur_choice) + startx + 2);
wprintw(extwin, " ");
/* if we have an autocomplete command, AND it matches uniquely */
if (matches == 1) {
curses_toggle_color_attr(extwin, NONE, A_UNDERLINE, ON);
wmove(extwin, starty, (int) strlen(cur_choice) + startx + 2);
wprintw(extwin, "%s",
extcmdlist[ret].ef_txt + (int) strlen(cur_choice));
curses_toggle_color_attr(extwin, NONE, A_UNDERLINE, OFF);
mvwprintw(extwin, starty,
(int) strlen(extcmdlist[ret].ef_txt) + 2, " ");
}
wrefresh(extwin);
letter = getch();
prompt_width = (int) strlen(cur_choice);
matches = 0;
if (letter == '\033' || letter == ERR) {
ret = -1;
break;
}
if (letter == '\r' || letter == '\n') {
if (ret == -1) {
for (count = 0; extcmdlist[count].ef_txt; count++) {
if (!strcasecmp(cur_choice, extcmdlist[count].ef_txt)) {
ret = count;
break;
}
}
}
break;
}
if (letter == '\177') /* DEL/Rubout */
letter = '\b';
if (letter == '\b' || letter == KEY_BACKSPACE) {
if (prompt_width == 0) {
ret = -1;
break;
} else {
cur_choice[prompt_width - 1] = '\0';
letter = '*';
prompt_width--;
}
}
if (letter != '*' && prompt_width < maxlen) {
cur_choice[prompt_width] = letter;
cur_choice[prompt_width + 1] = '\0';
ret = -1;
}
for (count = 0; extcmdlist[count].ef_txt; count++) {
if (!wizard && (extcmdlist[count].flags & WIZMODECMD))
continue;
if (!(extcmdlist[count].flags & AUTOCOMPLETE))
continue;
if ((int) strlen(extcmdlist[count].ef_txt) > prompt_width) {
if (strncasecmp(cur_choice, extcmdlist[count].ef_txt,
prompt_width) == 0) {
if ((extcmdlist[count].ef_txt[prompt_width] ==
lowc(letter)) || letter == '*') {
if (matches == 0) {
ret = count;
}
matches++;
}
}
}
}
}
curses_destroy_win(extwin);
if (extwin2) curses_destroy_win(extwin2);
return ret;
}
/* Initialize a menu from given NetHack winid */
void
curses_create_nhmenu(winid wid)
{
nhmenu *new_menu = NULL;
nhmenu *menuptr = nhmenus;
nhmenu_item *menu_item_ptr = NULL;
nhmenu_item *tmp_menu_item = NULL;
new_menu = get_menu(wid);
if (new_menu != NULL) {
/* Reuse existing menu, clearing out current entries */
menu_item_ptr = new_menu->entries;
if (menu_item_ptr != NULL) {
while (menu_item_ptr->next_item != NULL) {
tmp_menu_item = menu_item_ptr->next_item;
free((genericptr_t) menu_item_ptr->str);
free((genericptr_t) menu_item_ptr);
menu_item_ptr = tmp_menu_item;
}
free((genericptr_t) menu_item_ptr->str);
free((genericptr_t) menu_item_ptr); /* Last entry */
new_menu->entries = NULL;
}
if (new_menu->prompt != NULL) { /* Reusing existing menu */
free((genericptr_t) new_menu->prompt);
new_menu->prompt = NULL;
}
new_menu->num_pages = 0;
new_menu->height = 0;
new_menu->width = 0;
new_menu->reuse_accels = FALSE;
new_menu->bottom_heavy = FALSE;
return;
}
new_menu = (nhmenu *) alloc((signed) sizeof (nhmenu));
new_menu->wid = wid;
new_menu->prompt = NULL;
new_menu->entries = NULL;
new_menu->num_pages = 0;
new_menu->height = 0;
new_menu->width = 0;
new_menu->reuse_accels = FALSE;
new_menu->bottom_heavy = FALSE;
new_menu->next_menu = NULL;
if (nhmenus == NULL) { /* no menus in memory yet */
new_menu->prev_menu = NULL;
nhmenus = new_menu;
} else {
while (menuptr->next_menu != NULL) {
menuptr = menuptr->next_menu;
}
new_menu->prev_menu = menuptr;
menuptr->next_menu = new_menu;
}
}
static nhmenu_item *
curs_new_menu_item(winid wid, const char *str)
{
char *new_str;
nhmenu_item *new_item;
new_str = curses_copy_of(str);
curses_rtrim(new_str);
new_item = (nhmenu_item *) alloc((unsigned) sizeof (nhmenu_item));
new_item->wid = wid;
new_item->glyph = NO_GLYPH;
new_item->identifier = cg.zeroany;
new_item->accelerator = '\0';
new_item->group_accel = '\0';
new_item->attr = 0;
new_item->str = new_str;
new_item->presel = FALSE;
new_item->selected = FALSE;
new_item->page_num = 0;
new_item->line_num = 0;
new_item->num_lines = 0;
new_item->count = -1;
new_item->next_item = NULL;
return new_item;
}
/* Add a menu item to the given menu window */
void
curses_add_nhmenu_item(winid wid, int glyph, const ANY_P *identifier,
CHAR_P accelerator, CHAR_P group_accel, int attr,
const char *str, BOOLEAN_P presel)
{
nhmenu_item *new_item, *current_items, *menu_item_ptr;
nhmenu *current_menu = get_menu(wid);
if (current_menu == NULL) {
impossible(
"curses_add_nhmenu_item: attempt to add item to nonexistent menu");
return;
}
if (str == NULL) {
return;
}
new_item = curs_new_menu_item(wid, str);
new_item->glyph = glyph;
new_item->identifier = *identifier;
new_item->accelerator = accelerator;
new_item->group_accel = group_accel;
new_item->attr = attr;
new_item->presel = presel;
current_items = current_menu->entries;
menu_item_ptr = current_items;
if (current_items == NULL) {
new_item->prev_item = NULL;
current_menu->entries = new_item;
} else {
while (menu_item_ptr->next_item != NULL) {
menu_item_ptr = menu_item_ptr->next_item;
}
new_item->prev_item = menu_item_ptr;
menu_item_ptr->next_item = new_item;
}
}
/* for menu->bottom_heavy -- insert enough blank lines at top of
first page to make the last page become a full one */
static void
curs_pad_menu(nhmenu *current_menu, boolean do_pad UNUSED)
{
nhmenu_item *menu_item_ptr;
int numpages = current_menu->num_pages;
/* caller has already called menu_win_size() */
menu_determine_pages(current_menu); /* sets 'menu->num_pages' */
numpages = current_menu->num_pages;
/* pad beginning of menu so that partial last page becomes full;
might be slightly less than full if any entries take multiple
lines and the padding would force those to span page boundary
and that gets prevented; so we re-count the number of pages
with every insertion instead of trying to calculate the number
of them to add */
do {
menu_item_ptr = curs_new_menu_item(current_menu->wid, "");
menu_item_ptr->next_item = current_menu->entries;
current_menu->entries->prev_item = menu_item_ptr;
current_menu->entries = menu_item_ptr;
current_menu->num_entries += 1;
menu_determine_pages(current_menu);
} while (current_menu->num_pages == numpages);
/* we inserted blank lines at beginning until final entry spilled
over to another page; take the most recent blank one back out */
current_menu->num_entries -= 1;
current_menu->entries = menu_item_ptr->next_item;
current_menu->entries->prev_item = (nhmenu_item *) 0;
free((genericptr_t) menu_item_ptr->str);
free((genericptr_t) menu_item_ptr);
/* reset page count; shouldn't need to re-count */
current_menu->num_pages = numpages;
return;
}
/* mark ^P message recall menu, for msg_window:full (FIFO), where we'll
start viewing on the last page so be able to see most recent immediately */
void
curs_menu_set_bottom_heavy(winid wid)
{
nhmenu *menu = get_menu(wid);
/*
* Called after end_menu + finalize_nhmenu,
* before select_menu + display_nhmenu.
*/
menu_win_size(menu); /* (normally not done until display_nhmenu) */
if (menu_is_multipage(menu, menu->width, menu->height)) {
menu->bottom_heavy = TRUE;
/* insert enough blank lines at top of first page to make the
last page become a full one */
curs_pad_menu(menu, TRUE);
}
return;
}
/* No more entries are to be added to menu, so details of the menu can be
finalized in memory */
void
curses_finalize_nhmenu(winid wid, const char *prompt)
{
int count = 0;
nhmenu_item *menu_item_ptr;
nhmenu *current_menu = get_menu(wid);
if (current_menu == NULL) {
impossible(
"curses_finalize_nhmenu: attempt to finalize nonexistent menu");
return;
}
for (menu_item_ptr = current_menu->entries;
menu_item_ptr != NULL; menu_item_ptr = menu_item_ptr->next_item)
count++;
current_menu->num_entries = count;
current_menu->prompt = curses_copy_of(prompt);
}
/* Display a nethack menu, and return a selection, if applicable */
int
curses_display_nhmenu(winid wid, int how, MENU_ITEM_P ** _selected)
{
nhmenu *current_menu = get_menu(wid);
nhmenu_item *menu_item_ptr;
int num_chosen, count;
WINDOW *win;
MENU_ITEM_P *selected = NULL;
*_selected = NULL;
if (current_menu == NULL) {
impossible(
"curses_display_nhmenu: attempt to display nonexistent menu");
return '\033';
}
menu_item_ptr = current_menu->entries;
if (menu_item_ptr == NULL) {
impossible("curses_display_nhmenu: attempt to display empty menu");
return '\033';
}
/* Reset items to unselected to clear out selections from previous
invocations of this menu, and preselect appropriate items */
while (menu_item_ptr != NULL) {
menu_item_ptr->selected = menu_item_ptr->presel;
menu_item_ptr = menu_item_ptr->next_item;
}
menu_win_size(current_menu);
menu_determine_pages(current_menu);
/* Display pre and post-game menus centered */
if (((g.moves <= 1) && !g.invent) || g.program_state.gameover) {
win = curses_create_window(current_menu->width,
current_menu->height, CENTER);
} else { /* Display during-game menus on the right out of the way */
win = curses_create_window(current_menu->width,
current_menu->height, RIGHT);
}
num_chosen = menu_get_selections(win, current_menu, how);
curses_destroy_win(win);
if (num_chosen > 0) {
selected = (MENU_ITEM_P *) alloc((unsigned)
(num_chosen * sizeof (MENU_ITEM_P)));
count = 0;
menu_item_ptr = current_menu->entries;
while (menu_item_ptr != NULL) {
if (menu_item_ptr->selected) {
if (count == num_chosen) {
impossible("curses_display_nhmenu: Selected items "
"exceeds expected number");
break;
}
selected[count].item = menu_item_ptr->identifier;
selected[count].count = menu_item_ptr->count;
count++;
}
menu_item_ptr = menu_item_ptr->next_item;
}
if (count != num_chosen) {
impossible(
"curses_display_nhmenu: Selected items less than expected number");
}
}
*_selected = selected;
return num_chosen;
}
boolean
curses_menu_exists(winid wid)
{
if (get_menu(wid) != NULL) {
return TRUE;
} else {
return FALSE;
}
}
/* Delete the menu associated with the given NetHack winid from memory */
void
curses_del_menu(winid wid, boolean del_wid_too)
{
nhmenu_item *tmp_menu_item;
nhmenu_item *menu_item_ptr;
nhmenu *tmpmenu;
nhmenu *current_menu = get_menu(wid);
if (current_menu == NULL) {
return;
}
menu_item_ptr = current_menu->entries;
/* First free entries associated with this menu from memory */
if (menu_item_ptr != NULL) {
while (menu_item_ptr->next_item != NULL) {
tmp_menu_item = menu_item_ptr->next_item;
free((genericptr_t) menu_item_ptr->str);
free(menu_item_ptr);
menu_item_ptr = tmp_menu_item;
}
free((genericptr_t) menu_item_ptr->str);
free(menu_item_ptr); /* Last entry */
current_menu->entries = NULL;
}
/* Now unlink the menu from the list and free it as well */
if ((tmpmenu = current_menu->prev_menu) != NULL) {
tmpmenu->next_menu = current_menu->next_menu;
} else {
nhmenus = current_menu->next_menu; /* New head node or NULL */
}
if ((tmpmenu = current_menu->next_menu) != NULL) {
tmpmenu->prev_menu = current_menu->prev_menu;
}
if (current_menu->prompt)
free((genericptr_t) current_menu->prompt);
free((genericptr_t) current_menu);
if (del_wid_too)
curses_del_wid(wid);
}
/* return a pointer to the menu associated with the given NetHack winid */
static nhmenu *
get_menu(winid wid)
{
nhmenu *menuptr = nhmenus;
while (menuptr != NULL) {
if (menuptr->wid == wid) {
return menuptr;
}
menuptr = menuptr->next_menu;
}
return NULL; /* Not found */
}
static char
menu_get_accel(boolean first)
{
static char next_letter;
char ret;
if (first) {
next_letter = 'a';
}
ret = next_letter;
if ((next_letter < 'z' && next_letter >= 'a')
|| (next_letter < 'Z' && next_letter >= 'A')) {
next_letter++;
} else if (next_letter == 'z') {
next_letter = 'A';
} else if (next_letter == 'Z') {
next_letter = 'a'; /* wrap to beginning */
}
return ret;
}
/* Determine if menu will require multiple pages to display */
static boolean
menu_is_multipage(nhmenu *menu, int width, int height)
{
int num_lines;
int curline = 0, accel_per_page = 0;
nhmenu_item *menu_item_ptr = menu->entries;
if (*menu->prompt) {
curline += curses_num_lines(menu->prompt, width) + 1;
}
while (menu_item_ptr != NULL) {
menu_item_ptr->line_num = curline;
if (menu_item_ptr->identifier.a_void == NULL) {
num_lines = curses_num_lines(menu_item_ptr->str, width);
} else {
if (menu_item_ptr->accelerator != GOLD_SYM) {
if (++accel_per_page > MAX_ACCEL_PER_PAGE)
break;
}
/* Add space for accelerator */
num_lines = curses_num_lines(menu_item_ptr->str, width - 4);
}
menu_item_ptr->num_lines = num_lines;
curline += num_lines;
menu_item_ptr = menu_item_ptr->next_item;
if (curline > height
|| (curline > height - 2 && height == menu_max_height())) {
break;
}
}
return (menu_item_ptr != NULL) ? TRUE : FALSE;
}
/* Determine which entries go on which page, and total number of pages */
static void
menu_determine_pages(nhmenu *menu)
{
int tmpline, num_lines, accel_per_page;
int curline = 0;
int page_num = 1;
nhmenu_item *menu_item_ptr = menu->entries;
int width = menu->width;
int height = menu->height;
int page_end = height;
if (*menu->prompt) {
curline += curses_num_lines(menu->prompt, width) + 1;
}
tmpline = curline;
if (menu_is_multipage(menu, width, height)) {
page_end -= 2; /* Room to display current page number */
}
accel_per_page = 0;
/* Determine what entries belong on which page */
for (menu_item_ptr = menu->entries; menu_item_ptr != NULL;
menu_item_ptr = menu_item_ptr->next_item) {
menu_item_ptr->page_num = page_num;
menu_item_ptr->line_num = curline;
if (menu_item_ptr->identifier.a_void == NULL) {
num_lines = curses_num_lines(menu_item_ptr->str, width);
} else {
if (menu_item_ptr->accelerator != GOLD_SYM)
++accel_per_page;
/* Add space for accelerator */
num_lines = curses_num_lines(menu_item_ptr->str, width - 4);
}
menu_item_ptr->num_lines = num_lines;
curline += num_lines;
if (curline > page_end || accel_per_page > MAX_ACCEL_PER_PAGE) {
++page_num;
accel_per_page = 0; /* reset */
curline = tmpline;
/* Move ptr back so entry will be reprocessed on new page */
menu_item_ptr = menu_item_ptr->prev_item;
}
}
menu->num_pages = page_num;
}
/* Determine dimensions of menu window based on term size and entries */
static void
menu_win_size(nhmenu *menu)
{
int width, height, maxwidth, maxheight, curentrywidth, lastline;
int maxentrywidth = (int) strlen(menu->prompt);
int maxheaderwidth = 0;
nhmenu_item *menu_item_ptr;
maxwidth = 38; /* Reasonable minimum usable width */
if ((term_cols / 2) > maxwidth) {
maxwidth = (term_cols / 2); /* Half the screen */
}
maxheight = menu_max_height();
/* First, determine the width of the longest menu entry */
for (menu_item_ptr = menu->entries; menu_item_ptr != NULL;
menu_item_ptr = menu_item_ptr->next_item) {
curentrywidth = (int) strlen(menu_item_ptr->str);
if (menu_item_ptr->identifier.a_void == NULL) {
if (curentrywidth > maxheaderwidth) {
maxheaderwidth = curentrywidth;
}
} else {
/* Add space for accelerator */
curentrywidth += 4;
#if 0 /* FIXME: menu glyphs */
if (menu_item_ptr->glyph != NO_GLYPH && iflags.use_menu_glyphs)
curentrywidth += 2;
#endif
}
if (curentrywidth > maxentrywidth) {
maxentrywidth = curentrywidth;
}
}
/* If widest entry is smaller than maxwidth, reduce maxwidth accordingly */
if (maxentrywidth < maxwidth) {
maxwidth = maxentrywidth;
}
/* Try not to wrap headers/normal text lines if possible. We can
go wider than half the screen for this purpose if need be */
if (maxheaderwidth > maxwidth) {
if (maxheaderwidth < (term_cols - 2))
maxwidth = maxheaderwidth;
else
maxwidth = term_cols - 2;
}
width = maxwidth;
/* Possibly reduce height if only 1 page */
if (!menu_is_multipage(menu, maxwidth, maxheight)) {
menu_item_ptr = menu->entries;
while (menu_item_ptr->next_item != NULL) {
menu_item_ptr = menu_item_ptr->next_item;
}
lastline = (menu_item_ptr->line_num) + menu_item_ptr->num_lines;
if (lastline < maxheight) {
maxheight = lastline;
}
} else { /* If multipage, make sure we have enough width for page footer */
if (width < 20) {
width = 20;
}
}
height = maxheight;
menu->width = width;
menu->height = height;
}
/* Displays menu selections in the given window */
static void
menu_display_page(nhmenu *menu, WINDOW * win, int page_num, char *selectors)
{
nhmenu_item *menu_item_ptr;
int count, curletter, entry_cols, start_col, num_lines;
char *tmpstr;
boolean first_accel = TRUE;
int color = NO_COLOR, attr = A_NORMAL;
boolean menu_color = FALSE;
/* letters assigned to entries on current page */
if (selectors)
(void) memset((genericptr_t) selectors, 0, 256);
/* Cycle through entries until we are on the correct page */
menu_item_ptr = menu->entries;
while (menu_item_ptr != NULL) {
if (menu_item_ptr->page_num == page_num) {
break;
}
menu_item_ptr = menu_item_ptr->next_item;
}
if (menu_item_ptr == NULL) { /* Page not found */
impossible("menu_display_page: attempt to display nonexistent page");
return;
}
werase(win);
if (menu->prompt && *menu->prompt) {
num_lines = curses_num_lines(menu->prompt, menu->width);
for (count = 0; count < num_lines; count++) {
tmpstr = curses_break_str(menu->prompt, menu->width, count + 1);
mvwprintw(win, count + 1, 1, "%s", tmpstr);
free(tmpstr);
}
}
/* Display items for current page */
while (menu_item_ptr != NULL) {
if (menu_item_ptr->page_num != page_num) {
break;
}
if (menu_item_ptr->identifier.a_void != NULL) {
if (menu_item_ptr->accelerator != 0) {
curletter = menu_item_ptr->accelerator;
} else {
if (first_accel) {
curletter = menu_get_accel(TRUE);
first_accel = FALSE;
if (!menu->reuse_accels && (menu->num_pages > 1)) {
menu->reuse_accels = TRUE;
}
} else {
curletter = menu_get_accel(FALSE);
}
menu_item_ptr->accelerator = curletter;
}
/* we have a selector letter; tell caller about it */
if (selectors)
selectors[(unsigned) (curletter & 0xFF)]++;
if (menu_item_ptr->selected) {
curses_toggle_color_attr(win, HIGHLIGHT_COLOR, A_REVERSE, ON);
mvwaddch(win, menu_item_ptr->line_num + 1, 1, '<');
mvwaddch(win, menu_item_ptr->line_num + 1, 2, curletter);
mvwaddch(win, menu_item_ptr->line_num + 1, 3, '>');
curses_toggle_color_attr(win, HIGHLIGHT_COLOR, A_REVERSE, OFF);
} else {
curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, ON);
mvwaddch(win, menu_item_ptr->line_num + 1, 2, curletter);
curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, OFF);
mvwprintw(win, menu_item_ptr->line_num + 1, 3, ") ");
}
}
entry_cols = menu->width;
start_col = 1;
if (menu_item_ptr->identifier.a_void != NULL) {
entry_cols -= 4;
start_col += 4;
}
#if 0
/* FIXME: menuglyphs not implemented yet */
if (menu_item_ptr->glyph != NO_GLYPH && iflags.use_menu_glyphs) {
unsigned special; /*notused */
mapglyph(menu_item_ptr->glyph, &curletter, &color, &special, 0, 0);
curses_toggle_color_attr(win, color, NONE, ON);
mvwaddch(win, menu_item_ptr->line_num + 1, start_col, curletter);
curses_toggle_color_attr(win, color, NONE, OFF);
mvwaddch(win, menu_item_ptr->line_num + 1, start_col + 1, ' ');
entry_cols -= 2;
start_col += 2;
}
#endif
if (iflags.use_menu_color
&& (menu_color = get_menu_coloring(menu_item_ptr->str,
&color, &attr)) != 0) {
if (color != NO_COLOR) {
curses_toggle_color_attr(win, color, NONE, ON);
}
attr = curses_convert_attr(attr);
if (attr != A_NORMAL) {
menu_item_ptr->attr = menu_item_ptr->attr | attr;
}
}
curses_toggle_color_attr(win, NONE, menu_item_ptr->attr, ON);
num_lines = curses_num_lines(menu_item_ptr->str, entry_cols);
for (count = 0; count < num_lines; count++) {
if (menu_item_ptr->str && *menu_item_ptr->str) {
tmpstr = curses_break_str(menu_item_ptr->str,
entry_cols, count + 1);
mvwprintw(win, menu_item_ptr->line_num + count + 1, start_col,
"%s", tmpstr);
free(tmpstr);
}
}
if (menu_color && (color != NO_COLOR)) {
curses_toggle_color_attr(win, color, NONE, OFF);
}
curses_toggle_color_attr(win, NONE, menu_item_ptr->attr, OFF);
menu_item_ptr = menu_item_ptr->next_item;
}
if (menu->num_pages > 1) {
int footer_x, footwidth, shoesize = menu->num_pages;
footwidth = (int) (sizeof "<- (Page X of Y) ->" - sizeof "");
while (shoesize >= 10) { /* possible for pickup from big piles... */
/* room for wider feet; extra digit for both X and Y */
footwidth += 2;
shoesize /= 10;
}
footer_x = !menu->bottom_heavy ? (menu->width - footwidth) : 2;
if (page_num != 1) {
curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, ON);
mvwaddstr(win, menu->height, footer_x, "<=");
curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, OFF);
}
mvwprintw(win, menu->height, footer_x + 2, " (Page %d of %d) ",
page_num, menu->num_pages);
if (page_num != menu->num_pages) {
curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, ON);
mvwaddstr(win, menu->height, footer_x + footwidth - 2, "=>");
curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, OFF);
}
}
curses_toggle_color_attr(win, DIALOG_BORDER_COLOR, NONE, ON);
box(win, 0, 0);
curses_toggle_color_attr(win, DIALOG_BORDER_COLOR, NONE, OFF);
wrefresh(win);
}
static int
menu_get_selections(WINDOW * win, nhmenu *menu, int how)
{
int curletter;
int count = -1;
int count_letter = '\0';
int curpage = !menu->bottom_heavy ? 1 : menu->num_pages;
int num_selected = 0;
boolean dismiss = FALSE;
char search_key[BUFSZ], selectors[256];
nhmenu_item *menu_item_ptr = menu->entries;
menu_display_page(menu, win, curpage, selectors);
while (!dismiss) {
curletter = getch();
if (curletter == ERR) {
num_selected = -1;
dismiss = TRUE;
}
if (curletter == '\033') {
curletter = curses_convert_keys(curletter);
}
switch (how) {
case PICK_NONE:
if (menu->num_pages == 1) {
if (curletter == KEY_ESC) {
num_selected = -1;
} else {
num_selected = 0;
}
dismiss = TRUE;
break;
}
break;
case PICK_ANY:
if (curletter <= 0 || curletter >= 256 || !selectors[curletter]) {
switch (curletter) {
case MENU_SELECT_PAGE:
(void) menu_operation(win, menu, SELECT, curpage);
break;
case MENU_SELECT_ALL:
curpage = menu_operation(win, menu, SELECT, 0);
break;
case MENU_UNSELECT_PAGE:
(void) menu_operation(win, menu, DESELECT, curpage);
break;
case MENU_UNSELECT_ALL:
curpage = menu_operation(win, menu, DESELECT, 0);
break;
case MENU_INVERT_PAGE:
(void) menu_operation(win, menu, INVERT, curpage);
break;
case MENU_INVERT_ALL:
curpage = menu_operation(win, menu, INVERT, 0);
break;
}
}
/*FALLTHRU*/
default:
if (isdigit(curletter)) {
count = curses_get_count(curletter - '0');
touchwin(win);
refresh();
curletter = getch();
if (count > 0) {
count_letter = curletter;
}
}
}
if (curletter <= 0 || curletter >= 256 || !selectors[curletter]) {
switch (curletter) {
case KEY_ESC:
num_selected = -1;
dismiss = TRUE;
break;
case '\n':
case '\r':
dismiss = TRUE;
break;
case KEY_RIGHT:
case KEY_NPAGE:
case MENU_NEXT_PAGE:
case ' ':
if (curpage < menu->num_pages) {
curpage++;
menu_display_page(menu, win, curpage, selectors);
} else if (curletter == ' ') {
dismiss = TRUE;
break;
}
break;
case KEY_LEFT:
case KEY_PPAGE:
case MENU_PREVIOUS_PAGE:
if (curpage > 1) {
curpage--;
menu_display_page(menu, win, curpage, selectors);
}
break;
case KEY_END:
case MENU_LAST_PAGE:
if (curpage != menu->num_pages) {
curpage = menu->num_pages;
menu_display_page(menu, win, curpage, selectors);
}
break;
case KEY_HOME:
case MENU_FIRST_PAGE:
if (curpage != 1) {
curpage = 1;
menu_display_page(menu, win, curpage, selectors);
}
break;
case MENU_SEARCH:
search_key[0] = '\0';
curses_line_input_dialog("Search for:", search_key, BUFSZ);
refresh();
touchwin(win);
wrefresh(win);
if (!*search_key)
break;
menu_item_ptr = menu->entries;
while (menu_item_ptr != NULL) {
if (menu_item_ptr->identifier.a_void != NULL
&& strstri(menu_item_ptr->str, search_key)) {
if (how == PICK_ONE) {
menu_clear_selections(menu);
menu_select_deselect(win, menu_item_ptr,
SELECT, curpage);
num_selected = 1;
dismiss = TRUE;
break;
} else {
menu_select_deselect(win, menu_item_ptr,
INVERT, curpage);
}
}
menu_item_ptr = menu_item_ptr->next_item;
}
menu_item_ptr = menu->entries;
break;
default:
if (how == PICK_NONE) {
num_selected = 0;
dismiss = TRUE;
break;
}
}
}
menu_item_ptr = menu->entries;
while (menu_item_ptr != NULL) {
if (menu_item_ptr->identifier.a_void != NULL) {
if ((curletter == menu_item_ptr->accelerator
&& (curpage == menu_item_ptr->page_num
|| !menu->reuse_accels))
|| (menu_item_ptr->group_accel
&& curletter == menu_item_ptr->group_accel)) {
if (curpage != menu_item_ptr->page_num) {
curpage = menu_item_ptr->page_num;
menu_display_page(menu, win, curpage, selectors);
}
if (how == PICK_ONE) {
menu_clear_selections(menu);
menu_select_deselect(win, menu_item_ptr,
SELECT, curpage);
if (count)
menu_item_ptr->count = count;
num_selected = 1;
dismiss = TRUE;
break;
} else if (how == PICK_ANY && curletter == count_letter) {
menu_select_deselect(win, menu_item_ptr,
SELECT, curpage);
menu_item_ptr->count = count;
count = 0;
count_letter = '\0';
} else {
menu_select_deselect(win, menu_item_ptr,
INVERT, curpage);
}
}
}
menu_item_ptr = menu_item_ptr->next_item;
}
}
if (how == PICK_ANY && num_selected != -1) {
num_selected = 0;
menu_item_ptr = menu->entries;
while (menu_item_ptr != NULL) {
if (menu_item_ptr->identifier.a_void != NULL) {
if (menu_item_ptr->selected) {
num_selected++;
}
}
menu_item_ptr = menu_item_ptr->next_item;
}
}
return num_selected;
}
/* Select, deselect, or toggle selected for the given menu entry.
For search operations, the toggled entry might be on a different
page than the one currently shown. */
static void
menu_select_deselect(WINDOW *win, nhmenu_item *item,
menu_op operation, int current_page)
{
int curletter = item->accelerator;
boolean visible = (item->page_num == current_page);
if (operation == DESELECT || (item->selected && operation == INVERT)) {
item->selected = FALSE;
if (visible) {
mvwaddch(win, item->line_num + 1, 1, ' ');
curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, ON);
mvwaddch(win, item->line_num + 1, 2, curletter);
curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, OFF);
mvwaddch(win, item->line_num + 1, 3, ')');
}
} else {
item->selected = TRUE;
if (visible) {
curses_toggle_color_attr(win, HIGHLIGHT_COLOR, A_REVERSE, ON);
mvwaddch(win, item->line_num + 1, 1, '<');
mvwaddch(win, item->line_num + 1, 2, curletter);
mvwaddch(win, item->line_num + 1, 3, '>');
curses_toggle_color_attr(win, HIGHLIGHT_COLOR, A_REVERSE, OFF);
}
}
if (visible)
wrefresh(win);
}
/* Perform the selected operation (select, unselect, invert selection)
on the given menu page. If menu_page is 0, then perform opetation on
all pages in menu. Returns last page displayed. */
static int
menu_operation(WINDOW * win, nhmenu *menu, menu_op
operation, int page_num)
{
int first_page, last_page, current_page;
nhmenu_item *menu_item_ptr = menu->entries;
if (page_num == 0) { /* Operation to occur on all pages */
first_page = 1;
last_page = menu->num_pages;
} else {
first_page = page_num;
last_page = page_num;
}
/* Cycle through entries until we are on the correct page */
while (menu_item_ptr != NULL) {
if (menu_item_ptr->page_num == first_page) {
break;
}
menu_item_ptr = menu_item_ptr->next_item;
}
current_page = first_page;
if (page_num == 0) {
menu_display_page(menu, win, current_page, (char *) 0);
}
if (menu_item_ptr == NULL) { /* Page not found */
impossible("menu_display_page: attempt to display nonexistent page");
return 0;
}
while (menu_item_ptr != NULL) {
if (menu_item_ptr->page_num != current_page) {
if (menu_item_ptr->page_num > last_page) {
break;
}
current_page = menu_item_ptr->page_num;
menu_display_page(menu, win, current_page, (char *) 0);
}
if (menu_item_ptr->identifier.a_void != NULL) {
menu_select_deselect(win, menu_item_ptr, operation, current_page);
}
menu_item_ptr = menu_item_ptr->next_item;
}
return current_page;
}
/* Set all menu items to unselected in menu */
static void
menu_clear_selections(nhmenu *menu)
{
nhmenu_item *menu_item_ptr = menu->entries;
while (menu_item_ptr != NULL) {
menu_item_ptr->selected = FALSE;
menu_item_ptr = menu_item_ptr->next_item;
}
}
/* Get the maximum height for a menu */
static int
menu_max_height(void)
{
return term_rows - 2;
}