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