1 /* 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" -*-*/
2 /* NetHack 3.6 cursdial.c */
3 /* Copyright (c) Karl Garrison, 2010. */
4 /* NetHack may be freely redistributed. See license for details. */
13 #if defined(FILENAME_CMP) && !defined(strcasecmp)
14 #define strcasecmp FILENAME_CMP
16 #if defined(STRNCMPI) && !defined(strncasecmp)
17 #define strncasecmp strncmpi
20 /* defined in sys/<foo>/<foo>tty.c or cursmain.c as last resort;
21 set up by curses_init_nhwindows() */
22 extern char erase_char, kill_char;
27 * Prototypes need to use the widened/unwidened type macros (CHAR_P, &c)
28 * in order to match fields of the window_procs struct (see winprocs.h).
29 * But for a standard-conforming compiler, we'll end up with the widened
30 * types necessary to match the mixed prototype/old-style function
31 * definition environment as used by nethack's core. Prototype
35 * after expansion, which matches the definition
36 int func(arg) char arg; { ... }
37 * according to the rules of the C standard. But the use of new-style
39 int func(char arg) { ... }
40 * by the curses interface turns that into a conflict. No widening takes
41 * place so it ought to be 'int func(char);' instead. Unfortunately that
42 * would be incompatible for functions assigned to window_procs.
44 * So, the code here (also cursmain.c and cursinvt.c) is mis-using the
45 * widening macros for variable types
46 int func(CHAR_P arg) { ... }
47 * (no doubt modelling it after the C++ code in win/Qt where the option
48 * to switch the applicable definitions to old-style isn't available).
49 * Other alternatives aren't significantly better so just live with it.
50 * [Redoing the windowing interface to avoid narrow arguments would be
51 * better since that would fix Qt's usage too.]
54 /* Dialog windows for curses interface */
57 /* Private declarations */
60 winid wid; /* NetHack window id */
61 int glyph; /* Menu glyphs */
62 anything identifier; /* Value returned if item selected */
63 CHAR_P accelerator; /* Character used to select item from menu */
64 CHAR_P group_accel; /* Group accelerator for menu item, if any */
65 int attr; /* Text attributes for item */
66 const char *str; /* Text of menu item */
67 BOOLEAN_P presel; /* Whether menu item should be preselected */
68 boolean selected; /* Whether item is currently selected */
69 int page_num; /* Display page number for entry */
70 int line_num; /* Line number on page where entry begins */
71 int num_lines; /* Number of lines entry uses on page */
72 int count; /* Count for selected item */
73 struct nhmi *prev_item; /* Pointer to previous entry */
74 struct nhmi *next_item; /* Pointer to next entry */
78 winid wid; /* NetHack window id */
79 const char *prompt; /* Menu prompt text */
80 nhmenu_item *entries; /* Menu entries */
81 int num_entries; /* Number of menu entries */
82 int num_pages; /* Number of display pages for entry */
83 int height; /* Window height of menu */
84 int width; /* Window width of menu */
85 boolean reuse_accels; /* Non-unique accelerators per page */
86 boolean bottom_heavy; /* display multi-page menu starting at end */
87 struct nhm *prev_menu; /* Pointer to previous entry */
88 struct nhm *next_menu; /* Pointer to next entry */
91 typedef enum menu_op_type {
97 static nhmenu_item *curs_new_menu_item(winid, const char *);
98 static void curs_pad_menu(nhmenu *, boolean);
99 static nhmenu *get_menu(winid wid);
100 static char menu_get_accel(boolean first);
101 static void menu_determine_pages(nhmenu *menu);
102 static boolean menu_is_multipage(nhmenu *menu, int width, int height);
103 static void menu_win_size(nhmenu *menu);
104 static void menu_display_page(nhmenu *menu, WINDOW * win, int page_num,
106 static int menu_get_selections(WINDOW * win, nhmenu *menu, int how);
107 static void menu_select_deselect(WINDOW * win, nhmenu_item *item,
108 menu_op operation, int);
109 static int menu_operation(WINDOW * win, nhmenu *menu, menu_op operation,
111 static void menu_clear_selections(nhmenu *menu);
112 static int menu_max_height(void);
114 static nhmenu *nhmenus = NULL; /* NetHack menu array */
116 /* maximum number of selector entries per page; if '$' is present
117 it isn't counted toward maximum, so actual max per page is 53 */
118 #define MAX_ACCEL_PER_PAGE 52 /* 'a'..'z' + 'A'..'Z' */
119 /* TODO? limit per page should be ignored for perm_invent, which might
120 have some '#' overflow entries and isn't used to select items */
123 /* Get a line of text from the player, such as asking for a character name
124 or a wish. Note: EDIT_GETLIN not supported for popup prompting. */
127 curses_line_input_dialog(const char *prompt, char *answer, int buffer)
129 int map_height, map_width, maxwidth, remaining_buf, winx, winy, count;
130 WINDOW *askwin, *bwin;
132 int prompt_width, prompt_height = 1, height = prompt_height;
135 /* if messages were being suppressed for the remainder of the turn,
136 re-activate them now that input is being requested */
139 if (buffer > (int) sizeof input)
140 buffer = (int) sizeof input;
141 /* +1: space between prompt and answer; buffer already accounts for \0 */
142 prompt_width = (int) strlen(prompt) + 1 + buffer;
143 maxwidth = term_cols - 2;
145 if (iflags.window_inited) {
146 if (!iflags.wc_popup_dialog) {
147 curses_message_win_getline(prompt, answer, buffer);
150 curses_get_window_size(MAP_WIN, &map_height, &map_width);
151 if ((prompt_width + 2) > map_width)
152 maxwidth = map_width - 2;
155 if (prompt_width > maxwidth) {
156 prompt_height = curses_num_lines(prompt, maxwidth);
157 height = prompt_height;
158 prompt_width = maxwidth;
159 tmpstr = curses_break_str(prompt, maxwidth, prompt_height);
160 remaining_buf = buffer - ((int) strlen(tmpstr) - 1);
161 if (remaining_buf > 0) {
162 height += (remaining_buf / prompt_width);
163 if ((remaining_buf % prompt_width) > 0) {
170 bwin = curses_create_window(prompt_width, height,
171 iflags.window_inited ? UP : CENTER);
173 getbegyx(bwin, winy, winx);
174 askwin = newwin(height, prompt_width, winy + 1, winx + 1);
176 for (count = 0; count < prompt_height; count++) {
177 tmpstr = curses_break_str(prompt, maxwidth, count + 1);
178 mvwaddstr(askwin, count, 0, tmpstr);
179 if (count == prompt_height - 1) { /* Last line */
180 if ((int) strlen(tmpstr) < maxwidth)
183 wmove(askwin, count + 1, 0);
190 wgetnstr(askwin, input, buffer - 1);
192 Strcpy(answer, input);
195 curses_destroy_win(askwin);
200 /* Get a single character response from the player, such as a y/n prompt */
203 curses_character_input_dialog(const char *prompt, const char *choices,
206 WINDOW *askwin = NULL;
208 WINDOW *message_window;
210 int answer, count, maxwidth, map_height, map_width;
212 char askstr[BUFSZ + QBUFSZ];
213 char choicestr[QBUFSZ];
215 int prompt_height = 1;
216 boolean any_choice = FALSE;
217 boolean accept_count = FALSE;
219 /* if messages were being suppressed for the remainder of the turn,
220 re-activate them now that input is being requested */
223 if (invent || (moves > 1)) {
224 curses_get_window_size(MAP_WIN, &map_height, &map_width);
226 map_height = term_rows;
227 map_width = term_cols;
231 message_window = curses_get_nhwin(MESSAGE_WIN);
233 maxwidth = map_width - 2;
235 if (choices != NULL) {
236 for (count = 0; choices[count] != '\0'; count++) {
237 if (choices[count] == '#') { /* Accept a count */
243 for (count = 0; choices[count] != '\0'; count++) {
244 if (choices[count] == '\033') { /* Escape */
247 choicestr[count + 2] = choices[count];
249 choicestr[count + 2] = ']';
250 if (((def >= 'A') && (def <= 'Z')) || ((def >= 'a') && (def <= 'z'))) {
251 choicestr[count + 3] = ' ';
252 choicestr[count + 4] = '(';
253 choicestr[count + 5] = def;
254 choicestr[count + 6] = ')';
255 choicestr[count + 7] = '\0';
256 } else { /* No usable default choice */
258 choicestr[count + 3] = '\0';
259 def = '\0'; /* Mark as no default */
261 strcpy(askstr, prompt);
262 strcat(askstr, choicestr);
264 strcpy(askstr, prompt);
268 /* +1: room for a trailing space where the cursor will rest */
269 prompt_width = (int) strlen(askstr) + 1;
271 if ((prompt_width + 2) > maxwidth) {
272 prompt_height = curses_num_lines(askstr, maxwidth);
273 prompt_width = map_width - 2;
276 if (iflags.wc_popup_dialog /*|| curses_stupid_hack*/) {
277 askwin = curses_create_window(prompt_width, prompt_height, UP);
278 for (count = 0; count < prompt_height; count++) {
279 linestr = curses_break_str(askstr, maxwidth, count + 1);
280 mvwaddstr(askwin, count + 1, 1, linestr);
286 /* TODO: add SUPPRESS_HISTORY flag, then after getting a response,
287 append it and use put_msghistory() on combined prompt+answer */
288 custompline(OVERRIDE_MSGTYPE, "%s", askstr);
291 /*curses_stupid_hack = 0; */
296 answer = wgetch(message_window);
305 answer = curses_convert_keys(answer);
307 if (answer == KEY_ESC) {
308 if (choices == NULL) {
312 for (count = 0; choices[count] != '\0'; count++) {
313 if (choices[count] == 'q') { /* q is preferred over n */
315 } else if ((choices[count] == 'n') && answer != 'q') {
320 } else if ((answer == '\n') || (answer == '\r') || (answer == ' ')) {
321 if ((choices != NULL) && (def != '\0')) {
330 yn_number = curses_get_count(answer - '0');
344 if (choices != NULL && answer != '\0' && index(choices, answer))
349 if (iflags.wc_popup_dialog) {
350 /* Kludge to make prompt visible after window is dismissed
351 when inputting a number */
353 custompline(OVERRIDE_MSGTYPE, "%s", askstr);
357 curses_destroy_win(askwin);
359 curses_clear_unhighlight_message_window();
366 /* Return an extended command from the user */
371 int count, letter, prompt_width, startx, starty, winx, winy;
372 int messageh, messagew, maxlen = BUFSZ - 1;
374 char cur_choice[BUFSZ];
376 WINDOW *extwin = NULL, *extwin2 = NULL;
378 if (iflags.extmenu) {
379 return extcmd_via_menu();
384 if (iflags.wc_popup_dialog) { /* Prompt in popup window */
385 int x0, y0, w, h; /* bounding coords of popup */
387 extwin2 = curses_create_window(25, 1, UP);
389 /* create window inside window to prevent overwriting of border */
390 getbegyx(extwin2, y0, x0);
391 getmaxyx(extwin2, h, w);
392 extwin = newwin(1, w - 2, y0 + 1, x0 + 1);
395 nhUse(h); /* needed only to give getmaxyx three arguments */
397 curses_get_window_xy(MESSAGE_WIN, &winx, &winy);
398 curses_get_window_size(MESSAGE_WIN, &messageh, &messagew);
400 if (curses_window_has_border(MESSAGE_WIN)) {
405 winy += messageh - 1;
406 extwin = newwin(1, messagew - 2, winy, winx);
407 if (messagew - 4 < maxlen)
408 maxlen = messagew - 4;
409 custompline(OVERRIDE_MSGTYPE, "#");
412 cur_choice[0] = '\0';
415 wmove(extwin, starty, startx);
416 waddstr(extwin, "# ");
417 wmove(extwin, starty, startx + 2);
418 waddstr(extwin, cur_choice);
419 wmove(extwin, starty, (int) strlen(cur_choice) + startx + 2);
422 /* if we have an autocomplete command, AND it matches uniquely */
424 curses_toggle_color_attr(extwin, NONE, A_UNDERLINE, ON);
425 wmove(extwin, starty, (int) strlen(cur_choice) + startx + 2);
426 wprintw(extwin, "%s",
427 extcmdlist[ret].ef_txt + (int) strlen(cur_choice));
428 curses_toggle_color_attr(extwin, NONE, A_UNDERLINE, OFF);
435 prompt_width = (int) strlen(cur_choice);
438 if (letter == '\033' || letter == ERR) {
443 if (letter == '\r' || letter == '\n') {
445 for (count = 0; extcmdlist[count].ef_txt; count++) {
446 if (!strcasecmp(cur_choice, extcmdlist[count].ef_txt)) {
455 if (letter == '\177') /* DEL/Rubout */
457 if (letter == '\b' || letter == KEY_BACKSPACE
458 || (erase_char && letter == (int) (uchar) erase_char)) {
459 if (prompt_width == 0) {
463 cur_choice[prompt_width - 1] = '\0';
468 /* honor kill_char if it's ^U or similar, but not if it's '@' */
469 } else if (kill_char && letter == (int) (uchar) kill_char
470 && (letter < ' ' || letter >= '\177')) { /*ASCII*/
471 cur_choice[0] = '\0';
476 if (letter != '*' && prompt_width < maxlen) {
477 cur_choice[prompt_width] = letter;
478 cur_choice[prompt_width + 1] = '\0';
481 for (count = 0; extcmdlist[count].ef_txt; count++) {
482 if (!wizard && (extcmdlist[count].flags & WIZMODECMD))
484 if (!(extcmdlist[count].flags & AUTOCOMPLETE))
486 if ((int) strlen(extcmdlist[count].ef_txt) > prompt_width) {
487 if (strncasecmp(cur_choice, extcmdlist[count].ef_txt,
488 prompt_width) == 0) {
489 if ((extcmdlist[count].ef_txt[prompt_width] ==
490 lowc(letter)) || letter == '*') {
502 curses_destroy_win(extwin);
504 curses_destroy_win(extwin2);
509 /* Initialize a menu from given NetHack winid */
512 curses_create_nhmenu(winid wid)
514 nhmenu *new_menu = NULL;
515 nhmenu *menuptr = nhmenus;
516 nhmenu_item *menu_item_ptr = NULL;
517 nhmenu_item *tmp_menu_item = NULL;
519 new_menu = get_menu(wid);
521 if (new_menu != NULL) {
522 /* Reuse existing menu, clearing out current entries */
523 menu_item_ptr = new_menu->entries;
525 if (menu_item_ptr != NULL) {
526 while (menu_item_ptr->next_item != NULL) {
527 tmp_menu_item = menu_item_ptr->next_item;
528 free((genericptr_t) menu_item_ptr->str);
529 free((genericptr_t) menu_item_ptr);
530 menu_item_ptr = tmp_menu_item;
532 free((genericptr_t) menu_item_ptr->str);
533 free((genericptr_t) menu_item_ptr); /* Last entry */
534 new_menu->entries = NULL;
536 if (new_menu->prompt != NULL) { /* Reusing existing menu */
537 free((genericptr_t) new_menu->prompt);
538 new_menu->prompt = NULL;
540 new_menu->num_pages = 0;
541 new_menu->height = 0;
543 new_menu->reuse_accels = FALSE;
544 new_menu->bottom_heavy = FALSE;
548 new_menu = (nhmenu *) alloc((signed) sizeof (nhmenu));
550 new_menu->prompt = NULL;
551 new_menu->entries = NULL;
552 new_menu->num_pages = 0;
553 new_menu->height = 0;
555 new_menu->reuse_accels = FALSE;
556 new_menu->bottom_heavy = FALSE;
557 new_menu->next_menu = NULL;
559 if (nhmenus == NULL) { /* no menus in memory yet */
560 new_menu->prev_menu = NULL;
563 while (menuptr->next_menu != NULL) {
564 menuptr = menuptr->next_menu;
566 new_menu->prev_menu = menuptr;
567 menuptr->next_menu = new_menu;
572 curs_new_menu_item(winid wid, const char *str)
575 nhmenu_item *new_item;
577 new_str = curses_copy_of(str);
578 curses_rtrim(new_str);
579 new_item = (nhmenu_item *) alloc((unsigned) sizeof (nhmenu_item));
581 new_item->glyph = NO_GLYPH;
582 new_item->identifier = zeroany;
583 new_item->accelerator = '\0';
584 new_item->group_accel = '\0';
586 new_item->str = new_str;
587 new_item->presel = FALSE;
588 new_item->selected = FALSE;
589 new_item->page_num = 0;
590 new_item->line_num = 0;
591 new_item->num_lines = 0;
592 new_item->count = -1;
593 new_item->next_item = NULL;
596 /* Add a menu item to the given menu window */
599 curses_add_nhmenu_item(winid wid, int glyph, const ANY_P *identifier,
600 CHAR_P accelerator, CHAR_P group_accel, int attr,
601 const char *str, BOOLEAN_P presel)
603 nhmenu_item *new_item, *current_items, *menu_item_ptr;
604 nhmenu *current_menu = get_menu(wid);
606 if (current_menu == NULL) {
608 "curses_add_nhmenu_item: attempt to add item to nonexistent menu");
616 new_item = curs_new_menu_item(wid, str);
617 new_item->glyph = glyph;
618 new_item->identifier = *identifier;
619 new_item->accelerator = accelerator;
620 new_item->group_accel = group_accel;
621 new_item->attr = attr;
622 new_item->presel = presel;
624 current_items = current_menu->entries;
625 menu_item_ptr = current_items;
627 if (current_items == NULL) {
628 new_item->prev_item = NULL;
629 current_menu->entries = new_item;
631 while (menu_item_ptr->next_item != NULL) {
632 menu_item_ptr = menu_item_ptr->next_item;
634 new_item->prev_item = menu_item_ptr;
635 menu_item_ptr->next_item = new_item;
639 /* for menu->bottom_heavy -- insert enough blank lines at top of
640 first page to make the last page become a full one */
642 curs_pad_menu(nhmenu *current_menu, boolean do_pad UNUSED)
644 nhmenu_item *menu_item_ptr;
645 int numpages = current_menu->num_pages;
647 /* caller has already called menu_win_size() */
648 menu_determine_pages(current_menu); /* sets 'menu->num_pages' */
649 numpages = current_menu->num_pages;
650 /* pad beginning of menu so that partial last page becomes full;
651 might be slightly less than full if any entries take multiple
652 lines and the padding would force those to span page boundary
653 and that gets prevented; so we re-count the number of pages
654 with every insertion instead of trying to calculate the number
657 menu_item_ptr = curs_new_menu_item(current_menu->wid, "");
658 menu_item_ptr->next_item = current_menu->entries;
659 current_menu->entries->prev_item = menu_item_ptr;
660 current_menu->entries = menu_item_ptr;
661 current_menu->num_entries += 1;
663 menu_determine_pages(current_menu);
664 } while (current_menu->num_pages == numpages);
666 /* we inserted blank lines at beginning until final entry spilled
667 over to another page; take the most recent blank one back out */
668 current_menu->num_entries -= 1;
669 current_menu->entries = menu_item_ptr->next_item;
670 current_menu->entries->prev_item = (nhmenu_item *) 0;
671 free((genericptr_t) menu_item_ptr->str);
672 free((genericptr_t) menu_item_ptr);
674 /* reset page count; shouldn't need to re-count */
675 current_menu->num_pages = numpages;
679 /* mark ^P message recall menu, for msg_window:full (FIFO), where we'll
680 start viewing on the last page so be able to see most recent immediately */
682 curs_menu_set_bottom_heavy(winid wid)
684 nhmenu *menu = get_menu(wid);
687 * Called after end_menu + finalize_nhmenu,
688 * before select_menu + display_nhmenu.
690 menu_win_size(menu); /* (normally not done until display_nhmenu) */
691 if (menu_is_multipage(menu, menu->width, menu->height)) {
692 menu->bottom_heavy = TRUE;
694 /* insert enough blank lines at top of first page to make the
695 last page become a full one */
696 curs_pad_menu(menu, TRUE);
701 /* No more entries are to be added to menu, so details of the menu can be
702 finalized in memory */
705 curses_finalize_nhmenu(winid wid, const char *prompt)
708 nhmenu_item *menu_item_ptr;
709 nhmenu *current_menu = get_menu(wid);
711 if (current_menu == NULL) {
713 "curses_finalize_nhmenu: attempt to finalize nonexistent menu");
716 for (menu_item_ptr = current_menu->entries;
717 menu_item_ptr != NULL; menu_item_ptr = menu_item_ptr->next_item)
720 current_menu->num_entries = count;
721 current_menu->prompt = curses_copy_of(prompt);
725 /* Display a nethack menu, and return a selection, if applicable */
728 curses_display_nhmenu(winid wid, int how, MENU_ITEM_P ** _selected)
730 nhmenu *current_menu = get_menu(wid);
731 nhmenu_item *menu_item_ptr;
732 int num_chosen, count;
734 MENU_ITEM_P *selected = NULL;
738 if (current_menu == NULL) {
740 "curses_display_nhmenu: attempt to display nonexistent menu");
744 menu_item_ptr = current_menu->entries;
746 if (menu_item_ptr == NULL) {
747 impossible("curses_display_nhmenu: attempt to display empty menu");
751 /* Reset items to unselected to clear out selections from previous
752 invocations of this menu, and preselect appropriate items */
753 while (menu_item_ptr != NULL) {
754 menu_item_ptr->selected = menu_item_ptr->presel;
755 menu_item_ptr = menu_item_ptr->next_item;
758 menu_win_size(current_menu);
759 menu_determine_pages(current_menu);
761 /* Display pre and post-game menus centered */
762 if ((moves <= 1 && !invent) || program_state.gameover) {
763 win = curses_create_window(current_menu->width,
764 current_menu->height, CENTER);
765 } else { /* Display during-game menus on the right out of the way */
766 win = curses_create_window(current_menu->width,
767 current_menu->height, RIGHT);
770 num_chosen = menu_get_selections(win, current_menu, how);
771 curses_destroy_win(win);
773 if (num_chosen > 0) {
774 selected = (MENU_ITEM_P *) alloc((unsigned)
775 (num_chosen * sizeof (MENU_ITEM_P)));
778 menu_item_ptr = current_menu->entries;
780 while (menu_item_ptr != NULL) {
781 if (menu_item_ptr->selected) {
782 if (count == num_chosen) {
783 impossible("curses_display_nhmenu: Selected items "
784 "exceeds expected number");
787 selected[count].item = menu_item_ptr->identifier;
788 selected[count].count = menu_item_ptr->count;
791 menu_item_ptr = menu_item_ptr->next_item;
794 if (count != num_chosen) {
796 "curses_display_nhmenu: Selected items less than expected number");
800 *_selected = selected;
807 curses_menu_exists(winid wid)
809 if (get_menu(wid) != NULL) {
816 /* Delete the menu associated with the given NetHack winid from memory */
819 curses_del_menu(winid wid, boolean del_wid_too)
821 nhmenu_item *tmp_menu_item;
822 nhmenu_item *menu_item_ptr;
824 nhmenu *current_menu = get_menu(wid);
826 if (current_menu == NULL) {
830 menu_item_ptr = current_menu->entries;
832 /* First free entries associated with this menu from memory */
833 if (menu_item_ptr != NULL) {
834 while (menu_item_ptr->next_item != NULL) {
835 tmp_menu_item = menu_item_ptr->next_item;
836 free((genericptr_t) menu_item_ptr->str);
838 menu_item_ptr = tmp_menu_item;
840 free((genericptr_t) menu_item_ptr->str);
841 free(menu_item_ptr); /* Last entry */
842 current_menu->entries = NULL;
845 /* Now unlink the menu from the list and free it as well */
846 if ((tmpmenu = current_menu->prev_menu) != NULL) {
847 tmpmenu->next_menu = current_menu->next_menu;
849 nhmenus = current_menu->next_menu; /* New head node or NULL */
851 if ((tmpmenu = current_menu->next_menu) != NULL) {
852 tmpmenu->prev_menu = current_menu->prev_menu;
855 if (current_menu->prompt)
856 free((genericptr_t) current_menu->prompt);
857 free((genericptr_t) current_menu);
864 /* return a pointer to the menu associated with the given NetHack winid */
869 nhmenu *menuptr = nhmenus;
871 while (menuptr != NULL) {
872 if (menuptr->wid == wid) {
875 menuptr = menuptr->next_menu;
878 return NULL; /* Not found */
883 menu_get_accel(boolean first)
885 static char next_letter;
894 if ((next_letter < 'z' && next_letter >= 'a')
895 || (next_letter < 'Z' && next_letter >= 'A')) {
897 } else if (next_letter == 'z') {
899 } else if (next_letter == 'Z') {
900 next_letter = 'a'; /* wrap to beginning */
907 /* Determine if menu will require multiple pages to display */
910 menu_is_multipage(nhmenu *menu, int width, int height)
913 int curline = 0, accel_per_page = 0;
914 nhmenu_item *menu_item_ptr = menu->entries;
917 curline += curses_num_lines(menu->prompt, width) + 1;
920 while (menu_item_ptr != NULL) {
921 menu_item_ptr->line_num = curline;
922 if (menu_item_ptr->identifier.a_void == NULL) {
923 num_lines = curses_num_lines(menu_item_ptr->str, width);
925 if (menu_item_ptr->accelerator != GOLD_SYM) {
926 if (++accel_per_page > MAX_ACCEL_PER_PAGE)
929 /* Add space for accelerator */
930 num_lines = curses_num_lines(menu_item_ptr->str, width - 4);
932 menu_item_ptr->num_lines = num_lines;
933 curline += num_lines;
934 menu_item_ptr = menu_item_ptr->next_item;
936 || (curline > height - 2 && height == menu_max_height())) {
940 return (menu_item_ptr != NULL) ? TRUE : FALSE;
944 /* Determine which entries go on which page, and total number of pages */
947 menu_determine_pages(nhmenu *menu)
949 int tmpline, num_lines, accel_per_page;
952 nhmenu_item *menu_item_ptr = menu->entries;
953 int width = menu->width;
954 int height = menu->height;
955 int page_end = height;
959 curline += curses_num_lines(menu->prompt, width) + 1;
963 if (menu_is_multipage(menu, width, height)) {
964 page_end -= 2; /* Room to display current page number */
968 /* Determine what entries belong on which page */
969 for (menu_item_ptr = menu->entries; menu_item_ptr != NULL;
970 menu_item_ptr = menu_item_ptr->next_item) {
971 menu_item_ptr->page_num = page_num;
972 menu_item_ptr->line_num = curline;
973 if (menu_item_ptr->identifier.a_void == NULL) {
974 num_lines = curses_num_lines(menu_item_ptr->str, width);
976 if (menu_item_ptr->accelerator != GOLD_SYM)
978 /* Add space for accelerator */
979 num_lines = curses_num_lines(menu_item_ptr->str, width - 4);
981 menu_item_ptr->num_lines = num_lines;
982 curline += num_lines;
983 if (curline > page_end || accel_per_page > MAX_ACCEL_PER_PAGE) {
985 accel_per_page = 0; /* reset */
987 /* Move ptr back so entry will be reprocessed on new page */
988 menu_item_ptr = menu_item_ptr->prev_item;
992 menu->num_pages = page_num;
996 /* Determine dimensions of menu window based on term size and entries */
999 menu_win_size(nhmenu *menu)
1001 int maxwidth, maxheight, curentrywidth, lastline;
1002 int maxentrywidth = 0;
1003 int maxheaderwidth = menu->prompt ? (int) strlen(menu->prompt) : 0;
1004 nhmenu_item *menu_item_ptr;
1006 if (program_state.gameover) {
1007 /* for final inventory disclosure, use full width */
1008 maxwidth = term_cols - 2; /* +2: borders assumed */
1010 /* this used to be 38, which is 80/2 - 2 (half a 'normal' sized
1011 screen minus room for a border box), but some data files
1012 have been manually formatted for 80 columns (usually limited
1013 to 78 but sometimes 79, rarely 80 itself) and using a value
1014 less that 40 meant that a full line would wrap twice:
1015 1..38, 39..76, and 77..80 */
1016 maxwidth = 40; /* Reasonable minimum usable width */
1017 if ((term_cols / 2) > maxwidth)
1018 maxwidth = (term_cols / 2); /* Half the screen */
1020 maxheight = menu_max_height();
1022 /* First, determine the width of the longest menu entry */
1023 for (menu_item_ptr = menu->entries; menu_item_ptr != NULL;
1024 menu_item_ptr = menu_item_ptr->next_item) {
1025 curentrywidth = (int) strlen(menu_item_ptr->str);
1026 if (menu_item_ptr->identifier.a_void == NULL) {
1027 if (curentrywidth > maxheaderwidth) {
1028 maxheaderwidth = curentrywidth;
1031 /* Add space for accelerator (selector letter) */
1033 #if 0 /* FIXME: menu glyphs */
1034 if (menu_item_ptr->glyph != NO_GLYPH && iflags.use_menu_glyphs)
1038 if (curentrywidth > maxentrywidth) {
1039 maxentrywidth = curentrywidth;
1044 * 3.6.3: This used to set maxwidth to maxheaderwidth when that was
1045 * bigger but only set it to maxentrywidth if the latter was smaller,
1046 * so entries wider than the default would always wrap unless at
1047 * least one header or separator line was long, even when there was
1048 * lots of space available to display them without wrapping. The
1049 * reason to force narrow menus isn't known. It may have been to
1050 * reduce the amount of map rewriting when menu is dismissed, but if
1051 * so, that was an issue due to excessive screen writing for the map
1052 * (output was flushed for each character) which was fixed long ago.
1054 maxwidth = max(maxentrywidth, maxheaderwidth);
1055 if (maxwidth > term_cols - 2) { /* -2: space for left and right borders */
1056 maxwidth = term_cols - 2;
1059 /* Possibly reduce height if only 1 page */
1060 if (!menu_is_multipage(menu, maxwidth, maxheight)) {
1061 menu_item_ptr = menu->entries;
1063 while (menu_item_ptr->next_item != NULL) {
1064 menu_item_ptr = menu_item_ptr->next_item;
1067 lastline = (menu_item_ptr->line_num) + menu_item_ptr->num_lines;
1069 if (lastline < maxheight) {
1070 maxheight = lastline;
1074 /* avoid a tiny popup window; when it's shown over the endings of
1075 old messsages rather than over the map, it is fairly easy for
1076 the player to overlook it, particularly when walking around and
1077 stepping on a pile of 2 items; also, multi-page menus need enough
1078 room for "(Page M of N) => " even if all entries are narrower
1079 than that; we specify same minimum width even when single page */
1080 menu->width = max(maxwidth, 25);
1081 menu->height = max(maxheight, 5);
1085 /* Displays menu selections in the given window */
1088 menu_display_page(nhmenu *menu, WINDOW * win, int page_num, char *selectors)
1090 nhmenu_item *menu_item_ptr;
1091 int count, curletter, entry_cols, start_col, num_lines;
1093 boolean first_accel = TRUE;
1094 int color = NO_COLOR, attr = A_NORMAL;
1095 boolean menu_color = FALSE;
1097 /* letters assigned to entries on current page */
1099 (void) memset((genericptr_t) selectors, 0, 256);
1101 /* Cycle through entries until we are on the correct page */
1103 menu_item_ptr = menu->entries;
1105 while (menu_item_ptr != NULL) {
1106 if (menu_item_ptr->page_num == page_num) {
1109 menu_item_ptr = menu_item_ptr->next_item;
1112 if (menu_item_ptr == NULL) { /* Page not found */
1113 impossible("menu_display_page: attempt to display nonexistent page");
1119 if (menu->prompt && *menu->prompt) {
1120 num_lines = curses_num_lines(menu->prompt, menu->width);
1122 for (count = 0; count < num_lines; count++) {
1123 tmpstr = curses_break_str(menu->prompt, menu->width, count + 1);
1124 mvwprintw(win, count + 1, 1, "%s", tmpstr);
1129 /* Display items for current page */
1131 while (menu_item_ptr != NULL) {
1132 if (menu_item_ptr->page_num != page_num) {
1135 if (menu_item_ptr->identifier.a_void != NULL) {
1136 if (menu_item_ptr->accelerator != 0) {
1137 curletter = menu_item_ptr->accelerator;
1140 curletter = menu_get_accel(TRUE);
1141 first_accel = FALSE;
1142 if (!menu->reuse_accels && (menu->num_pages > 1)) {
1143 menu->reuse_accels = TRUE;
1146 curletter = menu_get_accel(FALSE);
1148 menu_item_ptr->accelerator = curletter;
1150 /* we have a selector letter; tell caller about it */
1152 selectors[(unsigned) (curletter & 0xFF)]++;
1154 if (menu_item_ptr->selected) {
1155 curses_toggle_color_attr(win, HIGHLIGHT_COLOR, A_REVERSE, ON);
1156 mvwaddch(win, menu_item_ptr->line_num + 1, 1, '<');
1157 mvwaddch(win, menu_item_ptr->line_num + 1, 2, curletter);
1158 mvwaddch(win, menu_item_ptr->line_num + 1, 3, '>');
1159 curses_toggle_color_attr(win, HIGHLIGHT_COLOR, A_REVERSE, OFF);
1161 curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, ON);
1162 mvwaddch(win, menu_item_ptr->line_num + 1, 2, curletter);
1163 curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, OFF);
1164 mvwprintw(win, menu_item_ptr->line_num + 1, 3, ") ");
1167 entry_cols = menu->width;
1170 if (menu_item_ptr->identifier.a_void != NULL) {
1175 /* FIXME: menuglyphs not implemented yet */
1176 if (menu_item_ptr->glyph != NO_GLYPH && iflags.use_menu_glyphs) {
1177 unsigned special; /*notused */
1179 mapglyph(menu_item_ptr->glyph, &curletter, &color, &special, 0, 0, 0);
1180 curses_toggle_color_attr(win, color, NONE, ON);
1181 mvwaddch(win, menu_item_ptr->line_num + 1, start_col, curletter);
1182 curses_toggle_color_attr(win, color, NONE, OFF);
1183 mvwaddch(win, menu_item_ptr->line_num + 1, start_col + 1, ' ');
1189 menu_color = iflags.use_menu_color
1190 && get_menu_coloring(menu_item_ptr->str, &color, &attr);
1192 attr = curses_convert_attr(attr);
1193 if (color != NO_COLOR || attr != A_NORMAL)
1194 curses_menu_color_attr(win, color, attr, ON);
1196 attr = menu_item_ptr->attr;
1197 if (color != NO_COLOR || attr != A_NORMAL)
1198 curses_toggle_color_attr(win, color, attr, ON);
1201 num_lines = curses_num_lines(menu_item_ptr->str, entry_cols);
1202 for (count = 0; count < num_lines; count++) {
1203 if (menu_item_ptr->str && *menu_item_ptr->str) {
1204 tmpstr = curses_break_str(menu_item_ptr->str,
1205 entry_cols, count + 1);
1206 mvwprintw(win, menu_item_ptr->line_num + count + 1, start_col,
1211 if (color != NO_COLOR || attr != A_NORMAL) {
1213 curses_menu_color_attr(win, color, attr, OFF);
1215 curses_toggle_color_attr(win, color, attr, OFF);
1218 menu_item_ptr = menu_item_ptr->next_item;
1221 if (menu->num_pages > 1) {
1222 int footer_x, footwidth, shoesize = menu->num_pages;
1224 footwidth = (int) (sizeof "<- (Page X of Y) ->" - sizeof "");
1225 while (shoesize >= 10) { /* possible for pickup from big piles... */
1226 /* room for wider feet; extra digit for both X and Y */
1230 footer_x = !menu->bottom_heavy ? (menu->width - footwidth) : 2;
1231 if (page_num != 1) {
1232 curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, ON);
1233 mvwaddstr(win, menu->height, footer_x, "<=");
1234 curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, OFF);
1236 mvwprintw(win, menu->height, footer_x + 2, " (Page %d of %d) ",
1237 page_num, menu->num_pages);
1238 if (page_num != menu->num_pages) {
1239 curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, ON);
1240 mvwaddstr(win, menu->height, footer_x + footwidth - 2, "=>");
1241 curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, OFF);
1244 curses_toggle_color_attr(win, DIALOG_BORDER_COLOR, NONE, ON);
1246 curses_toggle_color_attr(win, DIALOG_BORDER_COLOR, NONE, OFF);
1252 menu_get_selections(WINDOW * win, nhmenu *menu, int how)
1256 int count_letter = '\0';
1257 int curpage = !menu->bottom_heavy ? 1 : menu->num_pages;
1258 int num_selected = 0;
1259 boolean dismiss = FALSE;
1260 char search_key[BUFSZ], selectors[256];
1261 nhmenu_item *menu_item_ptr = menu->entries;
1263 menu_display_page(menu, win, curpage, selectors);
1266 curletter = getch();
1268 if (curletter == ERR) {
1273 if (curletter == '\033') {
1274 curletter = curses_convert_keys(curletter);
1279 if (menu->num_pages == 1) {
1280 if (curletter == KEY_ESC) {
1290 if (curletter <= 0 || curletter >= 256 || !selectors[curletter]) {
1291 switch (curletter) {
1292 case MENU_SELECT_PAGE:
1293 (void) menu_operation(win, menu, SELECT, curpage);
1295 case MENU_SELECT_ALL:
1296 curpage = menu_operation(win, menu, SELECT, 0);
1298 case MENU_UNSELECT_PAGE:
1299 (void) menu_operation(win, menu, DESELECT, curpage);
1301 case MENU_UNSELECT_ALL:
1302 curpage = menu_operation(win, menu, DESELECT, 0);
1304 case MENU_INVERT_PAGE:
1305 (void) menu_operation(win, menu, INVERT, curpage);
1307 case MENU_INVERT_ALL:
1308 curpage = menu_operation(win, menu, INVERT, 0);
1314 if (isdigit(curletter)) {
1315 count = curses_get_count(curletter - '0');
1318 curletter = getch();
1320 count_letter = curletter;
1325 if (curletter <= 0 || curletter >= 256 || !selectors[curletter]) {
1326 switch (curletter) {
1337 case MENU_NEXT_PAGE:
1339 if (curpage < menu->num_pages) {
1341 menu_display_page(menu, win, curpage, selectors);
1342 } else if (curletter == ' ') {
1349 case MENU_PREVIOUS_PAGE:
1352 menu_display_page(menu, win, curpage, selectors);
1356 case MENU_LAST_PAGE:
1357 if (curpage != menu->num_pages) {
1358 curpage = menu->num_pages;
1359 menu_display_page(menu, win, curpage, selectors);
1363 case MENU_FIRST_PAGE:
1366 menu_display_page(menu, win, curpage, selectors);
1370 search_key[0] = '\0';
1371 curses_line_input_dialog("Search for:", search_key, BUFSZ);
1380 menu_item_ptr = menu->entries;
1382 while (menu_item_ptr != NULL) {
1383 if (menu_item_ptr->identifier.a_void != NULL
1384 && strstri(menu_item_ptr->str, search_key)) {
1385 if (how == PICK_ONE) {
1386 menu_clear_selections(menu);
1387 menu_select_deselect(win, menu_item_ptr,
1393 menu_select_deselect(win, menu_item_ptr,
1398 menu_item_ptr = menu_item_ptr->next_item;
1401 menu_item_ptr = menu->entries;
1404 if (how == PICK_NONE) {
1412 menu_item_ptr = menu->entries;
1413 while (menu_item_ptr != NULL) {
1414 if (menu_item_ptr->identifier.a_void != NULL) {
1415 if ((curletter == menu_item_ptr->accelerator
1416 && (curpage == menu_item_ptr->page_num
1417 || !menu->reuse_accels))
1418 || (menu_item_ptr->group_accel
1419 && curletter == menu_item_ptr->group_accel)) {
1420 if (curpage != menu_item_ptr->page_num) {
1421 curpage = menu_item_ptr->page_num;
1422 menu_display_page(menu, win, curpage, selectors);
1425 if (how == PICK_ONE) {
1426 menu_clear_selections(menu);
1427 menu_select_deselect(win, menu_item_ptr,
1430 menu_item_ptr->count = count;
1434 } else if (how == PICK_ANY && curletter == count_letter) {
1435 menu_select_deselect(win, menu_item_ptr,
1437 menu_item_ptr->count = count;
1439 count_letter = '\0';
1441 menu_select_deselect(win, menu_item_ptr,
1446 menu_item_ptr = menu_item_ptr->next_item;
1450 if (how == PICK_ANY && num_selected != -1) {
1452 menu_item_ptr = menu->entries;
1454 while (menu_item_ptr != NULL) {
1455 if (menu_item_ptr->identifier.a_void != NULL) {
1456 if (menu_item_ptr->selected) {
1460 menu_item_ptr = menu_item_ptr->next_item;
1464 return num_selected;
1468 /* Select, deselect, or toggle selected for the given menu entry.
1469 For search operations, the toggled entry might be on a different
1470 page than the one currently shown. */
1473 menu_select_deselect(WINDOW *win, nhmenu_item *item,
1474 menu_op operation, int current_page)
1476 int curletter = item->accelerator;
1477 boolean visible = (item->page_num == current_page);
1479 if (operation == DESELECT || (item->selected && operation == INVERT)) {
1480 item->selected = FALSE;
1482 mvwaddch(win, item->line_num + 1, 1, ' ');
1483 curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, ON);
1484 mvwaddch(win, item->line_num + 1, 2, curletter);
1485 curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, OFF);
1486 mvwaddch(win, item->line_num + 1, 3, ')');
1489 item->selected = TRUE;
1491 curses_toggle_color_attr(win, HIGHLIGHT_COLOR, A_REVERSE, ON);
1492 mvwaddch(win, item->line_num + 1, 1, '<');
1493 mvwaddch(win, item->line_num + 1, 2, curletter);
1494 mvwaddch(win, item->line_num + 1, 3, '>');
1495 curses_toggle_color_attr(win, HIGHLIGHT_COLOR, A_REVERSE, OFF);
1503 /* Perform the selected operation (select, unselect, invert selection)
1504 on the given menu page. If menu_page is 0, then perform opetation on
1505 all pages in menu. Returns last page displayed. */
1508 menu_operation(WINDOW * win, nhmenu *menu, menu_op
1509 operation, int page_num)
1511 int first_page, last_page, current_page;
1512 nhmenu_item *menu_item_ptr = menu->entries;
1514 if (page_num == 0) { /* Operation to occur on all pages */
1516 last_page = menu->num_pages;
1518 first_page = page_num;
1519 last_page = page_num;
1522 /* Cycle through entries until we are on the correct page */
1523 while (menu_item_ptr != NULL) {
1524 if (menu_item_ptr->page_num == first_page) {
1527 menu_item_ptr = menu_item_ptr->next_item;
1530 current_page = first_page;
1532 if (page_num == 0) {
1533 menu_display_page(menu, win, current_page, (char *) 0);
1536 if (menu_item_ptr == NULL) { /* Page not found */
1537 impossible("menu_display_page: attempt to display nonexistent page");
1541 while (menu_item_ptr != NULL) {
1542 if (menu_item_ptr->page_num != current_page) {
1543 if (menu_item_ptr->page_num > last_page) {
1547 current_page = menu_item_ptr->page_num;
1548 menu_display_page(menu, win, current_page, (char *) 0);
1551 if (menu_item_ptr->identifier.a_void != NULL) {
1552 menu_select_deselect(win, menu_item_ptr, operation, current_page);
1555 menu_item_ptr = menu_item_ptr->next_item;
1558 return current_page;
1562 /* Set all menu items to unselected in menu */
1565 menu_clear_selections(nhmenu *menu)
1567 nhmenu_item *menu_item_ptr = menu->entries;
1569 while (menu_item_ptr != NULL) {
1570 menu_item_ptr->selected = FALSE;
1571 menu_item_ptr = menu_item_ptr->next_item;
1576 /* Get the maximum height for a menu */
1579 menu_max_height(void)
1581 return term_rows - 2;