1 /*
2 * $Id: treeview.c,v 1.46 2022/04/05 00:15:15 tom Exp $
3 *
4 * treeview.c -- implements the treeview dialog
5 *
6 * Copyright 2012-2021,2022 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 #define INDENT 3
28 #define MIN_HIGH (1 + (5 * MARGIN))
29
30 typedef struct {
31 /* the outer-window */
32 WINDOW *dialog;
33 bool is_check;
34 int box_y;
35 int box_x;
36 int check_x;
37 int item_x;
38 int use_height;
39 int use_width;
40 /* the inner-window */
41 WINDOW *list;
42 DIALOG_LISTITEM *items;
43 int item_no;
44 int *depths;
45 const char *states;
46 } ALL_DATA;
47
48 /*
49 * Print list item. The 'selected' parameter is true if 'choice' is the
50 * current item. That one is colored differently from the other items.
51 */
52 static void
print_item(ALL_DATA * data,DIALOG_LISTITEM * item,const char * states,int depths,int choice,int selected)53 print_item(ALL_DATA * data,
54 DIALOG_LISTITEM * item,
55 const char *states,
56 int depths,
57 int choice,
58 int selected)
59 {
60 WINDOW *win = data->list;
61 chtype save = dlg_get_attrs(win);
62 int i;
63 bool first = TRUE;
64 int climit = (getmaxx(win) - data->check_x + 1);
65 const char *show = (dialog_vars.no_items
66 ? item->name
67 : item->text);
68
69 /* Clear 'residue' of last item */
70 dlg_attrset(win, menubox_attr);
71 (void) wmove(win, choice, 0);
72 for (i = 0; i < data->use_width; i++)
73 (void) waddch(win, ' ');
74
75 (void) wmove(win, choice, data->check_x);
76 dlg_attrset(win, selected ? check_selected_attr : check_attr);
77 (void) wprintw(win,
78 data->is_check ? "[%c]" : "(%c)",
79 states[item->state]);
80 dlg_attrset(win, menubox_attr);
81
82 dlg_attrset(win, selected ? item_selected_attr : item_attr);
83 for (i = 0; i < depths; ++i) {
84 int j;
85 (void) wmove(win, choice, data->item_x + INDENT * i);
86 (void) waddch(win, ACS_VLINE);
87 for (j = INDENT - 1; j > 0; --j)
88 (void) waddch(win, ' ');
89 }
90 (void) wmove(win, choice, data->item_x + INDENT * depths);
91
92 dlg_print_listitem(win, show, climit, first, selected);
93
94 if (selected) {
95 dlg_item_help(item->help);
96 }
97 dlg_attrset(win, save);
98 }
99
100 static void
print_list(ALL_DATA * data,int choice,int scrollamt,int max_choice,int max_items)101 print_list(ALL_DATA * data,
102 int choice,
103 int scrollamt,
104 int max_choice,
105 int max_items)
106 {
107 int i;
108 int cur_y, cur_x;
109
110 getyx(data->dialog, cur_y, cur_x);
111
112 for (i = 0; i < max_choice; i++) {
113 int ii = i + scrollamt;
114 if (ii < max_items)
115 print_item(data,
116 &data->items[ii],
117 data->states,
118 data->depths[ii],
119 i, i == choice);
120 }
121 (void) wnoutrefresh(data->list);
122
123 dlg_draw_scrollbar(data->dialog,
124 (long) (scrollamt),
125 (long) (scrollamt),
126 (long) (scrollamt + max_choice),
127 (long) (data->item_no),
128 data->box_x + data->check_x,
129 data->box_x + data->use_width,
130 data->box_y,
131 data->box_y + data->use_height + 1,
132 menubox_border2_attr,
133 menubox_border_attr);
134
135 (void) wmove(data->dialog, cur_y, cur_x);
136 }
137
138 static bool
check_hotkey(DIALOG_LISTITEM * items,int choice)139 check_hotkey(DIALOG_LISTITEM * items, int choice)
140 {
141 bool result = FALSE;
142
143 if (dlg_match_char(dlg_last_getc(),
144 (dialog_vars.no_tags
145 ? items[choice].text
146 : items[choice].name))) {
147 result = TRUE;
148 }
149 return result;
150 }
151
152 /*
153 * This is an alternate interface to 'treeview' which allows the application
154 * to read the list item states back directly without putting them in the
155 * output buffer.
156 */
157 int
dlg_treeview(const char * title,const char * cprompt,int height,int width,int list_height,int item_no,DIALOG_LISTITEM * items,const char * states,int * depths,int flag,int * current_item)158 dlg_treeview(const char *title,
159 const char *cprompt,
160 int height,
161 int width,
162 int list_height,
163 int item_no,
164 DIALOG_LISTITEM * items,
165 const char *states,
166 int *depths,
167 int flag,
168 int *current_item)
169 {
170 /* *INDENT-OFF* */
171 static DLG_KEYS_BINDING binding[] = {
172 HELPKEY_BINDINGS,
173 ENTERKEY_BINDINGS,
174 DLG_KEYS_DATA( DLGK_FIELD_NEXT, KEY_RIGHT ),
175 DLG_KEYS_DATA( DLGK_FIELD_NEXT, TAB ),
176 DLG_KEYS_DATA( DLGK_FIELD_PREV, KEY_BTAB ),
177 DLG_KEYS_DATA( DLGK_FIELD_PREV, KEY_LEFT ),
178 DLG_KEYS_DATA( DLGK_ITEM_FIRST, KEY_HOME ),
179 DLG_KEYS_DATA( DLGK_ITEM_LAST, KEY_END ),
180 DLG_KEYS_DATA( DLGK_ITEM_LAST, KEY_LL ),
181 DLG_KEYS_DATA( DLGK_ITEM_NEXT, '+' ),
182 DLG_KEYS_DATA( DLGK_ITEM_NEXT, KEY_DOWN ),
183 DLG_KEYS_DATA( DLGK_ITEM_NEXT, CHR_NEXT ),
184 DLG_KEYS_DATA( DLGK_ITEM_PREV, '-' ),
185 DLG_KEYS_DATA( DLGK_ITEM_PREV, KEY_UP ),
186 DLG_KEYS_DATA( DLGK_ITEM_PREV, CHR_PREVIOUS ),
187 DLG_KEYS_DATA( DLGK_PAGE_NEXT, KEY_NPAGE ),
188 DLG_KEYS_DATA( DLGK_PAGE_NEXT, DLGK_MOUSE(KEY_NPAGE) ),
189 DLG_KEYS_DATA( DLGK_PAGE_PREV, KEY_PPAGE ),
190 DLG_KEYS_DATA( DLGK_PAGE_PREV, DLGK_MOUSE(KEY_PPAGE) ),
191 TOGGLEKEY_BINDINGS,
192 END_KEYS_BINDING
193 };
194 /* *INDENT-ON* */
195
196 #ifdef KEY_RESIZE
197 int old_height = height;
198 int old_width = width;
199 #endif
200 ALL_DATA all;
201 int i, j, key2, found, x, y, cur_y, box_x, box_y;
202 int key, fkey;
203 int button = dialog_state.visit_items ? -1 : dlg_default_button();
204 int choice = dlg_default_listitem(items);
205 int scrollamt = 0;
206 int max_choice;
207 int use_height;
208 int use_width, name_width, text_width, tree_width;
209 int result = DLG_EXIT_UNKNOWN;
210 int num_states;
211 WINDOW *dialog, *list;
212 char *prompt = dlg_strclone(cprompt);
213 const char **buttons = dlg_ok_labels();
214 const char *widget_name;
215
216 /* we need at least two states */
217 if (states == 0 || strlen(states) < 2)
218 states = " *";
219 num_states = (int) strlen(states);
220
221 dialog_state.plain_buttons = TRUE;
222
223 memset(&all, 0, sizeof(all));
224 all.items = items;
225 all.item_no = item_no;
226 all.states = states;
227 all.depths = depths;
228
229 dlg_does_output();
230 dlg_tab_correct_str(prompt);
231
232 /*
233 * If this is a radiobutton list, ensure that no more than one item is
234 * selected initially. Allow none to be selected, since some users may
235 * wish to provide this flavor.
236 */
237 if (flag == FLAG_RADIO) {
238 bool first = TRUE;
239
240 for (i = 0; i < item_no; i++) {
241 if (items[i].state) {
242 if (first) {
243 first = FALSE;
244 } else {
245 items[i].state = 0;
246 }
247 }
248 }
249 } else {
250 all.is_check = TRUE;
251 }
252 widget_name = "treeview";
253 #ifdef KEY_RESIZE
254 retry:
255 #endif
256
257 use_height = list_height;
258 use_width = dlg_calc_list_width(item_no, items) + 10;
259 use_width = MAX(26, use_width);
260 if (use_height == 0) {
261 /* calculate height without items (4) */
262 dlg_auto_size(title, prompt, &height, &width, MIN_HIGH, use_width);
263 dlg_calc_listh(&height, &use_height, item_no);
264 } else {
265 dlg_auto_size(title, prompt, &height, &width, MIN_HIGH + use_height, use_width);
266 }
267 dlg_button_layout(buttons, &width);
268 dlg_print_size(height, width);
269 dlg_ctl_size(height, width);
270
271 x = dlg_box_x_ordinate(width);
272 y = dlg_box_y_ordinate(height);
273
274 dialog = dlg_new_window(height, width, y, x);
275 dlg_register_window(dialog, widget_name, binding);
276 dlg_register_buttons(dialog, widget_name, buttons);
277
278 dlg_mouse_setbase(x, y);
279
280 dlg_draw_box2(dialog, 0, 0, height, width, dialog_attr, border_attr, border2_attr);
281 dlg_draw_bottom_box2(dialog, border_attr, border2_attr, dialog_attr);
282 dlg_draw_title(dialog, title);
283
284 dlg_attrset(dialog, dialog_attr);
285 dlg_print_autowrap(dialog, prompt, height, width);
286
287 all.use_width = width - 4;
288 cur_y = getcury(dialog);
289 box_y = cur_y + 1;
290 box_x = (width - all.use_width) / 2 - 1;
291
292 /*
293 * After displaying the prompt, we know how much space we really have.
294 * Limit the list to avoid overwriting the ok-button.
295 */
296 use_height = height - MIN_HIGH - cur_y;
297 if (use_height <= 0)
298 use_height = 1;
299
300 max_choice = MIN(use_height, item_no);
301
302 /* create new window for the list */
303 list = dlg_sub_window(dialog, use_height, all.use_width,
304 y + box_y + 1, x + box_x + 1);
305
306 /* draw a box around the list items */
307 dlg_draw_box(dialog, box_y, box_x,
308 use_height + 2 * MARGIN,
309 all.use_width + 2 * MARGIN,
310 menubox_border_attr, menubox_border2_attr);
311
312 text_width = 0;
313 name_width = 0;
314 tree_width = 0;
315 /* Find length of longest item to center treeview */
316 for (i = 0; i < item_no; i++) {
317 tree_width = MAX(tree_width, INDENT * depths[i]);
318 text_width = MAX(text_width, dlg_count_columns(items[i].text));
319 name_width = MAX(name_width, dlg_count_columns(items[i].name));
320 }
321 if (dialog_vars.no_tags && !dialog_vars.no_items) {
322 tree_width += text_width;
323 } else if (dialog_vars.no_items) {
324 tree_width += name_width;
325 } else {
326 tree_width += (text_width + name_width);
327 }
328
329 use_width = (all.use_width - 4);
330 tree_width = MIN(tree_width, all.use_width);
331
332 all.check_x = (use_width - tree_width) / 2;
333 all.item_x = ((dialog_vars.no_tags
334 ? 0
335 : (dialog_vars.no_items
336 ? 0
337 : (2 + name_width)))
338 + all.check_x + 4);
339
340 /* ensure we are scrolled to show the current choice */
341 if (choice >= (max_choice + scrollamt)) {
342 scrollamt = choice - max_choice + 1;
343 choice = max_choice - 1;
344 }
345
346 /* register the new window, along with its borders */
347 dlg_mouse_mkbigregion(box_y + 1, box_x,
348 use_height, all.use_width + 2,
349 KEY_MAX, 1, 1, 1 /* by lines */ );
350
351 all.dialog = dialog;
352 all.box_x = box_x;
353 all.box_y = box_y;
354 all.use_height = use_height;
355 all.list = list;
356 #define PrintList() \
357 print_list(&all, choice, scrollamt, max_choice, item_no)
358 PrintList();
359
360 dlg_draw_buttons(dialog, height - 2, 0, buttons, button, FALSE, width);
361
362 dlg_trace_win(dialog);
363
364 while (result == DLG_EXIT_UNKNOWN) {
365 int was_mouse;
366
367 if (button < 0) /* --visit-items */
368 wmove(dialog, box_y + choice + 1, box_x + all.check_x + 2);
369
370 key = dlg_mouse_wgetch(dialog, &fkey);
371 if (dlg_result_key(key, fkey, &result)) {
372 if (!dlg_button_key(result, &button, &key, &fkey))
373 break;
374 }
375
376 was_mouse = (fkey && is_DLGK_MOUSE(key));
377 if (was_mouse)
378 key -= M_EVENT;
379
380 if (was_mouse && (key >= KEY_MAX)) {
381 i = (key - KEY_MAX);
382 if (i < max_choice) {
383 choice = (key - KEY_MAX);
384 PrintList();
385
386 key = DLGK_TOGGLE; /* force the selected item to toggle */
387 } else {
388 beep();
389 continue;
390 }
391 fkey = FALSE;
392 } else if (was_mouse && key >= KEY_MIN) {
393 key = dlg_lookup_key(dialog, key, &fkey);
394 }
395
396 /*
397 * A space toggles the item status.
398 */
399 if (key == DLGK_TOGGLE) {
400 int current = scrollamt + choice;
401 int next = items[current].state + 1;
402
403 if (next >= num_states)
404 next = 0;
405
406 if (flag == FLAG_CHECK) { /* checklist? */
407 items[current].state = next;
408 } else {
409 for (i = 0; i < item_no; i++) {
410 if (i != current) {
411 items[i].state = 0;
412 }
413 }
414 if (items[current].state) {
415 items[current].state = next ? next : 1;
416 } else {
417 items[current].state = 1;
418 }
419 }
420 PrintList();
421 continue; /* wait for another key press */
422 }
423
424 /*
425 * Check if key pressed matches first character of any item tag in
426 * list. If there is more than one match, we will cycle through
427 * each one as the same key is pressed repeatedly.
428 */
429 found = FALSE;
430 if (!fkey) {
431 if (button < 0 || !dialog_state.visit_items) {
432 for (j = scrollamt + choice + 1; j < item_no; j++) {
433 if (check_hotkey(items, j)) {
434 found = TRUE;
435 i = j - scrollamt;
436 break;
437 }
438 }
439 if (!found) {
440 for (j = 0; j <= scrollamt + choice; j++) {
441 if (check_hotkey(items, j)) {
442 found = TRUE;
443 i = j - scrollamt;
444 break;
445 }
446 }
447 }
448 if (found)
449 dlg_flush_getc();
450 } else if ((j = dlg_char_to_button(key, buttons)) >= 0) {
451 button = j;
452 ungetch('\n');
453 continue;
454 }
455 }
456
457 /*
458 * A single digit (1-9) positions the selection to that line in the
459 * current screen.
460 */
461 if (!found
462 && (key <= '9')
463 && (key > '0')
464 && (key - '1' < max_choice)) {
465 found = TRUE;
466 i = key - '1';
467 }
468
469 if (!found) {
470 if (fkey) {
471 found = TRUE;
472 switch (key) {
473 case DLGK_ITEM_FIRST:
474 i = -scrollamt;
475 break;
476 case DLGK_ITEM_LAST:
477 i = item_no - 1 - scrollamt;
478 break;
479 case DLGK_PAGE_PREV:
480 if (choice)
481 i = 0;
482 else if (scrollamt != 0)
483 i = -MIN(scrollamt, max_choice);
484 else
485 continue;
486 break;
487 case DLGK_PAGE_NEXT:
488 i = MIN(choice + max_choice, item_no - scrollamt - 1);
489 break;
490 case DLGK_ITEM_PREV:
491 i = choice - 1;
492 if (choice == 0 && scrollamt == 0)
493 continue;
494 break;
495 case DLGK_ITEM_NEXT:
496 i = choice + 1;
497 if (scrollamt + choice >= item_no - 1)
498 continue;
499 break;
500 default:
501 found = FALSE;
502 break;
503 }
504 }
505 }
506
507 if (found) {
508 if (i != choice) {
509 if (i < 0 || i >= max_choice) {
510 if (i < 0) {
511 scrollamt += i;
512 choice = 0;
513 } else {
514 choice = max_choice - 1;
515 scrollamt += (i - max_choice + 1);
516 }
517 PrintList();
518 } else {
519 choice = i;
520 PrintList();
521 }
522 }
523 continue; /* wait for another key press */
524 }
525
526 if (fkey) {
527 switch (key) {
528 case DLGK_ENTER:
529 result = dlg_enter_buttoncode(button);
530 break;
531 case DLGK_LEAVE:
532 result = dlg_ok_buttoncode(button);
533 break;
534 case DLGK_FIELD_PREV:
535 button = dlg_prev_button(buttons, button);
536 dlg_draw_buttons(dialog, height - 2, 0, buttons, button,
537 FALSE, width);
538 break;
539 case DLGK_FIELD_NEXT:
540 button = dlg_next_button(buttons, button);
541 dlg_draw_buttons(dialog, height - 2, 0, buttons, button,
542 FALSE, width);
543 break;
544 #ifdef KEY_RESIZE
545 case KEY_RESIZE:
546 dlg_will_resize(dialog);
547 /* reset data */
548 height = old_height;
549 width = old_width;
550 /* repaint */
551 _dlg_resize_cleanup(dialog);
552 /* keep position */
553 choice += scrollamt;
554 scrollamt = 0;
555 goto retry;
556 #endif
557 default:
558 if (was_mouse) {
559 if ((key2 = dlg_ok_buttoncode(key)) >= 0) {
560 result = key2;
561 break;
562 }
563 beep();
564 }
565 }
566 } else if (key > 0) {
567 beep();
568 }
569 }
570
571 dlg_del_window(dialog);
572 dlg_mouse_free_regions();
573 free(prompt);
574 *current_item = (scrollamt + choice);
575 return result;
576 }
577
578 /*
579 * Display a set of items as a tree.
580 */
581 int
dialog_treeview(const char * title,const char * cprompt,int height,int width,int list_height,int item_no,char ** items,int flag)582 dialog_treeview(const char *title,
583 const char *cprompt,
584 int height,
585 int width,
586 int list_height,
587 int item_no,
588 char **items,
589 int flag)
590 {
591 int result;
592 int i, j;
593 DIALOG_LISTITEM *listitems;
594 int *depths;
595 bool show_status = FALSE;
596 int current = 0;
597 char *help_result;
598
599 DLG_TRACE(("# treeview args:\n"));
600 DLG_TRACE2S("title", title);
601 DLG_TRACE2S("message", cprompt);
602 DLG_TRACE2N("height", height);
603 DLG_TRACE2N("width", width);
604 DLG_TRACE2N("lheight", list_height);
605 DLG_TRACE2N("llength", item_no);
606 /* FIXME dump the items[][] too */
607 DLG_TRACE2N("flag", flag);
608
609 listitems = dlg_calloc(DIALOG_LISTITEM, (size_t) item_no + 1);
610 assert_ptr(listitems, "dialog_treeview");
611
612 depths = dlg_calloc(int, (size_t) item_no + 1);
613 assert_ptr(depths, "dialog_treeview");
614
615 for (i = j = 0; i < item_no; ++i) {
616 listitems[i].name = items[j++];
617 listitems[i].text = (dialog_vars.no_items
618 ? dlg_strempty()
619 : items[j++]);
620 listitems[i].state = !dlg_strcmp(items[j++], "on");
621 depths[i] = atoi(items[j++]);
622 listitems[i].help = ((dialog_vars.item_help)
623 ? items[j++]
624 : dlg_strempty());
625 }
626 dlg_align_columns(&listitems[0].text, (int) sizeof(DIALOG_LISTITEM), item_no);
627
628 result = dlg_treeview(title,
629 cprompt,
630 height,
631 width,
632 list_height,
633 item_no,
634 listitems,
635 NULL,
636 depths,
637 flag,
638 ¤t);
639
640 switch (result) {
641 case DLG_EXIT_OK: /* FALLTHRU */
642 case DLG_EXIT_EXTRA:
643 show_status = TRUE;
644 break;
645 case DLG_EXIT_HELP:
646 dlg_add_help_listitem(&result, &help_result, &listitems[current]);
647 if ((show_status = dialog_vars.help_status)) {
648 if (dialog_vars.separate_output) {
649 dlg_add_string(help_result);
650 dlg_add_separator();
651 } else {
652 dlg_add_quoted(help_result);
653 }
654 } else {
655 dlg_add_string(help_result);
656 }
657 break;
658 }
659
660 if (show_status) {
661 for (i = 0; i < item_no; i++) {
662 if (listitems[i].state) {
663 if (dlg_need_separator())
664 dlg_add_separator();
665 if (dialog_vars.separate_output) {
666 dlg_add_string(listitems[i].name);
667 } else {
668 if (flag == FLAG_CHECK)
669 dlg_add_quoted(listitems[i].name);
670 else
671 dlg_add_string(listitems[i].name);
672 }
673 }
674 }
675 AddLastKey();
676 }
677
678 dlg_free_columns(&listitems[0].text, (int) sizeof(DIALOG_LISTITEM), item_no);
679 free(depths);
680 free(listitems);
681 return result;
682 }
683