xref: /dragonfly/contrib/dialog/buildlist.c (revision 0ca59c34)
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 }
1102