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
index2row(ALL_DATA * all,int choice,int selected)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
row2index(ALL_DATA * all,int row,int selected)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
print_item(ALL_DATA * all,WINDOW * win,DIALOG_LISTITEM * item,int row,int selected)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
print_1_list(ALL_DATA * all,int choice,int selected)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
prev_item(ALL_DATA * all,int choice,int selected)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
stop_prev(ALL_DATA * all,int choice,int selected)227 stop_prev(ALL_DATA * all, int choice, int selected)
228 {
229 return (prev_item(all, choice, selected) == choice);
230 }
231
232 static bool
check_hotkey(DIALOG_LISTITEM * items,int choice,int selected)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
next_item(ALL_DATA * all,int choice,int selected)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
first_item(ALL_DATA * all,int selected)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
last_item(ALL_DATA * all,int selected)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
skip_rows(ALL_DATA * all,int row,int skip,int selected)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
closest_item(ALL_DATA * all,int choice,int selected)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
print_both(ALL_DATA * all,int choice)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
set_top_item(ALL_DATA * all,int choice,int selected)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
fix_top_item(ALL_DATA * all,int cur_item,int selected)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
append_right_side(ALL_DATA * all,int choice)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
amend_right_side(ALL_DATA * all,int choice)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
fill_one_side(ALL_DATA * all,int selected)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
fill_both_sides(ALL_DATA * all)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
dlg_buildlist(const char * title,const char * cprompt,int height,int width,int list_height,int item_no,DIALOG_LISTITEM * items,const char * states,int order_mode,int * current_item)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
dialog_buildlist(const char * title,const char * cprompt,int height,int width,int list_height,int item_no,char ** items,int order_mode)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 ¤t);
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