1 /* 2 * $Id: editbox.c,v 1.85 2022/04/06 08:01:23 tom Exp $ 3 * 4 * editbox.c -- implements the edit box 5 * 6 * Copyright 2007-2020,2022 Thomas E. Dickey 7 * 8 * This program is free software; you can redistribute it and/or modify 9 * it under the terms of the GNU Lesser General Public License, version 2.1 10 * as published by the Free Software Foundation. 11 * 12 * This program is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 * Lesser General Public License for more details. 16 * 17 * You should have received a copy of the GNU Lesser General Public 18 * License along with this program; if not, write to 19 * Free Software Foundation, Inc. 20 * 51 Franklin St., Fifth Floor 21 * Boston, MA 02110, USA. 22 */ 23 24 #include <dlg_internals.h> 25 #include <dlg_keys.h> 26 27 #define sTEXT -1 28 29 static void 30 fail_list(void) 31 { 32 dlg_exiterr("File too large"); 33 } 34 35 static void 36 grow_list(char ***list, int *have, int want) 37 { 38 if (want > *have) { 39 size_t last = (size_t) *have; 40 size_t need = (size_t) (want | 31) + 3; 41 *have = (int) need; 42 (*list) = dlg_realloc(char *, need, *list); 43 if ((*list) == 0) { 44 fail_list(); 45 } else { 46 while (++last < need) { 47 (*list)[last] = 0; 48 } 49 } 50 } 51 } 52 53 static void 54 load_list(const char *file, char ***list, int *rows) 55 { 56 char *blob = 0; 57 struct stat sb; 58 size_t size; 59 60 *list = 0; 61 *rows = 0; 62 63 if (stat(file, &sb) < 0 || 64 (sb.st_mode & S_IFMT) != S_IFREG) 65 dlg_exiterr("Not a file: %s", file); 66 67 size = (size_t) sb.st_size; 68 if ((blob = dlg_malloc(char, size + 2)) == 0) { 69 fail_list(); 70 } else { 71 FILE *fp; 72 unsigned n, pass; 73 74 blob[size] = '\0'; 75 76 if ((fp = fopen(file, "r")) == 0) 77 dlg_exiterr("Cannot open: %s", file); 78 size = fread(blob, sizeof(char), size, fp); 79 fclose(fp); 80 81 /* 82 * If the file is not empty, ensure that it ends with a newline. 83 */ 84 if (size != 0 && blob[size - 1] != '\n') { 85 blob[++size - 1] = '\n'; 86 blob[size] = '\0'; 87 } 88 89 for (pass = 0; pass < 2; ++pass) { 90 int first = TRUE; 91 unsigned need = 0; 92 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) scroll_to(pagesize, listsize, &base_row, &thisrow, target) 277 #define SCROLL_TO(target) show_all = Scroll_To(target) 278 279 #define PREV_ROW (*list)[thisrow - 1] 280 #define THIS_ROW (*list)[thisrow] 281 #define NEXT_ROW (*list)[thisrow + 1] 282 283 #define UPDATE_COL(input) col_offset = dlg_edit_offset(input, chr_offset, box_width) 284 285 static int 286 widest_line(char **list) 287 { 288 int result = dlg_max_input(-1); 289 290 if (list != 0) { 291 char *value; 292 293 while ((value = *list++) != 0) { 294 int check = (int) strlen(value); 295 if (check > result) 296 result = check; 297 } 298 } 299 return result; 300 } 301 302 #define NAVIGATE_BINDINGS \ 303 DLG_KEYS_DATA( DLGK_GRID_DOWN, KEY_DOWN ), \ 304 DLG_KEYS_DATA( DLGK_GRID_RIGHT, KEY_RIGHT ), \ 305 DLG_KEYS_DATA( DLGK_GRID_LEFT, KEY_LEFT ), \ 306 DLG_KEYS_DATA( DLGK_GRID_UP, KEY_UP ), \ 307 DLG_KEYS_DATA( DLGK_FIELD_NEXT, TAB ), \ 308 DLG_KEYS_DATA( DLGK_FIELD_PREV, KEY_BTAB ), \ 309 DLG_KEYS_DATA( DLGK_PAGE_FIRST, KEY_HOME ), \ 310 DLG_KEYS_DATA( DLGK_PAGE_LAST, KEY_END ), \ 311 DLG_KEYS_DATA( DLGK_PAGE_LAST, KEY_LL ), \ 312 DLG_KEYS_DATA( DLGK_PAGE_NEXT, KEY_NPAGE ), \ 313 DLG_KEYS_DATA( DLGK_PAGE_NEXT, DLGK_MOUSE(KEY_NPAGE) ), \ 314 DLG_KEYS_DATA( DLGK_PAGE_PREV, KEY_PPAGE ), \ 315 DLG_KEYS_DATA( DLGK_PAGE_PREV, DLGK_MOUSE(KEY_PPAGE) ) 316 /* 317 * Display a dialog box for editing a copy of a file 318 */ 319 int 320 dlg_editbox(const char *title, 321 char ***list, 322 int *rows, 323 int height, 324 int width) 325 { 326 /* *INDENT-OFF* */ 327 static DLG_KEYS_BINDING binding[] = { 328 HELPKEY_BINDINGS, 329 ENTERKEY_BINDINGS, 330 NAVIGATE_BINDINGS, 331 TOGGLEKEY_BINDINGS, 332 END_KEYS_BINDING 333 }; 334 static DLG_KEYS_BINDING binding2[] = { 335 INPUTSTR_BINDINGS, 336 HELPKEY_BINDINGS, 337 ENTERKEY_BINDINGS, 338 NAVIGATE_BINDINGS, 339 /* no TOGGLEKEY_BINDINGS, since that includes space... */ 340 END_KEYS_BINDING 341 }; 342 /* *INDENT-ON* */ 343 344 #ifdef KEY_RESIZE 345 int old_height = height; 346 int old_width = width; 347 #endif 348 int x, y, box_y, box_x, box_height, box_width; 349 int show_buttons; 350 int thisrow, base_row, lastrow; 351 int goal_col = -1; 352 int col_offset = 0; 353 int chr_offset = 0; 354 int key, fkey, code; 355 int pagesize; 356 int listsize = size_list(*list); 357 int result = DLG_EXIT_UNKNOWN; 358 int state; 359 size_t max_len = (size_t) dlg_max_input(widest_line(*list)); 360 char *buffer; 361 bool show_all, show_one; 362 bool first_trace = TRUE; 363 WINDOW *dialog; 364 WINDOW *editing; 365 DIALOG_VARS save_vars; 366 const char **buttons = dlg_ok_labels(); 367 int mincols = (3 * COLS / 4); 368 369 DLG_TRACE(("# editbox args:\n")); 370 DLG_TRACE2S("title", title); 371 /* FIXME dump the rows & list */ 372 DLG_TRACE2N("height", height); 373 DLG_TRACE2N("width", width); 374 375 dlg_save_vars(&save_vars); 376 dialog_vars.separate_output = TRUE; 377 378 dlg_does_output(); 379 380 buffer = dlg_malloc(char, max_len + 1); 381 assert_ptr(buffer, "dlg_editbox"); 382 383 thisrow = base_row = lastrow = 0; 384 385 #ifdef KEY_RESIZE 386 retry: 387 #endif 388 show_buttons = TRUE; 389 state = dialog_vars.default_button >= 0 ? dlg_default_button() : sTEXT; 390 fkey = 0; 391 392 dlg_button_layout(buttons, &mincols); 393 dlg_auto_size(title, "", &height, &width, 3 * LINES / 4, mincols); 394 dlg_print_size(height, width); 395 dlg_ctl_size(height, width); 396 397 x = dlg_box_x_ordinate(width); 398 y = dlg_box_y_ordinate(height); 399 400 dialog = dlg_new_window(height, width, y, x); 401 dlg_register_window(dialog, "editbox", binding); 402 dlg_register_buttons(dialog, "editbox", buttons); 403 404 dlg_mouse_setbase(x, y); 405 406 dlg_draw_box2(dialog, 0, 0, height, width, dialog_attr, border_attr, border2_attr); 407 dlg_draw_bottom_box2(dialog, border_attr, border2_attr, dialog_attr); 408 dlg_draw_title(dialog, title); 409 410 dlg_attrset(dialog, dialog_attr); 411 412 /* Draw the editing field in a box */ 413 box_y = MARGIN + 0; 414 box_x = MARGIN + 1; 415 box_width = width - 2 - (2 * MARGIN); 416 box_height = height - (4 * MARGIN); 417 418 dlg_draw_box(dialog, 419 box_y, 420 box_x, 421 box_height, 422 box_width, 423 border_attr, border2_attr); 424 dlg_mouse_mkbigregion(box_y + MARGIN, 425 box_x + MARGIN, 426 box_height - (2 * MARGIN), 427 box_width - (2 * MARGIN), 428 KEY_MAX, 1, 1, 3); 429 editing = dlg_sub_window(dialog, 430 box_height - (2 * MARGIN), 431 box_width - (2 * MARGIN), 432 getbegy(dialog) + box_y + 1, 433 getbegx(dialog) + box_x + 1); 434 dlg_register_window(editing, "editbox2", binding2); 435 436 show_all = TRUE; 437 show_one = FALSE; 438 pagesize = getmaxy(editing); 439 440 while (result == DLG_EXIT_UNKNOWN) { 441 bool was_mouse; 442 char *input; 443 444 if (show_all) { 445 display_all(editing, *list, thisrow, base_row, listsize, chr_offset); 446 display_one(editing, THIS_ROW, 447 thisrow, thisrow, base_row, chr_offset); 448 show_all = FALSE; 449 show_one = TRUE; 450 } else { 451 if (thisrow != lastrow) { 452 display_one(editing, (*list)[lastrow], 453 lastrow, thisrow, base_row, 0); 454 show_one = TRUE; 455 } 456 } 457 if (show_one) { 458 display_one(editing, THIS_ROW, 459 thisrow, thisrow, base_row, chr_offset); 460 getyx(editing, y, x); 461 dlg_draw_scrollbar(dialog, 462 base_row, 463 base_row, 464 base_row + pagesize, 465 listsize, 466 box_x, 467 box_x + getmaxx(editing), 468 box_y + 0, 469 box_y + getmaxy(editing) + 1, 470 border2_attr, 471 border_attr); 472 wmove(editing, y, x); 473 show_one = FALSE; 474 } 475 lastrow = thisrow; 476 input = THIS_ROW; 477 478 /* 479 * The last field drawn determines where the cursor is shown: 480 */ 481 if (show_buttons) { 482 show_buttons = FALSE; 483 UPDATE_COL(input); 484 if (state != sTEXT) { 485 display_one(editing, input, thisrow, 486 -1, base_row, 0); 487 wrefresh(editing); 488 } 489 dlg_draw_buttons(dialog, 490 height - 2, 491 0, 492 buttons, 493 (state != sTEXT) ? state : 99, 494 FALSE, 495 width); 496 if (state == sTEXT) { 497 display_one(editing, input, thisrow, 498 thisrow, base_row, chr_offset); 499 } 500 } 501 502 if (first_trace) { 503 first_trace = FALSE; 504 dlg_trace_win(dialog); 505 } 506 507 key = dlg_mouse_wgetch((state == sTEXT) ? editing : dialog, &fkey); 508 if (key == ERR) { 509 result = DLG_EXIT_ERROR; 510 break; 511 } else if (key == ESC) { 512 result = DLG_EXIT_ESC; 513 break; 514 } 515 if (state != sTEXT) { 516 if (dlg_result_key(key, fkey, &result)) { 517 if (!dlg_button_key(result, &code, &key, &fkey)) 518 break; 519 } 520 } 521 522 was_mouse = (fkey && is_DLGK_MOUSE(key)); 523 if (was_mouse) 524 key -= M_EVENT; 525 526 /* 527 * Handle mouse clicks first, since we want to know if this is a 528 * button, or something that dlg_edit_string() should handle. 529 */ 530 if (fkey 531 && was_mouse 532 && (code = dlg_ok_buttoncode(key)) >= 0) { 533 result = code; 534 continue; 535 } 536 537 if (was_mouse 538 && (key >= KEY_MAX)) { 539 int wide = getmaxx(editing); 540 int cell = key - KEY_MAX; 541 int check = (cell / wide) + base_row; 542 if (check < listsize) { 543 thisrow = check; 544 col_offset = (cell % wide); 545 chr_offset = col_to_chr_offset(THIS_ROW, col_offset); 546 show_one = TRUE; 547 if (state != sTEXT) { 548 state = sTEXT; 549 show_buttons = TRUE; 550 } 551 } else { 552 beep(); 553 } 554 continue; 555 } else if (was_mouse && key >= KEY_MIN) { 556 key = dlg_lookup_key(dialog, key, &fkey); 557 } 558 559 if (state == sTEXT) { /* editing box selected */ 560 int edit = 0; 561 562 /* 563 * Intercept scrolling keys that dlg_edit_string() does not 564 * understand. 565 */ 566 if (fkey) { 567 bool moved = TRUE; 568 569 switch (key) { 570 case DLGK_GRID_UP: 571 SCROLL_TO(thisrow - 1); 572 break; 573 case DLGK_GRID_DOWN: 574 SCROLL_TO(thisrow + 1); 575 break; 576 case DLGK_PAGE_FIRST: 577 SCROLL_TO(0); 578 break; 579 case DLGK_PAGE_LAST: 580 SCROLL_TO(listsize); 581 break; 582 case DLGK_PAGE_NEXT: 583 SCROLL_TO(base_row + pagesize); 584 break; 585 case DLGK_PAGE_PREV: 586 if (thisrow > base_row) { 587 SCROLL_TO(base_row); 588 } else { 589 SCROLL_TO(base_row - pagesize); 590 } 591 break; 592 case DLGK_DELETE_LEFT: 593 if (chr_offset == 0) { 594 if (thisrow == 0) { 595 beep(); 596 } else { 597 size_t len = (strlen(THIS_ROW) + 598 strlen(PREV_ROW) + 1); 599 char *tmp = dlg_malloc(char, len); 600 601 assert_ptr(tmp, "dlg_editbox"); 602 603 chr_offset = dlg_count_wchars(PREV_ROW); 604 UPDATE_COL(PREV_ROW); 605 goal_col = col_offset; 606 607 sprintf(tmp, "%s%s", PREV_ROW, THIS_ROW); 608 if (len > max_len) 609 tmp[max_len] = '\0'; 610 611 free(PREV_ROW); 612 PREV_ROW = tmp; 613 for (y = thisrow; y < listsize; ++y) { 614 (*list)[y] = (*list)[y + 1]; 615 } 616 --listsize; 617 --thisrow; 618 (void) Scroll_To(thisrow); 619 620 show_all = TRUE; 621 } 622 } else { 623 /* dlg_edit_string() can handle this case */ 624 moved = FALSE; 625 } 626 break; 627 default: 628 moved = FALSE; 629 break; 630 } 631 if (moved) { 632 if (thisrow != lastrow) { 633 if (goal_col < 0) 634 goal_col = col_offset; 635 chr_offset = col_to_chr_offset(THIS_ROW, goal_col); 636 } else { 637 UPDATE_COL(THIS_ROW); 638 } 639 continue; 640 } 641 } 642 strncpy(buffer, input, max_len - 1)[max_len - 1] = '\0'; 643 if (chr_offset > (int) (max_len - 1)) 644 chr_offset = (int) (max_len - 1); 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