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