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