xref: /dragonfly/contrib/dialog/treeview.c (revision b2dabe2e739bd72461a68ac543307c2dedfb048c)
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                                 &current);
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