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