Merge branch 'vendor/DIALOG'
[dragonfly.git] / contrib / dialog / buildlist.c
1 /*
2  *  $Id: buildlist.c,v 1.61 2015/01/25 23:52:45 tom Exp $
3  *
4  *  buildlist.c -- implements the buildlist dialog
5  *
6  *  Copyright 2012-2014,2015    Thomas E. Dickey
7  *
8  *  This program is free software; you can redistribute it and/or modify
9  *  it under the terms of the GNU Lesser General Public License, version 2.1
10  *  as published by the Free Software Foundation.
11  *
12  *  This program is distributed in the hope that it will be useful, but
13  *  WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  *  Lesser General Public License for more details.
16  *
17  *  You should have received a copy of the GNU Lesser General Public
18  *  License along with this program; if not, write to
19  *      Free Software Foundation, Inc.
20  *      51 Franklin St., Fifth Floor
21  *      Boston, MA 02110, USA.
22  */
23
24 #include <dialog.h>
25 #include <dlg_keys.h>
26
27 /*
28  * Visually like menubox, but two columns.
29  */
30
31 #define sLEFT         (-2)
32 #define sRIGHT        (-1)
33
34 #define KEY_TOGGLE    ' '
35 #define KEY_LEFTCOL   '^'
36 #define KEY_RIGHTCOL  '$'
37
38 #define MIN_HIGH  (1 + (5 * MARGIN))
39
40 typedef struct {
41     WINDOW *win;
42     int box_y;
43     int box_x;
44     int top_index;
45     int cur_index;
46 } MY_DATA;
47
48 typedef struct {
49     DIALOG_LISTITEM *items;
50     int base_y;                 /* base for mouse coordinates */
51     int base_x;
52     int use_height;             /* actual size of column box */
53     int use_width;
54     int item_no;
55     int check_x;
56     int item_x;
57     MY_DATA list[2];
58 } ALL_DATA;
59
60 /*
61  * Print list item.  The 'selected' parameter is true if 'choice' is the
62  * current item.  That one is colored differently from the other items.
63  */
64 static void
65 print_item(ALL_DATA * data,
66            WINDOW *win,
67            DIALOG_LISTITEM * item,
68            int choice,
69            int selected)
70 {
71     chtype save = dlg_get_attrs(win);
72     int i;
73     bool both = (!dialog_vars.no_tags && !dialog_vars.no_items);
74     bool first = TRUE;
75     int climit = (data->item_x - data->check_x - 1);
76     const char *show = (dialog_vars.no_items
77                         ? item->name
78                         : item->text);
79
80     /* Clear 'residue' of last item */
81     (void) wattrset(win, menubox_attr);
82     (void) wmove(win, choice, 0);
83     for (i = 0; i < getmaxx(win); i++)
84         (void) waddch(win, ' ');
85
86     (void) wmove(win, choice, data->check_x);
87     (void) wattrset(win, menubox_attr);
88
89     if (both) {
90         dlg_print_listitem(win, item->name, climit, first, selected);
91         (void) waddch(win, ' ');
92         first = FALSE;
93     }
94
95     (void) wmove(win, choice, data->item_x);
96     climit = (getmaxx(win) - data->item_x + 1);
97     dlg_print_listitem(win, show, climit, first, selected);
98
99     if (selected) {
100         dlg_item_help(item->help);
101     }
102     (void) wattrset(win, save);
103 }
104
105 /*
106  * Prints either the left (unselected) or right (selected) list.
107  */
108 static void
109 print_1_list(ALL_DATA * data,
110              int choice,
111              int selected)
112 {
113     MY_DATA *moi = data->list + selected;
114     WINDOW *win = moi->win;
115     int i, j;
116     int last = 0;
117     int max_rows = getmaxy(win);
118
119     for (i = j = 0; j < max_rows; i++) {
120         int ii = i + moi->top_index;
121         if (ii < 0) {
122             continue;
123         } else if (ii >= data->item_no) {
124             break;
125         } else if (!(selected ^ (data->items[ii].state != 0))) {
126             print_item(data,
127                        win,
128                        &data->items[ii],
129                        j, ii == choice);
130             last = ++j;
131         }
132     }
133     if (wmove(win, last, 0) != ERR)
134         wclrtobot(win);
135     (void) wnoutrefresh(win);
136 }
137
138 /*
139  * Return the previous item from the list, staying in the same column.  If no
140  * further movement is possible, return the same choice as given.
141  */
142 static int
143 prev_item(ALL_DATA * data, int choice, int selected)
144 {
145     int result = choice;
146     int n;
147
148     for (n = choice - 1; n >= 0; --n) {
149         if ((data->items[n].state != 0) == selected) {
150             result = n;
151             break;
152         }
153     }
154     return result;
155 }
156
157 /*
158  * Return true if the given choice is on the first page in the current column.
159  */
160 static bool
161 stop_prev(ALL_DATA * data, int choice, int selected)
162 {
163     return (prev_item(data, choice, selected) == choice);
164 }
165
166 static bool
167 check_hotkey(DIALOG_LISTITEM * items, int choice, int selected)
168 {
169     bool result = FALSE;
170
171     if ((items[choice].state != 0) == selected) {
172         if (dlg_match_char(dlg_last_getc(),
173                            (dialog_vars.no_tags
174                             ? items[choice].text
175                             : items[choice].name))) {
176             result = TRUE;
177         }
178     }
179     return result;
180 }
181
182 /*
183  * Return the next item from the list, staying in the same column.  If no
184  * further movement is possible, return the same choice as given.
185  */
186 static int
187 next_item(ALL_DATA * data, int choice, int selected)
188 {
189     int result = choice;
190     int n;
191
192     for (n = choice + 1; n < data->item_no; ++n) {
193         if ((data->items[n].state != 0) == selected) {
194             result = n;
195             break;
196         }
197     }
198     dlg_trace_msg("next_item(%d) ->%d\n", choice, result);
199     return result;
200 }
201
202 /*
203  * Translate a choice from items[] to a row-number in an unbounded column,
204  * starting at zero.
205  */
206 static int
207 index2row(ALL_DATA * data, int choice, int selected)
208 {
209     int result = -1;
210     int n;
211     for (n = 0; n < data->item_no; ++n) {
212         if ((data->items[n].state != 0) == selected) {
213             ++result;
214         }
215         if (n == choice)
216             break;
217     }
218     return result;
219 }
220
221 /*
222  * Return the first choice from items[] for the given column.
223  */
224 static int
225 first_item(ALL_DATA * data, int selected)
226 {
227     int result = -1;
228     int n;
229
230     for (n = 0; n < data->item_no; ++n) {
231         if ((data->items[n].state != 0) == selected) {
232             result = n;
233             break;
234         }
235     }
236     return result;
237 }
238
239 /*
240  * Return the last choice from items[] for the given column.
241  */
242 static int
243 last_item(ALL_DATA * data, int selected)
244 {
245     int result = -1;
246     int n;
247
248     for (n = data->item_no - 1; n >= 0; --n) {
249         if ((data->items[n].state != 0) == selected) {
250             result = n;
251             break;
252         }
253     }
254     return result;
255 }
256
257 /*
258  * Convert a row-number back to an item number, i.e., index into items[].
259  */
260 static int
261 row2index(ALL_DATA * data, int row, int selected)
262 {
263     int result = -1;
264     int n;
265     for (n = 0; n < data->item_no; ++n) {
266         if ((data->items[n].state != 0) == selected) {
267             if (row-- <= 0) {
268                 result = n;
269                 break;
270             }
271         }
272     }
273     return result;
274 }
275
276 static int
277 skip_rows(ALL_DATA * data, int row, int skip, int selected)
278 {
279     int choice = row2index(data, row, selected);
280     int result = row;
281     int n;
282     if (skip > 0) {
283         for (n = choice + 1; n < data->item_no; ++n) {
284             if ((data->items[n].state != 0) == selected) {
285                 ++result;
286                 if (--skip <= 0)
287                     break;
288             }
289         }
290     } else if (skip < 0) {
291         for (n = choice - 1; n >= 0; --n) {
292             if ((data->items[n].state != 0) == selected) {
293                 --result;
294                 if (++skip >= 0)
295                     break;
296             }
297         }
298     }
299     return result;
300 }
301
302 /*
303  * Find the closest item in the given column starting with the given choice.
304  */
305 static int
306 closest_item(ALL_DATA * data, int choice, int selected)
307 {
308     int prev = choice;
309     int next = choice;
310     int result = choice;
311     int n;
312
313     for (n = choice; n >= 0; --n) {
314         if ((data->items[n].state != 0) == selected) {
315             prev = n;
316             break;
317         }
318     }
319     for (n = choice; n < data->item_no; ++n) {
320         if ((data->items[n].state != 0) == selected) {
321             next = n;
322             break;
323         }
324     }
325     if (prev != choice) {
326         result = prev;
327         if (next != choice) {
328             if ((choice - prev) > (next - choice)) {
329                 result = next;
330             }
331         }
332     } else if (next != choice) {
333         result = next;
334     }
335     return result;
336 }
337
338 static void
339 print_both(ALL_DATA * data,
340            int choice)
341 {
342     int selected;
343     int cur_y, cur_x;
344     WINDOW *dialog = wgetparent(data->list[0].win);
345
346     getyx(dialog, cur_y, cur_x);
347     for (selected = 0; selected < 2; ++selected) {
348         MY_DATA *moi = data->list + selected;
349         WINDOW *win = moi->win;
350         int thumb_top = index2row(data, moi->top_index, selected);
351         int thumb_max = index2row(data, -1, selected);
352         int thumb_end = thumb_top + getmaxy(win);
353
354         print_1_list(data, choice, selected);
355
356         dlg_mouse_setcode(selected * KEY_MAX);
357         dlg_draw_scrollbar(dialog,
358                            (long) (moi->top_index),
359                            (long) (thumb_top),
360                            (long) MIN(thumb_end, thumb_max),
361                            (long) thumb_max,
362                            moi->box_x + data->check_x,
363                            moi->box_x + getmaxx(win),
364                            moi->box_y,
365                            moi->box_y + getmaxy(win) + 1,
366                            menubox_border2_attr,
367                            menubox_border_attr);
368     }
369     (void) wmove(dialog, cur_y, cur_x);
370     dlg_mouse_setcode(0);
371 }
372
373 static void
374 set_top_item(ALL_DATA * data, int value, int selected)
375 {
376     if (value != data->list[selected].top_index) {
377         dlg_trace_msg("set top of %s column to %d\n",
378                       selected ? "right" : "left",
379                       value);
380         data->list[selected].top_index = value;
381     }
382 }
383
384 /*
385  * Adjust the top-index as needed to ensure that it and the given item are
386  * visible.
387  */
388 static void
389 fix_top_item(ALL_DATA * data, int cur_item, int selected)
390 {
391     int top_item = data->list[selected].top_index;
392     int cur_row = index2row(data, cur_item, selected);
393     int top_row = index2row(data, top_item, selected);
394
395     if (cur_row < top_row) {
396         top_item = cur_item;
397     } else if ((cur_row - top_row) > data->use_height) {
398         top_item = row2index(data, cur_row + 1 - data->use_height, selected);
399     }
400     if (cur_row < data->use_height) {
401         top_item = row2index(data, 0, selected);
402     }
403     dlg_trace_msg("fix_top_item(cur_item %d, selected %d) ->top_item %d\n",
404                   cur_item, selected, top_item);
405     set_top_item(data, top_item, selected);
406 }
407
408 /*
409  * This is an alternate interface to 'buildlist' which allows the application
410  * to read the list item states back directly without putting them in the
411  * output buffer.
412  */
413 int
414 dlg_buildlist(const char *title,
415               const char *cprompt,
416               int height,
417               int width,
418               int list_height,
419               int item_no,
420               DIALOG_LISTITEM * items,
421               const char *states,
422               int order_mode,
423               int *current_item)
424 {
425     /* *INDENT-OFF* */
426     static DLG_KEYS_BINDING binding[] = {
427         HELPKEY_BINDINGS,
428         ENTERKEY_BINDINGS,
429         DLG_KEYS_DATA( DLGK_FIELD_NEXT, KEY_RIGHT ),
430         DLG_KEYS_DATA( DLGK_FIELD_NEXT, TAB ),
431         DLG_KEYS_DATA( DLGK_FIELD_PREV, KEY_BTAB ),
432         DLG_KEYS_DATA( DLGK_FIELD_PREV, KEY_LEFT ),
433         DLG_KEYS_DATA( DLGK_ITEM_FIRST, KEY_HOME ),
434         DLG_KEYS_DATA( DLGK_ITEM_LAST,  KEY_END ),
435         DLG_KEYS_DATA( DLGK_ITEM_LAST,  KEY_LL ),
436         DLG_KEYS_DATA( DLGK_ITEM_NEXT,  '+' ),
437         DLG_KEYS_DATA( DLGK_ITEM_NEXT,  KEY_DOWN ),
438         DLG_KEYS_DATA( DLGK_ITEM_NEXT,  CHR_NEXT ),
439         DLG_KEYS_DATA( DLGK_ITEM_PREV,  '-' ),
440         DLG_KEYS_DATA( DLGK_ITEM_PREV,  KEY_UP ),
441         DLG_KEYS_DATA( DLGK_ITEM_PREV,  CHR_PREVIOUS ),
442         DLG_KEYS_DATA( DLGK_PAGE_NEXT,  KEY_NPAGE ),
443         DLG_KEYS_DATA( DLGK_PAGE_NEXT,  DLGK_MOUSE(KEY_NPAGE) ),
444         DLG_KEYS_DATA( DLGK_PAGE_NEXT,  DLGK_MOUSE(KEY_NPAGE+KEY_MAX) ),
445         DLG_KEYS_DATA( DLGK_PAGE_PREV,  KEY_PPAGE ),
446         DLG_KEYS_DATA( DLGK_PAGE_PREV,  DLGK_MOUSE(KEY_PPAGE) ),
447         DLG_KEYS_DATA( DLGK_PAGE_PREV,  DLGK_MOUSE(KEY_PPAGE+KEY_MAX) ),
448         DLG_KEYS_DATA( DLGK_GRID_LEFT,  KEY_LEFTCOL ),
449         DLG_KEYS_DATA( DLGK_GRID_RIGHT, KEY_RIGHTCOL ),
450         END_KEYS_BINDING
451     };
452     /* *INDENT-ON* */
453
454 #ifdef KEY_RESIZE
455     int old_height = height;
456     int old_width = width;
457 #endif
458     ALL_DATA all;
459     MY_DATA *data = all.list;
460     int i, j, k, key2, found, x, y, cur_x, cur_y;
461     int key = 0, fkey;
462     bool save_visit = dialog_state.visit_items;
463     int button;
464     int cur_item;
465     int was_mouse;
466     int name_width, text_width, full_width, list_width;
467     int result = DLG_EXIT_UNKNOWN;
468     int num_states;
469     bool first = TRUE;
470     WINDOW *dialog;
471     char *prompt = dlg_strclone(cprompt);
472     const char **buttons = dlg_ok_labels();
473     const char *widget_name = "buildlist";
474
475     (void) order_mode;
476
477     dialog_state.plain_buttons = TRUE;
478
479     /*
480      * Unlike other uses of --visit-items, we have two windows to visit.
481      */
482     if (dialog_state.visit_cols)
483         dialog_state.visit_cols = 2;
484
485     memset(&all, 0, sizeof(all));
486     all.items = items;
487     all.item_no = item_no;
488
489     if (dialog_vars.default_item != 0) {
490         cur_item = dlg_default_listitem(items);
491     } else {
492         if ((cur_item = first_item(&all, 0)) < 0)
493             cur_item = first_item(&all, 1);
494     }
495     button = (dialog_state.visit_items
496               ? (items[cur_item].state ? sRIGHT : sLEFT)
497               : dlg_default_button());
498
499     dlg_does_output();
500     dlg_tab_correct_str(prompt);
501
502 #ifdef KEY_RESIZE
503   retry:
504 #endif
505
506     all.use_height = list_height;
507     all.use_width = (2 * (dlg_calc_list_width(item_no, items)
508                           + 4
509                           + 2 * MARGIN)
510                      + 1);
511     all.use_width = MAX(26, all.use_width);
512     if (all.use_height == 0) {
513         /* calculate height without items (4) */
514         dlg_auto_size(title, prompt, &height, &width, MIN_HIGH, all.use_width);
515         dlg_calc_listh(&height, &all.use_height, item_no);
516     } else {
517         dlg_auto_size(title, prompt,
518                       &height, &width,
519                       MIN_HIGH + all.use_height, all.use_width);
520     }
521     dlg_button_layout(buttons, &width);
522     dlg_print_size(height, width);
523     dlg_ctl_size(height, width);
524
525     /* we need at least two states */
526     if (states == 0 || strlen(states) < 2)
527         states = " *";
528     num_states = (int) strlen(states);
529
530     x = dlg_box_x_ordinate(width);
531     y = dlg_box_y_ordinate(height);
532
533     dialog = dlg_new_window(height, width, y, x);
534     dlg_register_window(dialog, widget_name, binding);
535     dlg_register_buttons(dialog, widget_name, buttons);
536
537     dlg_mouse_setbase(all.base_x = x, all.base_y = y);
538
539     dlg_draw_box2(dialog, 0, 0, height, width, dialog_attr, border_attr, border2_attr);
540     dlg_draw_bottom_box2(dialog, border_attr, border2_attr, dialog_attr);
541     dlg_draw_title(dialog, title);
542
543     (void) wattrset(dialog, dialog_attr);
544     dlg_print_autowrap(dialog, prompt, height, width);
545
546     list_width = (width - 6 * MARGIN - 2) / 2;
547     getyx(dialog, cur_y, cur_x);
548     data[0].box_y = cur_y + 1;
549     data[0].box_x = MARGIN + 1;
550     data[1].box_y = cur_y + 1;
551     data[1].box_x = data[0].box_x + 1 + 2 * MARGIN + list_width;
552
553     /*
554      * After displaying the prompt, we know how much space we really have.
555      * Limit the list to avoid overwriting the ok-button.
556      */
557     if (all.use_height + MIN_HIGH > height - cur_y)
558         all.use_height = height - MIN_HIGH - cur_y;
559     if (all.use_height <= 0)
560         all.use_height = 1;
561
562     for (k = 0; k < 2; ++k) {
563         /* create new window for the list */
564         data[k].win = dlg_sub_window(dialog, all.use_height, list_width,
565                                      y + data[k].box_y + 1,
566                                      x + data[k].box_x + 1);
567
568         /* draw a box around the list items */
569         dlg_draw_box(dialog, data[k].box_y, data[k].box_x,
570                      all.use_height + 2 * MARGIN,
571                      list_width + 2 * MARGIN,
572                      menubox_border_attr, menubox_border2_attr);
573     }
574
575     text_width = 0;
576     name_width = 0;
577     /* Find length of longest item to center buildlist */
578     for (i = 0; i < item_no; i++) {
579         text_width = MAX(text_width, dlg_count_columns(items[i].text));
580         name_width = MAX(name_width, dlg_count_columns(items[i].name));
581     }
582
583     /* If the name+text is wider than the list is allowed, then truncate
584      * one or both of them.  If the name is no wider than 1/4 of the list,
585      * leave it intact.
586      */
587     all.use_width = (list_width - 6 * MARGIN);
588     if (dialog_vars.no_tags && !dialog_vars.no_items) {
589         full_width = MIN(all.use_width, text_width);
590     } else if (dialog_vars.no_items) {
591         full_width = MIN(all.use_width, name_width);
592     } else {
593         if (text_width >= 0
594             && name_width >= 0
595             && all.use_width > 0
596             && text_width + name_width > all.use_width) {
597             int need = (int) (0.25 * all.use_width);
598             if (name_width > need) {
599                 int want = (int) (all.use_width * ((double) name_width) /
600                                   (text_width + name_width));
601                 name_width = (want > need) ? want : need;
602             }
603             text_width = all.use_width - name_width;
604         }
605         full_width = text_width + name_width;
606     }
607
608     all.check_x = (all.use_width - full_width) / 2;
609     all.item_x = ((dialog_vars.no_tags
610                    ? 0
611                    : (dialog_vars.no_items
612                       ? 0
613                       : (name_width + 2)))
614                   + all.check_x);
615
616     /* ensure we are scrolled to show the current choice */
617     j = MIN(all.use_height, item_no);
618     for (i = 0; i < 2; ++i) {
619         int top_item = 0;
620         if ((items[cur_item].state != 0) == i) {
621             top_item = cur_item - j + 1;
622             if (top_item < 0)
623                 top_item = 0;
624             set_top_item(&all, top_item, i);
625         } else {
626             set_top_item(&all, 0, i);
627         }
628     }
629
630     /* register the new window, along with its borders */
631     for (i = 0; i < 2; ++i) {
632         dlg_mouse_mkbigregion(data[i].box_y + 1,
633                               data[i].box_x,
634                               all.use_height,
635                               list_width + 2,
636                               2 * KEY_MAX + (i * (1 + all.use_height)),
637                               1, 1, 1 /* by lines */ );
638     }
639
640     dlg_draw_buttons(dialog, height - 2, 0, buttons, button, FALSE, width);
641
642     while (result == DLG_EXIT_UNKNOWN) {
643         int which = (items[cur_item].state != 0);
644         MY_DATA *moi = data + which;
645         int at_top = index2row(&all, moi->top_index, which);
646         int at_end = index2row(&all, -1, which);
647         int at_bot = skip_rows(&all, at_top, all.use_height, which);
648
649         dlg_trace_msg("\t** state %d:%d top %d (%d:%d:%d) %d\n",
650                       cur_item, item_no - 1,
651                       moi->top_index,
652                       at_top, at_bot, at_end,
653                       which);
654
655         if (first) {
656             print_both(&all, cur_item);
657             dlg_trace_win(dialog);
658             first = FALSE;
659         }
660
661         if (button < 0) {       /* --visit-items */
662             int cur_row = index2row(&all, cur_item, which);
663             cur_y = (data[which].box_y
664                      + cur_row
665                      + 1);
666             if (at_top > 0)
667                 cur_y -= at_top;
668             cur_x = (data[which].box_x
669                      + all.check_x + 1);
670             dlg_trace_msg("\t...visit row %d (%d,%d)\n", cur_row, cur_y, cur_x);
671             wmove(dialog, cur_y, cur_x);
672         }
673
674         key = dlg_mouse_wgetch(dialog, &fkey);
675         if (dlg_result_key(key, fkey, &result))
676             break;
677
678         was_mouse = (fkey && is_DLGK_MOUSE(key));
679         if (was_mouse)
680             key -= M_EVENT;
681
682         if (!was_mouse) {
683             ;
684         } else if (key >= 2 * KEY_MAX) {
685             i = (key - 2 * KEY_MAX) % (1 + all.use_height);
686             j = (key - 2 * KEY_MAX) / (1 + all.use_height);
687             k = row2index(&all, i + at_top, j);
688             dlg_trace_msg("MOUSE column %d, row %d ->item %d\n", j, i, k);
689             if (k >= 0 && j < 2) {
690                 if (j != which) {
691                     /*
692                      * Mouse click was in the other column.
693                      */
694                     moi = data + j;
695                     fix_top_item(&all, k, j);
696                 }
697                 which = j;
698                 at_top = index2row(&all, moi->top_index, which);
699                 at_bot = skip_rows(&all, at_top, all.use_height, which);
700                 cur_item = k;
701                 print_both(&all, cur_item);
702                 key = KEY_TOGGLE;       /* force the selected item to toggle */
703             } else {
704                 beep();
705                 continue;
706             }
707             fkey = FALSE;
708         } else if (key >= KEY_MIN) {
709             if (key > KEY_MAX) {
710                 if (which == 0) {
711                     key = KEY_RIGHTCOL;         /* switch to right-column */
712                     fkey = FALSE;
713                 } else {
714                     key -= KEY_MAX;
715                 }
716             } else {
717                 if (which == 1) {
718                     key = KEY_LEFTCOL;  /* switch to left-column */
719                     fkey = FALSE;
720                 }
721             }
722             key = dlg_lookup_key(dialog, key, &fkey);
723         }
724
725         /*
726          * A space toggles the item status.  Normally we put the cursor on
727          * the next available item in the same column.  But if there are no
728          * more items in the column, move the cursor to the other column.
729          */
730         if (key == KEY_TOGGLE) {
731             int new_choice;
732             int new_state = items[cur_item].state + 1;
733
734             if ((new_choice = next_item(&all, cur_item, which)) == cur_item) {
735                 new_choice = prev_item(&all, cur_item, which);
736             }
737             dlg_trace_msg("cur_item %d, new_choice:%d\n", cur_item, new_choice);
738             if (new_state >= num_states)
739                 new_state = 0;
740
741             items[cur_item].state = new_state;
742             if (cur_item == moi->top_index) {
743                 set_top_item(&all, new_choice, which);
744             }
745
746             if (new_choice >= 0) {
747                 fix_top_item(&all, cur_item, !which);
748                 cur_item = new_choice;
749             }
750             print_both(&all, cur_item);
751             dlg_trace_win(dialog);
752             continue;           /* wait for another key press */
753         }
754
755         /*
756          * Check if key pressed matches first character of any item tag in
757          * list.  If there is more than one match, we will cycle through
758          * each one as the same key is pressed repeatedly.
759          */
760         found = FALSE;
761         if (!fkey) {
762             if (button < 0 || !dialog_state.visit_items) {
763                 for (j = cur_item + 1; j < item_no; j++) {
764                     if (check_hotkey(items, j, which)) {
765                         found = TRUE;
766                         i = j;
767                         break;
768                     }
769                 }
770                 if (!found) {
771                     for (j = 0; j <= cur_item; j++) {
772                         if (check_hotkey(items, j, which)) {
773                             found = TRUE;
774                             i = j;
775                             break;
776                         }
777                     }
778                 }
779                 if (found)
780                     dlg_flush_getc();
781             } else if ((j = dlg_char_to_button(key, buttons)) >= 0) {
782                 button = j;
783                 ungetch('\n');
784                 continue;
785             }
786         }
787
788         /*
789          * A single digit (1-9) positions the selection to that line in the
790          * current screen.
791          */
792         if (!found
793             && (key <= '9')
794             && (key > '0')
795             && (key - '1' < at_bot)) {
796             found = TRUE;
797             i = key - '1';
798         }
799
800         if (!found && fkey) {
801             switch (key) {
802             case DLGK_FIELD_PREV:
803                 if ((button == sRIGHT) && dialog_state.visit_items) {
804                     key = DLGK_GRID_LEFT;
805                     button = sLEFT;
806                 } else {
807                     button = dlg_prev_button(buttons, button);
808                     dlg_draw_buttons(dialog, height - 2, 0, buttons, button,
809                                      FALSE, width);
810                     if (button == sRIGHT) {
811                         key = DLGK_GRID_RIGHT;
812                     } else {
813                         continue;
814                     }
815                 }
816                 break;
817             case DLGK_FIELD_NEXT:
818                 if ((button == sLEFT) && dialog_state.visit_items) {
819                     key = DLGK_GRID_RIGHT;
820                     button = sRIGHT;
821                 } else {
822                     button = dlg_next_button(buttons, button);
823                     dlg_draw_buttons(dialog, height - 2, 0, buttons, button,
824                                      FALSE, width);
825                     if (button == sLEFT) {
826                         key = DLGK_GRID_LEFT;
827                     } else {
828                         continue;
829                     }
830                 }
831                 break;
832             }
833         }
834
835         if (!found && fkey) {
836             i = cur_item;
837             found = TRUE;
838             switch (key) {
839             case DLGK_GRID_LEFT:
840                 i = closest_item(&all, cur_item, 0);
841                 fix_top_item(&all, i, 0);
842                 break;
843             case DLGK_GRID_RIGHT:
844                 i = closest_item(&all, cur_item, 1);
845                 fix_top_item(&all, i, 1);
846                 break;
847             case DLGK_PAGE_PREV:
848                 if (cur_item > moi->top_index) {
849                     i = moi->top_index;
850                 } else if (moi->top_index != 0) {
851                     int temp = at_top;
852                     if ((temp -= all.use_height) < 0)
853                         temp = 0;
854                     i = row2index(&all, temp, which);
855                 }
856                 break;
857             case DLGK_PAGE_NEXT:
858                 if ((at_end - at_bot) < all.use_height) {
859                     i = next_item(&all,
860                                   row2index(&all, at_end, which),
861                                   which);
862                 } else {
863                     i = next_item(&all,
864                                   row2index(&all, at_bot, which),
865                                   which);
866                     at_top = at_bot;
867                     set_top_item(&all,
868                                  next_item(&all,
869                                            row2index(&all, at_top, which),
870                                            which),
871                                  which);
872                     at_bot = skip_rows(&all, at_top, all.use_height, which);
873                     at_bot = MIN(at_bot, at_end);
874                 }
875                 break;
876             case DLGK_ITEM_FIRST:
877                 i = first_item(&all, which);
878                 break;
879             case DLGK_ITEM_LAST:
880                 i = last_item(&all, which);
881                 break;
882             case DLGK_ITEM_PREV:
883                 i = prev_item(&all, cur_item, which);
884                 if (stop_prev(&all, cur_item, which))
885                     continue;
886                 break;
887             case DLGK_ITEM_NEXT:
888                 i = next_item(&all, cur_item, which);
889                 break;
890             default:
891                 found = FALSE;
892                 break;
893             }
894         }
895
896         if (found) {
897             if (i != cur_item) {
898                 int now_at = index2row(&all, i, which);
899                 int oops = item_no;
900                 int old_item;
901
902                 dlg_trace_msg("<--CHOICE %d\n", i);
903                 dlg_trace_msg("<--topITM %d\n", moi->top_index);
904                 dlg_trace_msg("<--now_at %d\n", now_at);
905                 dlg_trace_msg("<--at_top %d\n", at_top);
906                 dlg_trace_msg("<--at_bot %d\n", at_bot);
907
908                 if (now_at >= at_bot) {
909                     while (now_at >= at_bot) {
910                         if ((at_bot - at_top) >= all.use_height) {
911                             set_top_item(&all,
912                                          next_item(&all, moi->top_index, which),
913                                          which);
914                         }
915                         at_top = index2row(&all, moi->top_index, which);
916                         at_bot = skip_rows(&all, at_top, all.use_height, which);
917
918                         dlg_trace_msg("...at_bot %d (now %d vs %d)\n",
919                                       at_bot, now_at, at_end);
920                         dlg_trace_msg("...topITM %d\n", moi->top_index);
921                         dlg_trace_msg("...at_top %d (diff %d)\n", at_top,
922                                       at_bot - at_top);
923
924                         if (at_bot >= at_end) {
925                             /*
926                              * If we bumped into the end, move the top-item
927                              * down by one line so that we can display the
928                              * last item in the list.
929                              */
930                             if ((at_bot - at_top) > all.use_height) {
931                                 set_top_item(&all,
932                                              next_item(&all, moi->top_index, which),
933                                              which);
934                             } else if (at_top > 0 &&
935                                        (at_bot - at_top) >= all.use_height) {
936                                 set_top_item(&all,
937                                              next_item(&all, moi->top_index, which),
938                                              which);
939                             }
940                             break;
941                         }
942                         if (--oops < 0) {
943                             dlg_trace_msg("OOPS-forward\n");
944                             break;
945                         }
946                     }
947                 } else if (now_at < at_top) {
948                     while (now_at < at_top) {
949                         old_item = moi->top_index;
950                         set_top_item(&all,
951                                      prev_item(&all, moi->top_index, which),
952                                      which);
953                         at_top = index2row(&all, moi->top_index, which);
954
955                         dlg_trace_msg("...at_top %d (now %d)\n", at_top, now_at);
956                         dlg_trace_msg("...topITM %d\n", moi->top_index);
957
958                         if (moi->top_index >= old_item)
959                             break;
960                         if (at_top <= now_at)
961                             break;
962                         if (--oops < 0) {
963                             dlg_trace_msg("OOPS-backward\n");
964                             break;
965                         }
966                     }
967                 }
968                 dlg_trace_msg("-->now_at %d\n", now_at);
969                 cur_item = i;
970                 print_both(&all, cur_item);
971             }
972             dlg_trace_win(dialog);
973             continue;           /* wait for another key press */
974         }
975
976         if (fkey) {
977             switch (key) {
978             case DLGK_ENTER:
979                 result = dlg_enter_buttoncode(button);
980                 break;
981 #ifdef KEY_RESIZE
982             case KEY_RESIZE:
983                 /* reset data */
984                 height = old_height;
985                 width = old_width;
986                 /* repaint */
987                 dlg_clear();
988                 dlg_del_window(dialog);
989                 refresh();
990                 dlg_mouse_free_regions();
991                 goto retry;
992 #endif
993             default:
994                 if (was_mouse) {
995                     if ((key2 = dlg_ok_buttoncode(key)) >= 0) {
996                         result = key2;
997                         break;
998                     }
999                     beep();
1000                 }
1001             }
1002         } else {
1003             beep();
1004         }
1005     }
1006
1007     dialog_state.visit_cols = save_visit;
1008     dlg_del_window(dialog);
1009     dlg_mouse_free_regions();
1010     free(prompt);
1011     *current_item = cur_item;
1012     return result;
1013 }
1014
1015 /*
1016  * Display a dialog box with a list of options that can be turned on or off
1017  */
1018 int
1019 dialog_buildlist(const char *title,
1020                  const char *cprompt,
1021                  int height,
1022                  int width,
1023                  int list_height,
1024                  int item_no,
1025                  char **items,
1026                  int order_mode)
1027 {
1028     int result;
1029     int i, j;
1030     DIALOG_LISTITEM *listitems;
1031     bool separate_output = dialog_vars.separate_output;
1032     bool show_status = FALSE;
1033     int current = 0;
1034     char *help_result;
1035
1036     listitems = dlg_calloc(DIALOG_LISTITEM, (size_t) item_no + 1);
1037     assert_ptr(listitems, "dialog_buildlist");
1038
1039     for (i = j = 0; i < item_no; ++i) {
1040         listitems[i].name = items[j++];
1041         listitems[i].text = (dialog_vars.no_items
1042                              ? dlg_strempty()
1043                              : items[j++]);
1044         listitems[i].state = !dlg_strcmp(items[j++], "on");
1045         listitems[i].help = ((dialog_vars.item_help)
1046                              ? items[j++]
1047                              : dlg_strempty());
1048     }
1049     dlg_align_columns(&listitems[0].text, (int) sizeof(DIALOG_LISTITEM), item_no);
1050
1051     result = dlg_buildlist(title,
1052                            cprompt,
1053                            height,
1054                            width,
1055                            list_height,
1056                            item_no,
1057                            listitems,
1058                            NULL,
1059                            order_mode,
1060                            &current);
1061
1062     switch (result) {
1063     case DLG_EXIT_OK:           /* FALLTHRU */
1064     case DLG_EXIT_EXTRA:
1065         show_status = TRUE;
1066         break;
1067     case DLG_EXIT_HELP:
1068         dlg_add_help_listitem(&result, &help_result, &listitems[current]);
1069         if ((show_status = dialog_vars.help_status)) {
1070             if (separate_output) {
1071                 dlg_add_string(help_result);
1072                 dlg_add_separator();
1073             } else {
1074                 dlg_add_quoted(help_result);
1075             }
1076         } else {
1077             dlg_add_string(help_result);
1078         }
1079         break;
1080     }
1081
1082     if (show_status) {
1083         for (i = 0; i < item_no; i++) {
1084             if (listitems[i].state) {
1085                 if (separate_output) {
1086                     dlg_add_string(listitems[i].name);
1087                     dlg_add_separator();
1088                 } else {
1089                     if (dlg_need_separator())
1090                         dlg_add_separator();
1091                     dlg_add_quoted(listitems[i].name);
1092                 }
1093             }
1094         }
1095         dlg_add_last_key(-1);
1096     }
1097
1098     dlg_free_columns(&listitems[0].text, (int) sizeof(DIALOG_LISTITEM), item_no);
1099     free(listitems);
1100     return result;
1101 }