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