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