1 /* 2 * $Id: editbox.c,v 1.70 2018/06/19 22:57:01 tom Exp $ 3 * 4 * editbox.c -- implements the edit box 5 * 6 * Copyright 2007-2016,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 * 11 * This program is distributed in the hope that it will be useful, but 12 * WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 * Lesser General Public License for more details. 15 * 16 * You should have received a copy of the GNU Lesser General Public 17 * License along with this program; if not, write to 18 * Free Software Foundation, Inc. 19 * 51 Franklin St., Fifth Floor 20 * Boston, MA 02110, USA. 21 */ 22 23 #include <dialog.h> 24 #include <dlg_keys.h> 25 26 #include <sys/stat.h> 27 28 #define sTEXT -1 29 30 static void 31 fail_list(void) 32 { 33 dlg_exiterr("File too large"); 34 } 35 36 static void 37 grow_list(char ***list, int *have, int want) 38 { 39 if (want > *have) { 40 size_t last = (size_t) *have; 41 size_t need = (size_t) (want | 31) + 3; 42 *have = (int) need; 43 (*list) = dlg_realloc(char *, need, *list); 44 if ((*list) == 0) { 45 fail_list(); 46 } else { 47 while (++last < need) { 48 (*list)[last] = 0; 49 } 50 } 51 } 52 } 53 54 static void 55 load_list(const char *file, char ***list, int *rows) 56 { 57 FILE *fp; 58 char *blob = 0; 59 struct stat sb; 60 unsigned n, pass; 61 unsigned need; 62 size_t size; 63 64 *list = 0; 65 *rows = 0; 66 67 if (stat(file, &sb) < 0 || 68 (sb.st_mode & S_IFMT) != S_IFREG) 69 dlg_exiterr("Not a file: %s", file); 70 71 size = (size_t) sb.st_size; 72 if ((blob = dlg_malloc(char, size + 2)) == 0) { 73 fail_list(); 74 } else { 75 blob[size] = '\0'; 76 77 if ((fp = fopen(file, "r")) == 0) 78 dlg_exiterr("Cannot open: %s", file); 79 size = fread(blob, sizeof(char), size, fp); 80 fclose(fp); 81 82 /* 83 * If the file is not empty, ensure that it ends with a newline. 84 */ 85 if (size != 0 && blob[size - 1] != '\n') { 86 blob[++size - 1] = '\n'; 87 blob[size] = '\0'; 88 } 89 90 for (pass = 0; pass < 2; ++pass) { 91 int first = TRUE; 92 need = 0; 93 for (n = 0; n < size; ++n) { 94 if (first && pass) { 95 (*list)[need] = blob + n; 96 first = FALSE; 97 } 98 if (blob[n] == '\n') { 99 first = TRUE; 100 ++need; 101 if (pass) 102 blob[n] = '\0'; 103 } 104 } 105 if (pass) { 106 if (need == 0) { 107 (*list)[0] = dlg_strclone(""); 108 (*list)[1] = 0; 109 } else { 110 for (n = 0; n < need; ++n) { 111 (*list)[n] = dlg_strclone((*list)[n]); 112 } 113 (*list)[need] = 0; 114 } 115 } else { 116 grow_list(list, rows, (int) need + 1); 117 } 118 } 119 free(blob); 120 } 121 } 122 123 static void 124 free_list(char ***list, int *rows) 125 { 126 if (*list != 0) { 127 int n; 128 for (n = 0; n < (*rows); ++n) { 129 if ((*list)[n] != 0) 130 free((*list)[n]); 131 } 132 free(*list); 133 *list = 0; 134 } 135 *rows = 0; 136 } 137 138 /* 139 * Display a single row in the editing window: 140 * thisrow is the actual row number that's being displayed. 141 * show_row is the row number that's highlighted for edit. 142 * base_row is the first row number in the window 143 */ 144 static bool 145 display_one(WINDOW *win, 146 char *text, 147 int thisrow, 148 int show_row, 149 int base_row, 150 int chr_offset) 151 { 152 bool result; 153 154 if (text != 0) { 155 dlg_show_string(win, 156 text, 157 chr_offset, 158 ((thisrow == show_row) 159 ? form_active_text_attr 160 : form_text_attr), 161 thisrow - base_row, 162 0, 163 getmaxx(win), 164 FALSE, 165 FALSE); 166 result = TRUE; 167 } else { 168 result = FALSE; 169 } 170 return result; 171 } 172 173 static void 174 display_all(WINDOW *win, 175 char **list, 176 int show_row, 177 int firstrow, 178 int lastrow, 179 int chr_offset) 180 { 181 int limit = getmaxy(win); 182 int row; 183 184 dlg_attr_clear(win, getmaxy(win), getmaxx(win), dialog_attr); 185 if (lastrow - firstrow >= limit) 186 lastrow = firstrow + limit; 187 for (row = firstrow; row < lastrow; ++row) { 188 if (!display_one(win, list[row], 189 row, show_row, firstrow, 190 (row == show_row) ? chr_offset : 0)) 191 break; 192 } 193 } 194 195 static int 196 size_list(char **list) 197 { 198 int result = 0; 199 200 if (list != 0) { 201 while (*list++ != 0) { 202 ++result; 203 } 204 } 205 return result; 206 } 207 208 static bool 209 scroll_to(int pagesize, int rows, int *base_row, int *this_row, int target) 210 { 211 bool result = FALSE; 212 213 if (target < *base_row) { 214 if (target < 0) { 215 if (*base_row == 0 && *this_row == 0) { 216 beep(); 217 } else { 218 *this_row = 0; 219 *base_row = 0; 220 result = TRUE; 221 } 222 } else { 223 *this_row = target; 224 *base_row = target; 225 result = TRUE; 226 } 227 } else if (target >= rows) { 228 if (*this_row < rows - 1) { 229 *this_row = rows - 1; 230 *base_row = rows - 1; 231 result = TRUE; 232 } else { 233 beep(); 234 } 235 } else if (target >= *base_row + pagesize) { 236 *this_row = target; 237 *base_row = target; 238 result = TRUE; 239 } else { 240 *this_row = target; 241 result = FALSE; 242 } 243 if (pagesize < rows) { 244 if (*base_row + pagesize >= rows) { 245 *base_row = rows - pagesize; 246 } 247 } else { 248 *base_row = 0; 249 } 250 return result; 251 } 252 253 static int 254 col_to_chr_offset(const char *text, int col) 255 { 256 const int *cols = dlg_index_columns(text); 257 const int *indx = dlg_index_wchars(text); 258 bool found = FALSE; 259 int result = 0; 260 unsigned n; 261 unsigned len = (unsigned) dlg_count_wchars(text); 262 263 for (n = 0; n < len; ++n) { 264 if (cols[n] <= col && cols[n + 1] > col) { 265 result = indx[n]; 266 found = TRUE; 267 break; 268 } 269 } 270 if (!found && len && cols[len] == col) { 271 result = indx[len]; 272 } 273 return result; 274 } 275 276 #define SCROLL_TO(target) show_all = scroll_to(pagesize, listsize, &base_row, &thisrow, target) 277 278 #define PREV_ROW (*list)[thisrow - 1] 279 #define THIS_ROW (*list)[thisrow] 280 #define NEXT_ROW (*list)[thisrow + 1] 281 282 #define UPDATE_COL(input) col_offset = dlg_edit_offset(input, chr_offset, box_width) 283 284 static int 285 widest_line(char **list) 286 { 287 int result = MAX_LEN; 288 char *value; 289 290 if (list != 0) { 291 while ((value = *list++) != 0) { 292 int check = (int) strlen(value); 293 if (check > result) 294 result = check; 295 } 296 } 297 return result; 298 } 299 300 #define NAVIGATE_BINDINGS \ 301 DLG_KEYS_DATA( DLGK_GRID_DOWN, KEY_DOWN ), \ 302 DLG_KEYS_DATA( DLGK_GRID_RIGHT, KEY_RIGHT ), \ 303 DLG_KEYS_DATA( DLGK_GRID_LEFT, KEY_LEFT ), \ 304 DLG_KEYS_DATA( DLGK_GRID_UP, KEY_UP ), \ 305 DLG_KEYS_DATA( DLGK_FIELD_NEXT, TAB ), \ 306 DLG_KEYS_DATA( DLGK_FIELD_PREV, KEY_BTAB ), \ 307 DLG_KEYS_DATA( DLGK_PAGE_FIRST, KEY_HOME ), \ 308 DLG_KEYS_DATA( DLGK_PAGE_LAST, KEY_END ), \ 309 DLG_KEYS_DATA( DLGK_PAGE_LAST, KEY_LL ), \ 310 DLG_KEYS_DATA( DLGK_PAGE_NEXT, KEY_NPAGE ), \ 311 DLG_KEYS_DATA( DLGK_PAGE_NEXT, DLGK_MOUSE(KEY_NPAGE) ), \ 312 DLG_KEYS_DATA( DLGK_PAGE_PREV, KEY_PPAGE ), \ 313 DLG_KEYS_DATA( DLGK_PAGE_PREV, DLGK_MOUSE(KEY_PPAGE) ) 314 /* 315 * Display a dialog box for editing a copy of a file 316 */ 317 int 318 dlg_editbox(const char *title, 319 char ***list, 320 int *rows, 321 int height, 322 int width) 323 { 324 /* *INDENT-OFF* */ 325 static DLG_KEYS_BINDING binding[] = { 326 HELPKEY_BINDINGS, 327 ENTERKEY_BINDINGS, 328 NAVIGATE_BINDINGS, 329 TOGGLEKEY_BINDINGS, 330 END_KEYS_BINDING 331 }; 332 static DLG_KEYS_BINDING binding2[] = { 333 INPUTSTR_BINDINGS, 334 HELPKEY_BINDINGS, 335 ENTERKEY_BINDINGS, 336 NAVIGATE_BINDINGS, 337 /* no TOGGLEKEY_BINDINGS, since that includes space... */ 338 END_KEYS_BINDING 339 }; 340 /* *INDENT-ON* */ 341 342 #ifdef KEY_RESIZE 343 int old_height = height; 344 int old_width = width; 345 #endif 346 int x, y, box_y, box_x, box_height, box_width; 347 int show_buttons; 348 int thisrow, base_row, lastrow; 349 int goal_col = -1; 350 int col_offset = 0; 351 int chr_offset = 0; 352 int key, fkey, code; 353 int pagesize; 354 int listsize = size_list(*list); 355 int result = DLG_EXIT_UNKNOWN; 356 int state; 357 size_t max_len = (size_t) dlg_max_input(widest_line(*list)); 358 char *input, *buffer; 359 bool show_all, show_one, was_mouse; 360 bool first_trace = TRUE; 361 WINDOW *dialog; 362 WINDOW *editing; 363 DIALOG_VARS save_vars; 364 const char **buttons = dlg_ok_labels(); 365 int mincols = (3 * COLS / 4); 366 367 DLG_TRACE(("# editbox args:\n")); 368 DLG_TRACE2S("title", title); 369 /* FIXME dump the rows & list */ 370 DLG_TRACE2N("height", height); 371 DLG_TRACE2N("width", width); 372 373 dlg_save_vars(&save_vars); 374 dialog_vars.separate_output = TRUE; 375 376 dlg_does_output(); 377 378 buffer = dlg_malloc(char, max_len + 1); 379 assert_ptr(buffer, "dlg_editbox"); 380 381 thisrow = base_row = lastrow = 0; 382 383 #ifdef KEY_RESIZE 384 retry: 385 #endif 386 show_buttons = TRUE; 387 state = dialog_vars.default_button >= 0 ? dlg_default_button() : sTEXT; 388 fkey = 0; 389 390 dlg_button_layout(buttons, &mincols); 391 dlg_auto_size(title, "", &height, &width, 3 * LINES / 4, mincols); 392 dlg_print_size(height, width); 393 dlg_ctl_size(height, width); 394 395 x = dlg_box_x_ordinate(width); 396 y = dlg_box_y_ordinate(height); 397 398 dialog = dlg_new_window(height, width, y, x); 399 dlg_register_window(dialog, "editbox", binding); 400 dlg_register_buttons(dialog, "editbox", buttons); 401 402 dlg_mouse_setbase(x, y); 403 404 dlg_draw_box2(dialog, 0, 0, height, width, dialog_attr, border_attr, border2_attr); 405 dlg_draw_bottom_box2(dialog, border_attr, border2_attr, dialog_attr); 406 dlg_draw_title(dialog, title); 407 408 dlg_attrset(dialog, dialog_attr); 409 410 /* Draw the editing field in a box */ 411 box_y = MARGIN + 0; 412 box_x = MARGIN + 1; 413 box_width = width - 2 - (2 * MARGIN); 414 box_height = height - (4 * MARGIN); 415 416 dlg_draw_box(dialog, 417 box_y, 418 box_x, 419 box_height, 420 box_width, 421 border_attr, border2_attr); 422 dlg_mouse_mkbigregion(box_y + MARGIN, 423 box_x + MARGIN, 424 box_height - (2 * MARGIN), 425 box_width - (2 * MARGIN), 426 KEY_MAX, 1, 1, 3); 427 editing = dlg_sub_window(dialog, 428 box_height - (2 * MARGIN), 429 box_width - (2 * MARGIN), 430 getbegy(dialog) + box_y + 1, 431 getbegx(dialog) + box_x + 1); 432 dlg_register_window(editing, "editbox2", binding2); 433 434 show_all = TRUE; 435 show_one = FALSE; 436 pagesize = getmaxy(editing); 437 438 while (result == DLG_EXIT_UNKNOWN) { 439 int edit = 0; 440 441 if (show_all) { 442 display_all(editing, *list, thisrow, base_row, listsize, chr_offset); 443 display_one(editing, THIS_ROW, 444 thisrow, thisrow, base_row, chr_offset); 445 show_all = FALSE; 446 show_one = TRUE; 447 } else { 448 if (thisrow != lastrow) { 449 display_one(editing, (*list)[lastrow], 450 lastrow, thisrow, base_row, 0); 451 show_one = TRUE; 452 } 453 } 454 if (show_one) { 455 display_one(editing, THIS_ROW, 456 thisrow, thisrow, base_row, chr_offset); 457 getyx(editing, y, x); 458 dlg_draw_scrollbar(dialog, 459 base_row, 460 base_row, 461 base_row + pagesize, 462 listsize, 463 box_x, 464 box_x + getmaxx(editing), 465 box_y + 0, 466 box_y + getmaxy(editing) + 1, 467 border2_attr, 468 border_attr); 469 wmove(editing, y, x); 470 show_one = FALSE; 471 } 472 lastrow = thisrow; 473 input = THIS_ROW; 474 475 /* 476 * The last field drawn determines where the cursor is shown: 477 */ 478 if (show_buttons) { 479 show_buttons = FALSE; 480 UPDATE_COL(input); 481 if (state != sTEXT) { 482 display_one(editing, input, thisrow, 483 -1, base_row, 0); 484 wrefresh(editing); 485 } 486 dlg_draw_buttons(dialog, 487 height - 2, 488 0, 489 buttons, 490 (state != sTEXT) ? state : 99, 491 FALSE, 492 width); 493 if (state == sTEXT) { 494 display_one(editing, input, thisrow, 495 thisrow, base_row, chr_offset); 496 } 497 } 498 499 if (first_trace) { 500 first_trace = FALSE; 501 dlg_trace_win(dialog); 502 } 503 504 key = dlg_mouse_wgetch((state == sTEXT) ? editing : dialog, &fkey); 505 if (key == ERR) { 506 result = DLG_EXIT_ERROR; 507 break; 508 } else if (key == ESC) { 509 result = DLG_EXIT_ESC; 510 break; 511 } 512 if (state != sTEXT) { 513 if (dlg_result_key(key, fkey, &result)) 514 break; 515 } 516 517 was_mouse = (fkey && is_DLGK_MOUSE(key)); 518 if (was_mouse) 519 key -= M_EVENT; 520 521 /* 522 * Handle mouse clicks first, since we want to know if this is a 523 * button, or something that dlg_edit_string() should handle. 524 */ 525 if (fkey 526 && was_mouse 527 && (code = dlg_ok_buttoncode(key)) >= 0) { 528 result = code; 529 continue; 530 } 531 532 if (was_mouse 533 && (key >= KEY_MAX)) { 534 int wide = getmaxx(editing); 535 int cell = key - KEY_MAX; 536 int check = (cell / wide) + base_row; 537 if (check < listsize) { 538 thisrow = check; 539 col_offset = (cell % wide); 540 chr_offset = col_to_chr_offset(THIS_ROW, col_offset); 541 show_one = TRUE; 542 if (state != sTEXT) { 543 state = sTEXT; 544 show_buttons = TRUE; 545 } 546 } else { 547 beep(); 548 } 549 continue; 550 } else if (was_mouse && key >= KEY_MIN) { 551 key = dlg_lookup_key(dialog, key, &fkey); 552 } 553 554 if (state == sTEXT) { /* editing box selected */ 555 /* 556 * Intercept scrolling keys that dlg_edit_string() does not 557 * understand. 558 */ 559 if (fkey) { 560 bool moved = TRUE; 561 562 switch (key) { 563 case DLGK_GRID_UP: 564 SCROLL_TO(thisrow - 1); 565 break; 566 case DLGK_GRID_DOWN: 567 SCROLL_TO(thisrow + 1); 568 break; 569 case DLGK_PAGE_FIRST: 570 SCROLL_TO(0); 571 break; 572 case DLGK_PAGE_LAST: 573 SCROLL_TO(listsize); 574 break; 575 case DLGK_PAGE_NEXT: 576 SCROLL_TO(base_row + pagesize); 577 break; 578 case DLGK_PAGE_PREV: 579 if (thisrow > base_row) { 580 SCROLL_TO(base_row); 581 } else { 582 SCROLL_TO(base_row - pagesize); 583 } 584 break; 585 case DLGK_DELETE_LEFT: 586 if (chr_offset == 0) { 587 if (thisrow == 0) { 588 beep(); 589 } else { 590 size_t len = (strlen(THIS_ROW) + 591 strlen(PREV_ROW) + 1); 592 char *tmp = dlg_malloc(char, len); 593 594 assert_ptr(tmp, "dlg_editbox"); 595 596 chr_offset = dlg_count_wchars(PREV_ROW); 597 UPDATE_COL(PREV_ROW); 598 goal_col = col_offset; 599 600 sprintf(tmp, "%s%s", PREV_ROW, THIS_ROW); 601 if (len > max_len) 602 tmp[max_len] = '\0'; 603 604 free(PREV_ROW); 605 PREV_ROW = tmp; 606 for (y = thisrow; y < listsize; ++y) { 607 (*list)[y] = (*list)[y + 1]; 608 } 609 --listsize; 610 --thisrow; 611 SCROLL_TO(thisrow); 612 613 show_all = TRUE; 614 } 615 } else { 616 /* dlg_edit_string() can handle this case */ 617 moved = FALSE; 618 } 619 break; 620 default: 621 moved = FALSE; 622 break; 623 } 624 if (moved) { 625 if (thisrow != lastrow) { 626 if (goal_col < 0) 627 goal_col = col_offset; 628 chr_offset = col_to_chr_offset(THIS_ROW, goal_col); 629 } else { 630 UPDATE_COL(THIS_ROW); 631 } 632 continue; 633 } 634 } 635 strncpy(buffer, input, max_len - 1)[max_len - 1] = '\0'; 636 edit = dlg_edit_string(buffer, &chr_offset, key, fkey, FALSE); 637 638 if (edit) { 639 goal_col = UPDATE_COL(input); 640 if (strcmp(input, buffer)) { 641 free(input); 642 THIS_ROW = dlg_strclone(buffer); 643 input = THIS_ROW; 644 } 645 display_one(editing, input, thisrow, 646 thisrow, base_row, chr_offset); 647 continue; 648 } 649 } 650 651 /* handle non-functionkeys */ 652 if (!fkey && (code = dlg_char_to_button(key, buttons)) >= 0) { 653 dlg_del_window(dialog); 654 result = dlg_ok_buttoncode(code); 655 continue; 656 } 657 658 /* handle functionkeys */ 659 if (fkey) { 660 switch (key) { 661 case DLGK_GRID_UP: 662 case DLGK_GRID_LEFT: 663 case DLGK_FIELD_PREV: 664 show_buttons = TRUE; 665 state = dlg_prev_ok_buttonindex(state, sTEXT); 666 break; 667 case DLGK_GRID_RIGHT: 668 case DLGK_GRID_DOWN: 669 case DLGK_FIELD_NEXT: 670 show_buttons = TRUE; 671 state = dlg_next_ok_buttonindex(state, sTEXT); 672 break; 673 case DLGK_ENTER: 674 if (state == sTEXT) { 675 const int *indx = dlg_index_wchars(THIS_ROW); 676 int split = indx[chr_offset]; 677 char *tmp = dlg_strclone(THIS_ROW + split); 678 679 assert_ptr(tmp, "dlg_editbox"); 680 grow_list(list, rows, listsize + 1); 681 ++listsize; 682 for (y = listsize; y > thisrow; --y) { 683 (*list)[y] = (*list)[y - 1]; 684 } 685 THIS_ROW[split] = '\0'; 686 ++thisrow; 687 chr_offset = 0; 688 col_offset = 0; 689 THIS_ROW = tmp; 690 SCROLL_TO(thisrow); 691 show_all = TRUE; 692 } else { 693 result = dlg_ok_buttoncode(state); 694 } 695 break; 696 #ifdef KEY_RESIZE 697 case KEY_RESIZE: 698 dlg_will_resize(dialog); 699 /* reset data */ 700 height = old_height; 701 width = old_width; 702 dlg_clear(); 703 dlg_unregister_window(editing); 704 dlg_del_window(editing); 705 dlg_del_window(dialog); 706 dlg_mouse_free_regions(); 707 /* repaint */ 708 goto retry; 709 #endif 710 case DLGK_TOGGLE: 711 if (state != sTEXT) { 712 result = dlg_ok_buttoncode(state); 713 } else { 714 beep(); 715 } 716 break; 717 default: 718 beep(); 719 break; 720 } 721 } else { 722 beep(); 723 } 724 } 725 726 dlg_unregister_window(editing); 727 dlg_del_window(editing); 728 dlg_del_window(dialog); 729 dlg_mouse_free_regions(); 730 731 /* 732 * The caller's copy of the (*list)[] array has been updated, but for 733 * consistency with the other widgets, we put the "real" result in 734 * the output buffer. 735 */ 736 if (result == DLG_EXIT_OK) { 737 int n; 738 for (n = 0; n < listsize; ++n) { 739 dlg_add_result((*list)[n]); 740 dlg_add_separator(); 741 } 742 dlg_add_last_key(-1); 743 } 744 free(buffer); 745 dlg_restore_vars(&save_vars); 746 return result; 747 } 748 749 int 750 dialog_editbox(const char *title, const char *file, int height, int width) 751 { 752 int result; 753 char **list; 754 int rows; 755 756 load_list(file, &list, &rows); 757 result = dlg_editbox(title, &list, &rows, height, width); 758 free_list(&list, &rows); 759 return result; 760 } 761