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