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 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 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 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 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 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