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