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
39typedef 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
59typedef 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 */
75static int
76index2row(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 */
101static int
102row2index(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 */
124static void
125print_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 */
168static void
169print_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 */
210static int
211prev_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 */
226static bool
227stop_prev(ALL_DATA * all, int choice, int selected)
228{
229    return (prev_item(all, choice, selected) == choice);
230}
231
232static bool
233check_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 */
252static int
253next_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 */
269static int
270first_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 */
292static int
293last_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
309static int
310skip_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 */
336static int
337closest_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
371static void
372print_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
407static void
408set_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 */
422static void
423fix_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
442static void
443append_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
455static void
456amend_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
471static void
472fill_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
491static void
492fill_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 */
506int
507dlg_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 */
1175int
1176dialog_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