xref: /freebsd/contrib/dialog/buildlist.c (revision a98ff317388a00b992f1bf8404dee596f9383f5e)
1 /*
2  *  $Id: buildlist.c,v 1.57 2013/03/17 13:46:30 tom Exp $
3  *
4  *  buildlist.c -- implements the buildlist dialog
5  *
6  *  Copyright 2012,2013	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 	dlg_add_last_key(-1);
1106     }
1107 
1108     dlg_free_columns(&listitems[0].text, (int) sizeof(DIALOG_LISTITEM), item_no);
1109     free(listitems);
1110     return result;
1111 }
1112