1 /* 2 * $Id: buildlist.c,v 1.94 2020/11/23 00:37:17 tom Exp $ 3 * 4 * buildlist.c -- implements the buildlist dialog 5 * 6 * Copyright 2012-2019,2020 Thomas E. Dickey 7 * 8 * This program is free software; you can redistribute it and/or modify 9 * it under the terms of the GNU Lesser General Public License, version 2.1 10 * as published by the Free Software Foundation. 11 * 12 * This program is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 * Lesser General Public License for more details. 16 * 17 * You should have received a copy of the GNU Lesser General Public 18 * License along with this program; if not, write to 19 * Free Software Foundation, Inc. 20 * 51 Franklin St., Fifth Floor 21 * Boston, MA 02110, USA. 22 */ 23 24 #include <dlg_internals.h> 25 #include <dlg_keys.h> 26 27 /* 28 * Visually like menubox, but two columns. 29 */ 30 31 #define sLEFT (-2) 32 #define sRIGHT (-1) 33 34 #define KEY_LEFTCOL '^' 35 #define KEY_RIGHTCOL '$' 36 37 #define MIN_HIGH (1 + (5 * MARGIN)) 38 39 typedef struct { 40 WINDOW *win; 41 int box_y; 42 int box_x; 43 int top_index; 44 int cur_index; 45 DIALOG_LISTITEM **ip; /* pointers to items in this list */ 46 } MY_DATA; 47 48 #if 0 49 #define TRACE(p) dlg_trace_msg p 50 #else 51 #define TRACE(p) /* nothing */ 52 #endif 53 54 #define okIndex(all,index) ((index) >= 0 && (index) < (all)->item_no) 55 56 #define myItem(p,n) ((p)->ip)[n] 57 #define mySide(n) ((n)?"right":"left") 58 59 typedef struct { 60 DIALOG_LISTITEM *items; /* all items in the widget */ 61 int base_y; /* base for mouse coordinates */ 62 int base_x; 63 int use_height; /* actual size of column box */ 64 int use_width; 65 int item_no; 66 int check_x; 67 int item_x; 68 MY_DATA list[2]; 69 } ALL_DATA; 70 71 /* 72 * Translate a choice from items[] to a row-number in an unbounded column, 73 * starting at zero. 74 */ 75 static int 76 index2row(ALL_DATA * all, int choice, int selected) 77 { 78 MY_DATA *data = all->list + selected; 79 int result = -1; 80 81 if (okIndex(all, choice)) { 82 int row; 83 84 for (row = 0; row < all->item_no; ++row) { 85 TRACE(("!... choice %d: %p vs row %d: %p\n", 86 choice, all->items + choice, 87 row, myItem(data, row))); 88 if (myItem(data, row) == all->items + choice) { 89 result = row; 90 break; 91 } 92 } 93 } 94 TRACE(("! index2row(choice %d, %s) = %d\n", choice, mySide(selected), result)); 95 return result; 96 } 97 98 /* 99 * Convert a row-number back to an item number, i.e., index into items[]. 100 */ 101 static int 102 row2index(ALL_DATA * all, int row, int selected) 103 { 104 MY_DATA *data = all->list + selected; 105 int result = -1; 106 int n; 107 for (n = 0; n < all->item_no; ++n) { 108 TRACE(("!... row %d: %p vs choice %d: %p\n", 109 row, myItem(data, row), 110 n, all->items + n)); 111 if (myItem(data, row) == all->items + n) { 112 result = n; 113 break; 114 } 115 } 116 TRACE(("! row2index(row %d, %s) = %d\n", row, mySide(selected), result)); 117 return result; 118 } 119 120 /* 121 * Print list item. The 'selected' parameter is true if 'choice' is the 122 * current item. That one is colored differently from the other items. 123 */ 124 static void 125 print_item(ALL_DATA * all, 126 WINDOW *win, 127 DIALOG_LISTITEM * item, 128 int row, 129 int selected) 130 { 131 chtype save = dlg_get_attrs(win); 132 int i; 133 bool both = (!dialog_vars.no_tags && !dialog_vars.no_items); 134 bool first = TRUE; 135 int climit = (all->item_x - all->check_x - 1); 136 const char *show = (dialog_vars.no_items 137 ? item->name 138 : item->text); 139 140 /* Clear 'residue' of last item */ 141 dlg_attrset(win, menubox_attr); 142 (void) wmove(win, row, 0); 143 for (i = 0; i < getmaxx(win); i++) 144 (void) waddch(win, ' '); 145 146 (void) wmove(win, row, all->check_x); 147 dlg_attrset(win, menubox_attr); 148 149 if (both) { 150 dlg_print_listitem(win, item->name, climit, first, selected); 151 (void) waddch(win, ' '); 152 first = FALSE; 153 } 154 155 (void) wmove(win, row, all->item_x); 156 climit = (getmaxx(win) - all->item_x + 1); 157 dlg_print_listitem(win, show, climit, first, selected); 158 159 if (selected) { 160 dlg_item_help(item->help); 161 } 162 dlg_attrset(win, save); 163 } 164 165 /* 166 * Prints either the left (unselected) or right (selected) list. 167 */ 168 static void 169 print_1_list(ALL_DATA * all, 170 int choice, 171 int selected) 172 { 173 MY_DATA *data = all->list + selected; 174 DIALOG_LISTITEM *target = (okIndex(all, choice) 175 ? all->items + choice 176 : 0); 177 WINDOW *win = data->win; 178 int i, j; 179 int last = 0; 180 int top_row = index2row(all, data->top_index, selected); 181 int max_rows = getmaxy(win); 182 183 TRACE(("! print_1_list %d %s, top %d\n", choice, mySide(selected), top_row)); 184 for (i = j = 0; j < max_rows; i++) { 185 int ii = i + top_row; 186 if (ii < 0) { 187 continue; 188 } else if (myItem(data, ii)) { 189 print_item(all, 190 win, 191 myItem(data, ii), 192 j, myItem(data, ii) == target); 193 last = ++j; 194 } else { 195 break; 196 } 197 } 198 if (wmove(win, last, 0) != ERR) { 199 while (waddch(win, ' ') != ERR) { 200 ; 201 } 202 } 203 (void) wnoutrefresh(win); 204 } 205 206 /* 207 * Return the previous item from the list, staying in the same column. If no 208 * further movement is possible, return the same choice as given. 209 */ 210 static int 211 prev_item(ALL_DATA * all, int choice, int selected) 212 { 213 int result = choice; 214 int row = index2row(all, choice, selected); 215 if (row > 0) { 216 row--; 217 result = row2index(all, row, selected); 218 } 219 TRACE(("! prev_item choice %d, %s = %d\n", choice, mySide(selected), result)); 220 return result; 221 } 222 223 /* 224 * Return true if the given choice is on the first page in the current column. 225 */ 226 static bool 227 stop_prev(ALL_DATA * all, int choice, int selected) 228 { 229 return (prev_item(all, choice, selected) == choice); 230 } 231 232 static bool 233 check_hotkey(DIALOG_LISTITEM * items, int choice, int selected) 234 { 235 bool result = FALSE; 236 237 if ((items[choice].state != 0) == selected) { 238 if (dlg_match_char(dlg_last_getc(), 239 (dialog_vars.no_tags 240 ? items[choice].text 241 : items[choice].name))) { 242 result = TRUE; 243 } 244 } 245 return result; 246 } 247 248 /* 249 * Return the next item from the list, staying in the same column. If no 250 * further movement is possible, return the same choice as given. 251 */ 252 static int 253 next_item(ALL_DATA * all, int choice, int selected) 254 { 255 MY_DATA *data = all->list + selected; 256 int result = choice; 257 int row = index2row(all, choice, selected); 258 TRACE(("! given item %d, testing next-item on row %d\n", choice, row + 1)); 259 if (myItem(data, row + 1)) { 260 result = row2index(all, row + 1, selected); 261 } 262 TRACE(("! next_item(%d, %s) ->%d\n", choice, mySide(selected), result)); 263 return result; 264 } 265 266 /* 267 * Return the first choice from items[] for the given column. 268 */ 269 static int 270 first_item(ALL_DATA * all, int selected) 271 { 272 MY_DATA *data = all->list + selected; 273 int result = -1; 274 275 if (myItem(data, 0) != 0) { 276 int n; 277 278 for (n = 0; n < all->item_no; ++n) { 279 if (myItem(data, 0) == &all->items[n]) { 280 result = n; 281 break; 282 } 283 } 284 } 285 TRACE(("! first_item %s = %d\n", mySide(selected), result)); 286 return result; 287 } 288 289 /* 290 * Return the last choice from items[] for the given column. 291 */ 292 static int 293 last_item(ALL_DATA * all, int selected) 294 { 295 MY_DATA *data = all->list + selected; 296 int result = -1; 297 int n; 298 299 for (n = 0; myItem(data, n) != 0; ++n) { 300 result = n; 301 } 302 if (result >= 0) { 303 result = row2index(all, result, selected); 304 } 305 TRACE(("! last_item %s = %d\n", mySide(selected), result)); 306 return result; 307 } 308 309 static int 310 skip_rows(ALL_DATA * all, int row, int skip, int selected) 311 { 312 MY_DATA *data = all->list + selected; 313 int result = row; 314 315 if (skip > 0) { 316 int n; 317 318 for (n = row + 1; (n < all->item_no) && (n <= row + skip); ++n) { 319 if (myItem(data, n) == 0) 320 break; 321 result = n; 322 } 323 } else if (skip < 0) { 324 result -= skip; 325 if (result < 0) 326 result = 0; 327 } 328 TRACE(("! skip_rows row %d, skip %d, %s = %d\n", 329 row, skip, mySide(selected), result)); 330 return result; 331 } 332 333 /* 334 * Find the closest item in the given column starting with the given choice. 335 */ 336 static int 337 closest_item(ALL_DATA * all, int choice, int selected) 338 { 339 int prev = choice; 340 int next = choice; 341 int result = choice; 342 int n; 343 344 for (n = choice; n >= 0; --n) { 345 if ((all->items[n].state != 0) == selected) { 346 prev = n; 347 break; 348 } 349 } 350 for (n = choice; n < all->item_no; ++n) { 351 if ((all->items[n].state != 0) == selected) { 352 next = n; 353 break; 354 } 355 } 356 if (prev != choice) { 357 result = prev; 358 if (next != choice) { 359 if ((choice - prev) > (next - choice)) { 360 result = next; 361 } 362 } 363 } else if (next != choice) { 364 result = next; 365 } 366 TRACE(("! XXX closest item choice %d, %s = %d\n", 367 choice, mySide(selected), result)); 368 return result; 369 } 370 371 static void 372 print_both(ALL_DATA * all, 373 int choice) 374 { 375 int selected; 376 int cur_y, cur_x; 377 WINDOW *dialog = wgetparent(all->list[0].win); 378 379 TRACE(("! print_both %d\n", choice)); 380 getyx(dialog, cur_y, cur_x); 381 for (selected = 0; selected < 2; ++selected) { 382 MY_DATA *data = all->list + selected; 383 WINDOW *win = data->win; 384 int thumb_top = index2row(all, data->top_index, selected); 385 int thumb_max = index2row(all, -1, selected); 386 int thumb_end = thumb_top + getmaxy(win); 387 388 print_1_list(all, choice, selected); 389 390 dlg_mouse_setcode(selected * KEY_MAX); 391 dlg_draw_scrollbar(dialog, 392 (long) (data->top_index), 393 (long) (thumb_top), 394 (long) MIN(thumb_end, thumb_max), 395 (long) thumb_max, 396 data->box_x + all->check_x, 397 data->box_x + getmaxx(win), 398 data->box_y, 399 data->box_y + getmaxy(win) + 1, 400 menubox_border2_attr, 401 menubox_border_attr); 402 } 403 (void) wmove(dialog, cur_y, cur_x); 404 dlg_mouse_setcode(0); 405 } 406 407 static void 408 set_top_item(ALL_DATA * all, int choice, int selected) 409 { 410 if (choice != all->list[selected].top_index) { 411 DLG_TRACE(("# set top of %s column to %d\n", 412 mySide(selected), 413 choice)); 414 all->list[selected].top_index = choice; 415 } 416 } 417 418 /* 419 * Adjust the top-index as needed to ensure that it and the given item are 420 * visible. 421 */ 422 static void 423 fix_top_item(ALL_DATA * all, int cur_item, int selected) 424 { 425 int top_item = all->list[selected].top_index; 426 int cur_row = index2row(all, cur_item, selected); 427 int top_row = index2row(all, top_item, selected); 428 429 if (cur_row < top_row) { 430 top_item = cur_item; 431 } else if ((cur_row - top_row) >= all->use_height) { 432 top_item = row2index(all, cur_row + 1 - all->use_height, selected); 433 } 434 if (cur_row < all->use_height) { 435 top_item = row2index(all, 0, selected); 436 } 437 DLG_TRACE(("# fix_top_item(cur_item %d, %s) ->top_item %d\n", 438 cur_item, mySide(selected), top_item)); 439 set_top_item(all, top_item, selected); 440 } 441 442 static void 443 append_right_side(ALL_DATA * all, int choice) 444 { 445 MY_DATA *data = &all->list[1]; 446 int j; 447 for (j = 0; j < all->item_no; ++j) { 448 if (myItem(data, j) == 0) { 449 myItem(data, j) = &all->items[choice]; 450 break; 451 } 452 } 453 } 454 455 static void 456 amend_right_side(ALL_DATA * all, int choice) 457 { 458 MY_DATA *data = &all->list[1]; 459 int j, k; 460 for (j = 0; j < all->item_no; ++j) { 461 if (myItem(data, j) == &all->items[choice]) { 462 for (k = j; k < all->item_no; ++k) { 463 if ((myItem(data, k) = myItem(data, k + 1)) == 0) 464 break; 465 } 466 break; 467 } 468 } 469 } 470 471 static void 472 fill_one_side(ALL_DATA * all, int selected) 473 { 474 int i, j; 475 MY_DATA *data = all->list + selected; 476 477 for (i = j = 0; j < all->item_no; ++j) { 478 myItem(data, i) = 0; 479 if ((all->items[j].state != 0) == selected) { 480 myItem(data, i) = all->items + j; 481 TRACE(("! %s item[%d] %p = all[%d] %p\n", 482 mySide(selected), 483 i, myItem(data, i), 484 j, all->items + j)); 485 ++i; 486 } 487 } 488 myItem(data, i) = 0; 489 } 490 491 static void 492 fill_both_sides(ALL_DATA * all) 493 { 494 int k; 495 496 for (k = 0; k < 2; ++k) { 497 fill_one_side(all, k); 498 } 499 } 500 501 /* 502 * This is an alternate interface to 'buildlist' which allows the application 503 * to read the list item states back directly without putting them in the 504 * output buffer. 505 */ 506 int 507 dlg_buildlist(const char *title, 508 const char *cprompt, 509 int height, 510 int width, 511 int list_height, 512 int item_no, 513 DIALOG_LISTITEM * items, 514 const char *states, 515 int order_mode, 516 int *current_item) 517 { 518 #define THIS_FUNC "dlg_buildlist" 519 /* *INDENT-OFF* */ 520 static DLG_KEYS_BINDING binding[] = { 521 HELPKEY_BINDINGS, 522 ENTERKEY_BINDINGS, 523 DLG_KEYS_DATA( DLGK_FIELD_NEXT, KEY_RIGHT ), 524 DLG_KEYS_DATA( DLGK_FIELD_NEXT, TAB ), 525 DLG_KEYS_DATA( DLGK_FIELD_PREV, KEY_BTAB ), 526 DLG_KEYS_DATA( DLGK_FIELD_PREV, KEY_LEFT ), 527 DLG_KEYS_DATA( DLGK_ITEM_FIRST, KEY_HOME ), 528 DLG_KEYS_DATA( DLGK_ITEM_LAST, KEY_END ), 529 DLG_KEYS_DATA( DLGK_ITEM_LAST, KEY_LL ), 530 DLG_KEYS_DATA( DLGK_ITEM_NEXT, '+' ), 531 DLG_KEYS_DATA( DLGK_ITEM_NEXT, KEY_DOWN ), 532 DLG_KEYS_DATA( DLGK_ITEM_NEXT, CHR_NEXT ), 533 DLG_KEYS_DATA( DLGK_ITEM_PREV, '-' ), 534 DLG_KEYS_DATA( DLGK_ITEM_PREV, KEY_UP ), 535 DLG_KEYS_DATA( DLGK_ITEM_PREV, CHR_PREVIOUS ), 536 DLG_KEYS_DATA( DLGK_PAGE_NEXT, KEY_NPAGE ), 537 DLG_KEYS_DATA( DLGK_PAGE_NEXT, DLGK_MOUSE(KEY_NPAGE) ), 538 DLG_KEYS_DATA( DLGK_PAGE_NEXT, DLGK_MOUSE(KEY_NPAGE+KEY_MAX) ), 539 DLG_KEYS_DATA( DLGK_PAGE_PREV, KEY_PPAGE ), 540 DLG_KEYS_DATA( DLGK_PAGE_PREV, DLGK_MOUSE(KEY_PPAGE) ), 541 DLG_KEYS_DATA( DLGK_PAGE_PREV, DLGK_MOUSE(KEY_PPAGE+KEY_MAX) ), 542 DLG_KEYS_DATA( DLGK_GRID_LEFT, KEY_LEFTCOL ), 543 DLG_KEYS_DATA( DLGK_GRID_RIGHT, KEY_RIGHTCOL ), 544 TOGGLEKEY_BINDINGS, 545 END_KEYS_BINDING 546 }; 547 /* *INDENT-ON* */ 548 549 #ifdef KEY_RESIZE 550 int old_height = height; 551 int old_width = width; 552 #endif 553 ALL_DATA all; 554 MY_DATA *data = all.list; 555 int i, j, k, key2, found, x, y, cur_x, cur_y; 556 int key, fkey; 557 bool save_visit = dialog_state.visit_items; 558 int button; 559 int cur_item; 560 int name_width, text_width, full_width, list_width; 561 int result = DLG_EXIT_UNKNOWN; 562 int num_states; 563 bool first = TRUE; 564 WINDOW *dialog; 565 char *prompt; 566 const char **buttons = dlg_ok_labels(); 567 const char *widget_name = "buildlist"; 568 569 dialog_state.plain_buttons = TRUE; 570 571 /* 572 * Unlike other uses of --visit-items, we have two windows to visit. 573 */ 574 if (dialog_state.visit_cols) 575 dialog_state.visit_cols = 2; 576 577 memset(&all, 0, sizeof(all)); 578 all.items = items; 579 all.item_no = item_no; 580 for (k = 0; k < 2; ++k) { 581 data[k].ip = dlg_calloc(DIALOG_LISTITEM *, (item_no + 2)); 582 } 583 fill_both_sides(&all); 584 585 if (dialog_vars.default_item != 0) { 586 cur_item = dlg_default_listitem(items); 587 } else { 588 if ((cur_item = first_item(&all, 0)) < 0) 589 cur_item = first_item(&all, 1); 590 } 591 button = (dialog_state.visit_items 592 ? (items[cur_item].state ? sRIGHT : sLEFT) 593 : dlg_default_button()); 594 595 dlg_does_output(); 596 597 #ifdef KEY_RESIZE 598 retry: 599 #endif 600 601 prompt = dlg_strclone(cprompt); 602 dlg_tab_correct_str(prompt); 603 604 all.use_height = list_height; 605 all.use_width = (2 * (dlg_calc_list_width(item_no, items) 606 + 4 607 + 2 * MARGIN) 608 + 1); 609 all.use_width = MAX(26, all.use_width); 610 if (all.use_height == 0) { 611 /* calculate height without items (4) */ 612 dlg_auto_size(title, prompt, &height, &width, MIN_HIGH, all.use_width); 613 dlg_calc_listh(&height, &all.use_height, item_no); 614 } else { 615 dlg_auto_size(title, prompt, 616 &height, &width, 617 MIN_HIGH + all.use_height, all.use_width); 618 } 619 dlg_button_layout(buttons, &width); 620 dlg_print_size(height, width); 621 dlg_ctl_size(height, width); 622 623 /* we need at least two states */ 624 if (states == 0 || strlen(states) < 2) 625 states = " *"; 626 num_states = (int) strlen(states); 627 628 x = dlg_box_x_ordinate(width); 629 y = dlg_box_y_ordinate(height); 630 631 dialog = dlg_new_window(height, width, y, x); 632 dlg_register_window(dialog, widget_name, binding); 633 dlg_register_buttons(dialog, widget_name, buttons); 634 635 dlg_mouse_setbase(all.base_x = x, all.base_y = y); 636 637 dlg_draw_box2(dialog, 0, 0, height, width, dialog_attr, border_attr, border2_attr); 638 dlg_draw_bottom_box2(dialog, border_attr, border2_attr, dialog_attr); 639 dlg_draw_title(dialog, title); 640 641 dlg_attrset(dialog, dialog_attr); 642 dlg_print_autowrap(dialog, prompt, height, width); 643 644 list_width = (width - 6 * MARGIN - 2) / 2; 645 getyx(dialog, cur_y, cur_x); 646 data[0].box_y = cur_y + 1; 647 data[0].box_x = MARGIN + 1; 648 data[1].box_y = cur_y + 1; 649 data[1].box_x = data[0].box_x + 1 + 2 * MARGIN + list_width; 650 651 /* 652 * After displaying the prompt, we know how much space we really have. 653 * Limit the list to avoid overwriting the ok-button. 654 */ 655 all.use_height = height - MIN_HIGH - cur_y; 656 if (all.use_height <= 0) 657 all.use_height = 1; 658 659 for (k = 0; k < 2; ++k) { 660 /* create new window for the list */ 661 data[k].win = dlg_sub_window(dialog, all.use_height, list_width, 662 y + data[k].box_y + 1, 663 x + data[k].box_x + 1); 664 665 /* draw a box around the list items */ 666 dlg_draw_box(dialog, data[k].box_y, data[k].box_x, 667 all.use_height + 2 * MARGIN, 668 list_width + 2 * MARGIN, 669 menubox_border_attr, menubox_border2_attr); 670 } 671 672 text_width = 0; 673 name_width = 0; 674 /* Find length of longest item to center buildlist */ 675 for (i = 0; i < item_no; i++) { 676 text_width = MAX(text_width, dlg_count_columns(items[i].text)); 677 name_width = MAX(name_width, dlg_count_columns(items[i].name)); 678 } 679 680 /* If the name+text is wider than the list is allowed, then truncate 681 * one or both of them. If the name is no wider than 1/4 of the list, 682 * leave it intact. 683 */ 684 all.use_width = (list_width - 6 * MARGIN); 685 if (dialog_vars.no_tags && !dialog_vars.no_items) { 686 full_width = MIN(all.use_width, text_width); 687 } else if (dialog_vars.no_items) { 688 full_width = MIN(all.use_width, name_width); 689 } else { 690 if (text_width >= 0 691 && name_width >= 0 692 && all.use_width > 0 693 && text_width + name_width > all.use_width) { 694 int need = (int) (0.25 * all.use_width); 695 if (name_width > need) { 696 int want = (int) (all.use_width * ((double) name_width) / 697 (text_width + name_width)); 698 name_width = (want > need) ? want : need; 699 } 700 text_width = all.use_width - name_width; 701 } 702 full_width = text_width + name_width; 703 } 704 705 all.check_x = (all.use_width - full_width) / 2; 706 all.item_x = ((dialog_vars.no_tags 707 ? 0 708 : (dialog_vars.no_items 709 ? 0 710 : (name_width + 2))) 711 + all.check_x); 712 713 /* ensure we are scrolled to show the current choice */ 714 j = MIN(all.use_height, item_no); 715 for (i = 0; i < 2; ++i) { 716 if ((items[cur_item].state != 0) == i) { 717 int top_item = cur_item - j + 1; 718 if (top_item < 0) 719 top_item = 0; 720 while ((items[top_item].state != 0) != i) 721 ++top_item; 722 set_top_item(&all, top_item, i); 723 } else { 724 set_top_item(&all, 0, i); 725 } 726 } 727 728 /* register the new window, along with its borders */ 729 for (i = 0; i < 2; ++i) { 730 dlg_mouse_mkbigregion(data[i].box_y + 1, 731 data[i].box_x, 732 all.use_height, 733 list_width + 2, 734 2 * KEY_MAX + (i * (1 + all.use_height)), 735 1, 1, 1 /* by lines */ ); 736 } 737 738 dlg_draw_buttons(dialog, height - 2, 0, buttons, button, FALSE, width); 739 740 while (result == DLG_EXIT_UNKNOWN) { 741 int which = (items[cur_item].state != 0); 742 MY_DATA *moi = data + which; 743 int at_top = index2row(&all, moi->top_index, which); 744 int at_end = index2row(&all, -1, which); 745 int at_bot = skip_rows(&all, at_top, all.use_height, which); 746 int was_mouse; 747 748 DLG_TRACE(("# ** state %d:%d top %d (%d:%d:%d) %s\n", 749 cur_item, item_no - 1, 750 moi->top_index, 751 at_top, at_bot, at_end, 752 mySide(which))); 753 754 if (first) { 755 print_both(&all, cur_item); 756 dlg_trace_win(dialog); 757 first = FALSE; 758 } 759 760 if (button < 0) { /* --visit-items */ 761 int cur_row = index2row(&all, cur_item, which); 762 cur_y = (data[which].box_y 763 + cur_row 764 + 1); 765 if (at_top > 0) 766 cur_y -= at_top; 767 cur_x = (data[which].box_x 768 + all.check_x + 1); 769 DLG_TRACE(("# ...visit row %d (%d,%d)\n", cur_row, cur_y, cur_x)); 770 wmove(dialog, cur_y, cur_x); 771 } 772 773 key = dlg_mouse_wgetch(dialog, &fkey); 774 if (dlg_result_key(key, fkey, &result)) { 775 if (!dlg_button_key(result, &button, &key, &fkey)) 776 break; 777 } 778 779 was_mouse = (fkey && is_DLGK_MOUSE(key)); 780 if (was_mouse) 781 key -= M_EVENT; 782 783 if (!was_mouse) { 784 ; 785 } else if (key >= 2 * KEY_MAX) { 786 i = (key - 2 * KEY_MAX) % (1 + all.use_height); 787 j = (key - 2 * KEY_MAX) / (1 + all.use_height); 788 k = row2index(&all, i + at_top, j); 789 DLG_TRACE(("# MOUSE column %d, row %d ->item %d\n", j, i, k)); 790 if (k >= 0 && j < 2) { 791 if (j != which) { 792 /* 793 * Mouse click was in the other column. 794 */ 795 moi = data + j; 796 fix_top_item(&all, k, j); 797 } 798 which = j; 799 at_top = index2row(&all, moi->top_index, which); 800 at_bot = skip_rows(&all, at_top, all.use_height, which); 801 cur_item = k; 802 print_both(&all, cur_item); 803 key = DLGK_TOGGLE; /* force the selected item to toggle */ 804 } else { 805 beep(); 806 continue; 807 } 808 fkey = FALSE; 809 } else if (key >= KEY_MIN) { 810 if (key > KEY_MAX) { 811 if (which == 0) { 812 key = KEY_RIGHTCOL; /* switch to right-column */ 813 fkey = FALSE; 814 } else { 815 key -= KEY_MAX; 816 } 817 } else { 818 if (which == 1) { 819 key = KEY_LEFTCOL; /* switch to left-column */ 820 fkey = FALSE; 821 } 822 } 823 key = dlg_lookup_key(dialog, key, &fkey); 824 } 825 826 /* 827 * A space toggles the item status. Normally we put the cursor on 828 * the next available item in the same column. But if there are no 829 * more items in the column, move the cursor to the other column. 830 */ 831 if (key == DLGK_TOGGLE) { 832 int new_choice; 833 int new_state = items[cur_item].state + 1; 834 835 if ((new_choice = next_item(&all, cur_item, which)) == cur_item) { 836 new_choice = prev_item(&all, cur_item, which); 837 } 838 DLG_TRACE(("# cur_item %d, new_choice:%d\n", cur_item, new_choice)); 839 /* FIXME - how to test and handle multiple states? */ 840 if (new_state >= num_states) 841 new_state = 0; 842 843 items[cur_item].state = new_state; 844 if (order_mode) { 845 fill_one_side(&all, 0); 846 if (new_state) { 847 append_right_side(&all, cur_item); 848 } else { 849 amend_right_side(&all, cur_item); 850 } 851 } else { 852 fill_both_sides(&all); 853 } 854 if (cur_item == moi->top_index) { 855 set_top_item(&all, new_choice, which); 856 } 857 858 if (new_choice >= 0) { 859 fix_top_item(&all, cur_item, !which); 860 cur_item = new_choice; 861 } 862 print_both(&all, cur_item); 863 dlg_trace_win(dialog); 864 continue; /* wait for another key press */ 865 } 866 867 /* 868 * Check if key pressed matches first character of any item tag in 869 * list. If there is more than one match, we will cycle through 870 * each one as the same key is pressed repeatedly. 871 */ 872 found = FALSE; 873 if (!fkey) { 874 if (button < 0 || !dialog_state.visit_items) { 875 for (j = cur_item + 1; j < item_no; j++) { 876 if (check_hotkey(items, j, which)) { 877 found = TRUE; 878 i = j; 879 break; 880 } 881 } 882 if (!found) { 883 for (j = 0; j <= cur_item; j++) { 884 if (check_hotkey(items, j, which)) { 885 found = TRUE; 886 i = j; 887 break; 888 } 889 } 890 } 891 if (found) 892 dlg_flush_getc(); 893 } else if ((j = dlg_char_to_button(key, buttons)) >= 0) { 894 button = j; 895 ungetch('\n'); 896 continue; 897 } 898 } 899 900 /* 901 * A single digit (1-9) positions the selection to that line in the 902 * current screen. 903 */ 904 if (!found 905 && (key <= '9') 906 && (key > '0') 907 && (key - '1' < at_bot)) { 908 found = TRUE; 909 i = key - '1'; 910 } 911 912 if (!found && fkey) { 913 switch (key) { 914 case DLGK_FIELD_PREV: 915 if ((button == sRIGHT) && dialog_state.visit_items) { 916 key = DLGK_GRID_LEFT; 917 button = sLEFT; 918 } else { 919 button = dlg_prev_button(buttons, button); 920 dlg_draw_buttons(dialog, height - 2, 0, buttons, button, 921 FALSE, width); 922 if (button == sRIGHT) { 923 key = DLGK_GRID_RIGHT; 924 } else { 925 continue; 926 } 927 } 928 break; 929 case DLGK_FIELD_NEXT: 930 if ((button == sLEFT) && dialog_state.visit_items) { 931 key = DLGK_GRID_RIGHT; 932 button = sRIGHT; 933 } else { 934 button = dlg_next_button(buttons, button); 935 dlg_draw_buttons(dialog, height - 2, 0, buttons, button, 936 FALSE, width); 937 if (button == sLEFT) { 938 key = DLGK_GRID_LEFT; 939 } else { 940 continue; 941 } 942 } 943 break; 944 } 945 } 946 947 if (!found && fkey) { 948 i = cur_item; 949 found = TRUE; 950 switch (key) { 951 case DLGK_GRID_LEFT: 952 i = closest_item(&all, cur_item, 0); 953 fix_top_item(&all, i, 0); 954 break; 955 case DLGK_GRID_RIGHT: 956 if (order_mode) { 957 i = last_item(&all, 1); 958 } else { 959 i = closest_item(&all, cur_item, 1); 960 } 961 fix_top_item(&all, i, 1); 962 break; 963 case DLGK_PAGE_PREV: 964 if (cur_item > moi->top_index) { 965 i = moi->top_index; 966 } else if (moi->top_index != 0) { 967 int temp = at_top; 968 if ((temp -= all.use_height) < 0) 969 temp = 0; 970 i = row2index(&all, temp, which); 971 } 972 break; 973 case DLGK_PAGE_NEXT: 974 if ((at_end - at_bot) < all.use_height) { 975 i = next_item(&all, 976 row2index(&all, at_end, which), 977 which); 978 } else { 979 i = next_item(&all, 980 row2index(&all, at_bot, which), 981 which); 982 at_top = at_bot; 983 set_top_item(&all, 984 next_item(&all, 985 row2index(&all, at_top, which), 986 which), 987 which); 988 at_bot = skip_rows(&all, at_top, all.use_height, which); 989 at_bot = MIN(at_bot, at_end); 990 } 991 break; 992 case DLGK_ITEM_FIRST: 993 i = first_item(&all, which); 994 break; 995 case DLGK_ITEM_LAST: 996 i = last_item(&all, which); 997 break; 998 case DLGK_ITEM_PREV: 999 i = prev_item(&all, cur_item, which); 1000 if (stop_prev(&all, cur_item, which)) 1001 continue; 1002 break; 1003 case DLGK_ITEM_NEXT: 1004 i = next_item(&all, cur_item, which); 1005 break; 1006 default: 1007 found = FALSE; 1008 break; 1009 } 1010 } 1011 1012 if (found) { 1013 if (i != cur_item) { 1014 int now_at = index2row(&all, i, which); 1015 int oops = item_no; 1016 int old_item; 1017 1018 DLG_TRACE(("# <--CHOICE %d\n", i)); 1019 DLG_TRACE(("# <--topITM %d\n", moi->top_index)); 1020 DLG_TRACE(("# <--now_at %d\n", now_at)); 1021 DLG_TRACE(("# <--at_top %d\n", at_top)); 1022 DLG_TRACE(("# <--at_bot %d\n", at_bot)); 1023 1024 if (now_at >= at_bot) { 1025 while (now_at >= at_bot) { 1026 if ((at_bot - at_top) >= all.use_height) { 1027 set_top_item(&all, 1028 next_item(&all, moi->top_index, which), 1029 which); 1030 } 1031 at_top = index2row(&all, moi->top_index, which); 1032 at_bot = skip_rows(&all, at_top, all.use_height, which); 1033 1034 DLG_TRACE(("# ...at_bot %d (now %d vs %d)\n", 1035 at_bot, now_at, at_end)); 1036 DLG_TRACE(("# ...topITM %d\n", moi->top_index)); 1037 DLG_TRACE(("# ...at_top %d (diff %d)\n", at_top, 1038 at_bot - at_top)); 1039 1040 if (at_bot >= at_end) { 1041 /* 1042 * If we bumped into the end, move the top-item 1043 * down by one line so that we can display the 1044 * last item in the list. 1045 */ 1046 if ((at_bot - at_top) > all.use_height) { 1047 set_top_item(&all, 1048 next_item(&all, moi->top_index, which), 1049 which); 1050 } else if (at_top > 0 && 1051 (at_bot - at_top) >= all.use_height) { 1052 set_top_item(&all, 1053 next_item(&all, moi->top_index, which), 1054 which); 1055 } 1056 break; 1057 } 1058 if (--oops < 0) { 1059 DLG_TRACE(("# OOPS-forward\n")); 1060 break; 1061 } 1062 } 1063 } else if (now_at < at_top) { 1064 while (now_at < at_top) { 1065 old_item = moi->top_index; 1066 set_top_item(&all, 1067 prev_item(&all, moi->top_index, which), 1068 which); 1069 at_top = index2row(&all, moi->top_index, which); 1070 1071 DLG_TRACE(("# ...at_top %d (now %d)\n", at_top, now_at)); 1072 DLG_TRACE(("# ...topITM %d\n", moi->top_index)); 1073 1074 if (moi->top_index >= old_item) 1075 break; 1076 if (at_top <= now_at) 1077 break; 1078 if (--oops < 0) { 1079 DLG_TRACE(("# OOPS-backward\n")); 1080 break; 1081 } 1082 } 1083 } 1084 DLG_TRACE(("# -->now_at %d\n", now_at)); 1085 cur_item = i; 1086 print_both(&all, cur_item); 1087 } 1088 dlg_trace_win(dialog); 1089 continue; /* wait for another key press */ 1090 } 1091 1092 if (fkey) { 1093 switch (key) { 1094 case DLGK_ENTER: 1095 result = dlg_enter_buttoncode(button); 1096 break; 1097 case DLGK_LEAVE: 1098 result = dlg_ok_buttoncode(button); 1099 break; 1100 #ifdef KEY_RESIZE 1101 case KEY_RESIZE: 1102 dlg_will_resize(dialog); 1103 /* reset data */ 1104 height = old_height; 1105 width = old_width; 1106 free(prompt); 1107 _dlg_resize_cleanup(dialog); 1108 /* repaint */ 1109 first = TRUE; 1110 goto retry; 1111 #endif 1112 default: 1113 if (was_mouse) { 1114 if ((key2 = dlg_ok_buttoncode(key)) >= 0) { 1115 result = key2; 1116 break; 1117 } 1118 beep(); 1119 } 1120 } 1121 } else if (key > 0) { 1122 beep(); 1123 } 1124 } 1125 1126 /* 1127 * If told to re-order the list, update it to reflect the current display: 1128 * a) The left-side will be at the beginning, without gaps. 1129 * b) The right-side will follow, in display-order. 1130 */ 1131 if (order_mode) { 1132 DIALOG_LISTITEM *redo; 1133 int row; 1134 int choice; 1135 int new_item = cur_item; 1136 1137 redo = dlg_calloc(DIALOG_LISTITEM, (size_t) item_no + 1); 1138 assert_ptr(redo, THIS_FUNC); 1139 1140 j = 0; 1141 for (k = 0; k < 2; ++k) { 1142 for (row = 0; row < item_no; ++row) { 1143 if (myItem(all.list + k, row) == 0) 1144 break; 1145 choice = row2index(&all, row, k); 1146 if (choice == cur_item) 1147 new_item = j; 1148 redo[j++] = items[choice]; 1149 } 1150 } 1151 1152 cur_item = new_item; 1153 memcpy(items, redo, sizeof(DIALOG_LISTITEM) * (size_t) (item_no + 1)); 1154 1155 free(redo); 1156 } 1157 1158 for (k = 0; k < 2; ++k) { 1159 free(data[k].ip); 1160 } 1161 1162 dialog_state.visit_cols = save_visit; 1163 dlg_del_window(dialog); 1164 dlg_mouse_free_regions(); 1165 free(prompt); 1166 1167 *current_item = cur_item; 1168 return result; 1169 #undef THIS_FUNC 1170 } 1171 1172 /* 1173 * Display a dialog box with a list of options that can be turned on or off 1174 */ 1175 int 1176 dialog_buildlist(const char *title, 1177 const char *cprompt, 1178 int height, 1179 int width, 1180 int list_height, 1181 int item_no, 1182 char **items, 1183 int order_mode) 1184 { 1185 #define THIS_FUNC "dialog_buildlist" 1186 int result; 1187 int i, j; 1188 DIALOG_LISTITEM *listitems; 1189 bool separate_output = dialog_vars.separate_output; 1190 bool show_status = FALSE; 1191 int current = 0; 1192 char *help_result; 1193 1194 DLG_TRACE(("# buildlist args:\n")); 1195 DLG_TRACE2S("title", title); 1196 DLG_TRACE2S("message", cprompt); 1197 DLG_TRACE2N("height", height); 1198 DLG_TRACE2N("width", width); 1199 DLG_TRACE2N("lheight", list_height); 1200 DLG_TRACE2N("llength", item_no); 1201 /* FIXME dump the items[][] too */ 1202 DLG_TRACE2N("order", order_mode != 0); 1203 1204 listitems = dlg_calloc(DIALOG_LISTITEM, (size_t) item_no + 1); 1205 assert_ptr(listitems, THIS_FUNC); 1206 1207 for (i = j = 0; i < item_no; ++i) { 1208 listitems[i].name = items[j++]; 1209 listitems[i].text = (dialog_vars.no_items 1210 ? dlg_strempty() 1211 : items[j++]); 1212 listitems[i].state = !dlg_strcmp(items[j++], "on"); 1213 listitems[i].help = ((dialog_vars.item_help) 1214 ? items[j++] 1215 : dlg_strempty()); 1216 } 1217 dlg_align_columns(&listitems[0].text, (int) sizeof(DIALOG_LISTITEM), item_no); 1218 1219 result = dlg_buildlist(title, 1220 cprompt, 1221 height, 1222 width, 1223 list_height, 1224 item_no, 1225 listitems, 1226 NULL, 1227 order_mode, 1228 ¤t); 1229 1230 switch (result) { 1231 case DLG_EXIT_OK: /* FALLTHRU */ 1232 case DLG_EXIT_EXTRA: 1233 show_status = TRUE; 1234 break; 1235 case DLG_EXIT_HELP: 1236 dlg_add_help_listitem(&result, &help_result, &listitems[current]); 1237 if ((show_status = dialog_vars.help_status)) { 1238 if (separate_output) { 1239 dlg_add_string(help_result); 1240 } else { 1241 dlg_add_quoted(help_result); 1242 } 1243 } else { 1244 dlg_add_string(help_result); 1245 } 1246 break; 1247 } 1248 1249 if (show_status) { 1250 for (i = 0; i < item_no; i++) { 1251 if (listitems[i].state) { 1252 if (dlg_need_separator()) 1253 dlg_add_separator(); 1254 if (separate_output) { 1255 dlg_add_string(listitems[i].name); 1256 } else { 1257 dlg_add_quoted(listitems[i].name); 1258 } 1259 } 1260 } 1261 AddLastKey(); 1262 } 1263 1264 dlg_free_columns(&listitems[0].text, (int) sizeof(DIALOG_LISTITEM), item_no); 1265 free(listitems); 1266 return result; 1267 #undef THIS_FUNC 1268 } 1269