The Birdfont Source Code


All Repositories / birdfont.git / blob – RSS feed

TextArea.vala in libbirdfont/Renderer

This file is a part of the Birdfont project.

Contributing

Send patches or pull requests to johan.mattsson.m@gmail.com.
Clone this repository: git clone https://github.com/johanmattssonm/birdfont.git

Revisions

View the latest version of libbirdfont/Renderer/TextArea.vala.
Switch keyboard focus with tab key in name and description tab
1 /* 2 Copyright (C) 2014 Johan Mattsson 3 4 This library is free software; you can redistribute it and/or modify 5 it under the terms of the GNU Lesser General Public License as 6 published by the Free Software Foundation; either version 3 of the 7 License, or (at your option) any later version. 8 9 This library is distributed in the hope that it will be useful, but 10 WITHOUT ANY WARRANTY; without even the implied warranty of 11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 Lesser General Public License for more details. 13 */ 14 15 using Cairo; 16 using Math; 17 18 namespace BirdFont { 19 20 public class TextArea : Widget { 21 22 public double min_width = 500; 23 public double min_height = 100; 24 public double font_size; 25 public double padding = 3.3; 26 public bool single_line = false; 27 28 public bool draw_carret { 29 get { return carret_is_visible; } 30 set { 31 carret_is_visible = value; 32 if (!value) { 33 update_selection = false; 34 selection_end = carret.copy (); 35 } 36 } 37 } 38 public bool carret_is_visible = false; 39 public bool draw_border = true; 40 41 public double width; 42 public double height; 43 44 Carret carret = new Carret (); 45 Carret selection_end = new Carret (); 46 bool update_selection = false; 47 public bool show_selection = false; 48 49 public signal void scroll (double pixels); 50 public signal void text_changed (string text); 51 public signal void enter (string text); 52 53 Gee.ArrayList<Paragraph> paragraphs = new Gee.ArrayList<Paragraph> (); 54 private static const int DONE = -2; 55 56 int last_paragraph = 0; 57 string text; 58 int text_length; 59 60 Gee.ArrayList<TextUndoItem> undo_items = new Gee.ArrayList<TextUndoItem> (); 61 Gee.ArrayList<TextUndoItem> redo_items = new Gee.ArrayList<TextUndoItem> (); 62 63 bool store_undo_state_at_next_event = false; 64 65 public bool editable; 66 public bool use_cache = true; 67 68 public TextArea (double font_size = 20) { 69 this.font_size = font_size; 70 width = min_width; 71 height = min_height; 72 editable = true; 73 } 74 75 public override double get_height () { 76 return height + 2 * padding; 77 } 78 79 public override double get_width () { 80 return width + 2 * padding; 81 } 82 83 public void set_font_size (double z) { 84 font_size = z; 85 } 86 87 bool generate_paragraphs () { 88 Paragraph paragraph; 89 90 int next_paragraph = -1; 91 92 if (last_paragraph == DONE) { 93 return false; 94 } 95 96 next_paragraph = text.index_of ("\n", last_paragraph); 97 98 if (next_paragraph == -1) { 99 paragraph = new Paragraph (text.substring (last_paragraph), font_size, paragraphs.size); 100 paragraphs.add (paragraph); 101 last_paragraph = DONE; 102 } else { 103 next_paragraph += "\n".length; 104 paragraph = new Paragraph (text.substring (last_paragraph, next_paragraph - last_paragraph), font_size, paragraphs.size); 105 paragraphs.add (paragraph); 106 last_paragraph = next_paragraph; 107 } 108 109 return last_paragraph != DONE; 110 } 111 112 void generate_all_paragraphs () { 113 while (generate_paragraphs ()) { 114 } 115 } 116 117 public void key_press (uint keyval) { 118 unichar c; 119 TextUndoItem ui; 120 121 if (!editable) { 122 return; 123 } 124 125 c = (unichar) keyval; 126 127 switch (c) { 128 case ' ': 129 store_undo_edit_state (); 130 add_character (keyval); 131 break; 132 case 'a': 133 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) { 134 select_all (); 135 } else { 136 add_character (keyval); 137 } 138 break; 139 case 'c': 140 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) { 141 ClipTool.copy_text (this); 142 } else { 143 add_character (keyval); 144 } 145 break; 146 case 'v': 147 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) { 148 ClipTool.paste_text (this); 149 store_undo_state_at_next_event = true; 150 } else { 151 add_character (keyval); 152 } 153 break; 154 case 'y': 155 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) { 156 redo (); 157 } else { 158 add_character (keyval); 159 } 160 break; 161 case 'z': 162 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) { 163 undo (); 164 } else { 165 add_character (keyval); 166 } 167 break; 168 case Key.RIGHT: 169 check_selection (); 170 move_carret_next (); 171 break; 172 case Key.LEFT: 173 check_selection (); 174 move_carret_previous (); 175 break; 176 case Key.DOWN: 177 check_selection (); 178 move_carret_next_row (); 179 break; 180 case Key.UP: 181 check_selection (); 182 move_carret_previous_row (); 183 break; 184 case Key.END: 185 check_selection (); 186 move_carret_to_end_of_line (); 187 break; 188 case Key.HOME: 189 check_selection (); 190 move_carret_to_beginning_of_line (); 191 break; 192 case Key.BACK_SPACE: 193 if (has_selection ()) { 194 ui = delete_selected_text (); 195 undo_items.add (ui); 196 redo_items.clear (); 197 store_undo_state_at_next_event = true; 198 } else { 199 ui = remove_last_character (); 200 undo_items.add (ui); 201 redo_items.clear (); 202 store_undo_state_at_next_event = true; 203 } 204 text_changed (get_text ()); 205 break; 206 case Key.ENTER: 207 store_undo_edit_state (); 208 insert_text ("\n"); 209 210 if (single_line) { 211 enter (get_text ()); 212 } 213 break; 214 case Key.DEL: 215 if (has_selection ()) { 216 ui = delete_selected_text (); 217 undo_items.add (ui); 218 redo_items.clear (); 219 store_undo_state_at_next_event = true; 220 } else { 221 ui = remove_next_character (); 222 undo_items.add (ui); 223 redo_items.clear (); 224 store_undo_state_at_next_event = true; 225 } 226 text_changed (get_text ()); 227 break; 228 default: 229 add_character (keyval); 230 break; 231 } 232 233 GlyphCanvas.redraw (); 234 } 235 236 void check_selection () { 237 if (!has_selection () && KeyBindings.has_shift ()) { 238 show_selection = true; 239 selection_end = carret.copy (); 240 } 241 242 if (!KeyBindings.has_shift ()) { 243 show_selection = false; 244 } 245 } 246 247 private void add_character (uint keyval) { 248 unichar c = (unichar) keyval; 249 string s; 250 251 if (!is_modifier_key (keyval) 252 && !KeyBindings.has_ctrl () 253 && !KeyBindings.has_alt ()) { 254 255 s = (!) c.to_string (); 256 257 if (s.validate ()) { 258 if (store_undo_state_at_next_event) { 259 store_undo_edit_state (); 260 store_undo_state_at_next_event = false; 261 } 262 263 insert_text (s); 264 } 265 } 266 } 267 268 Paragraph get_current_paragraph () { 269 Paragraph p; 270 271 if (unlikely (!(0 <= carret.paragraph < paragraphs.size))) { 272 warning (@"No paragraph, index: $(carret.paragraph), size: $(paragraphs.size)"); 273 p = new Paragraph ("", 0, 0); 274 paragraphs.add (p); 275 return p; 276 } 277 278 p = paragraphs.get (carret.paragraph); 279 return p; 280 } 281 282 public void set_text (string t) { 283 int tl; 284 285 if (single_line) { 286 text = t.replace ("\n", "").replace ("\r", ""); 287 } else { 288 text = t; 289 } 290 291 tl = t.length; 292 text_length += tl; 293 294 paragraphs.clear (); 295 generate_paragraphs (); 296 297 return_if_fail (paragraphs.size != 0); 298 299 carret.paragraph = paragraphs.size - 1; 300 carret.character_index = paragraphs.get (paragraphs.size - 1).text.length; 301 selection_end = carret.copy (); 302 show_selection = false; 303 304 text_changed (get_text ()); 305 } 306 307 Carret get_selection_start () { 308 if (carret.paragraph == selection_end.paragraph) { 309 return carret.character_index < selection_end.character_index ? carret : selection_end; 310 } 311 312 return carret.paragraph < selection_end.paragraph ? carret : selection_end; 313 } 314 315 Carret get_selection_stop () { 316 if (carret.paragraph == selection_end.paragraph) { 317 return carret.character_index > selection_end.character_index ? carret : selection_end; 318 } 319 320 return carret.paragraph > selection_end.paragraph ? carret : selection_end; 321 } 322 323 public string get_selected_text () { 324 Carret selection_start, selection_stop; 325 int i; 326 Paragraph pg; 327 StringBuilder sb; 328 329 sb = new StringBuilder (); 330 331 if (!has_selection ()) { 332 return "".dup (); 333 } 334 335 selection_start = get_selection_start (); 336 selection_stop = get_selection_stop (); 337 338 if (selection_start.paragraph == selection_stop.paragraph) { 339 pg = paragraphs.get (selection_start.paragraph); 340 return pg.text.substring (selection_start.character_index, selection_stop.character_index - selection_start.character_index); 341 } 342 343 pg = paragraphs.get (selection_start.paragraph); 344 sb.append (pg.text.substring (selection_start.character_index)); 345 346 for (i = selection_start.paragraph + 1; i < selection_stop.paragraph; i++) { 347 return_if_fail (0 <= i < paragraphs.size); 348 pg = paragraphs.get (i); 349 sb.append (pg.text); 350 } 351 352 pg = paragraphs.get (selection_stop.paragraph); 353 sb.append (pg.text.substring (0, selection_stop.character_index)); 354 355 return sb.str; 356 } 357 358 public void select_all () { 359 while (last_paragraph != DONE) { 360 generate_paragraphs (); 361 } 362 363 if (paragraphs.size > 0) { 364 carret.paragraph = 0; 365 carret.character_index = 0; 366 selection_end.paragraph = paragraphs.size - 1; 367 selection_end.character_index = paragraphs.get (paragraphs.size - 1).text_length; 368 show_selection = true; 369 } 370 } 371 372 public TextUndoItem delete_selected_text () { 373 Carret selection_start, selection_stop; 374 int i; 375 Paragraph pg, pge; 376 string e, s, n; 377 bool same; 378 TextUndoItem ui; 379 380 ui = new TextUndoItem (carret); 381 382 e = ""; 383 s = ""; 384 n = ""; 385 386 if (!has_selection ()) { 387 warning ("No selected text."); 388 return ui; 389 } 390 391 selection_start = get_selection_start (); 392 selection_stop = get_selection_stop (); 393 394 same = selection_start.paragraph == selection_stop.paragraph; 395 396 if (!same) { 397 return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui); 398 pg = paragraphs.get (selection_start.paragraph); 399 s = pg.text.substring (0, selection_start.character_index); 400 401 return_val_if_fail (0 <= selection_stop.paragraph < paragraphs.size, ui); 402 pge = paragraphs.get (selection_stop.paragraph); 403 e = pge.text.substring (selection_stop.character_index); 404 405 if (!s.has_suffix ("\n")) { 406 ui.deleted.add (pge.copy ()); 407 ui.edited.add (pg.copy ()); 408 409 pg.set_text (s + e); 410 pge.set_text (""); 411 } else { 412 ui.edited.add (pg.copy ()); 413 ui.edited.add (pge.copy ()); 414 415 pg.set_text (s); 416 pge.set_text (e); 417 } 418 } else { 419 return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui); 420 421 pg = paragraphs.get (selection_start.paragraph); 422 n = pg.text.substring (0, selection_start.character_index); 423 n += pg.text.substring (selection_stop.character_index); 424 425 if (n == "") { 426 ui.deleted.add (pg.copy ()); 427 paragraphs.remove_at (selection_start.paragraph); 428 } else { 429 ui.edited.add (pg.copy ()); 430 } 431 432 pg.set_text (n); 433 } 434 435 if (e == "" && !same) { 436 paragraphs.remove_at (selection_stop.paragraph); 437 } 438 439 for (i = selection_stop.paragraph - 1; i > selection_start.paragraph; i--) { 440 return_val_if_fail (0 <= i < paragraphs.size, ui); 441 ui.deleted.add (paragraphs.get (i)); 442 paragraphs.remove_at (i); 443 } 444 445 if (s == "" && !same) { 446 return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui); 447 paragraphs.remove_at (selection_start.paragraph); 448 } 449 450 carret = selection_start.copy (); 451 selection_end = carret.copy (); 452 453 show_selection = false; 454 update_paragraph_index (); 455 layout (); 456 457 return ui; 458 } 459 460 void update_paragraph_index () { 461 int i = 0; 462 foreach (Paragraph p in paragraphs) { 463 p.index = i; 464 i++; 465 } 466 } 467 468 public TextUndoItem remove_last_character () { 469 TextUndoItem ui; 470 move_carret_previous (); 471 ui = remove_next_character (); 472 return ui; 473 } 474 475 public TextUndoItem remove_next_character () { 476 Paragraph paragraph; 477 Paragraph next_paragraph; 478 int index; 479 unichar c; 480 string np; 481 TextUndoItem ui; 482 483 ui = new TextUndoItem (carret); 484 485 return_val_if_fail (0 <= carret.paragraph < paragraphs.size, ui); 486 paragraph = paragraphs.get (carret.paragraph); 487 488 index = carret.character_index; 489 490 paragraph.text.get_next_char (ref index, out c); 491 492 if (index >= paragraph.text_length) { 493 np = paragraph.text.substring (0, carret.character_index); 494 495 if (carret.paragraph + 1 < paragraphs.size) { 496 next_paragraph = paragraphs.get (carret.paragraph + 1); 497 paragraphs.remove_at (carret.paragraph + 1); 498 499 np = np + next_paragraph.text; 500 501 ui.deleted.add (next_paragraph); 502 } 503 504 paragraph.set_text (np); 505 ui.edited.add (paragraph); 506 } else { 507 np = paragraph.text.substring (0, carret.character_index) + paragraph.text.substring (index); 508 paragraph.set_text (np); 509 510 if (np == "") { 511 return_if_fail (carret.paragraph > 0); 512 carret.paragraph--; 513 paragraph = paragraphs.get (carret.paragraph); 514 carret.character_index = paragraph.text_length; 515 516 ui.deleted.add (paragraphs.get (carret.paragraph + 1)); 517 518 paragraphs.remove_at (carret.paragraph + 1); 519 } else { 520 ui.edited.add (paragraph); 521 } 522 } 523 524 update_paragraph_index (); 525 layout (); 526 527 return ui; 528 } 529 530 public void move_carret_next () { 531 unichar c; 532 533 move_carret_one_character (); 534 535 if (KeyBindings.has_ctrl ()) { 536 while (true) { 537 c = move_carret_one_character (); 538 539 if (c == '\0' || c == ' ') { 540 break; 541 } 542 } 543 } 544 } 545 546 unichar move_carret_one_character () { 547 Paragraph paragraph; 548 int index; 549 unichar c; 550 551 return_if_fail (0 <= carret.paragraph < paragraphs.size); 552 paragraph = paragraphs.get (carret.paragraph); 553 554 index = carret.character_index; 555 556 paragraph.text.get_next_char (ref index, out c); 557 558 if (index >= paragraph.text_length && carret.paragraph + 1 < paragraphs.size) { 559 carret.paragraph++; 560 carret.character_index = 0; 561 c = ' '; 562 } else { 563 carret.character_index = index; 564 } 565 566 return c; 567 } 568 569 public void move_carret_previous () { 570 unichar c; 571 572 move_carret_back_one_character (); 573 574 if (KeyBindings.has_ctrl ()) { 575 while (true) { 576 c = move_carret_back_one_character (); 577 578 if (c == '\0' || c == ' ') { 579 break; 580 } 581 } 582 } 583 } 584 585 unichar move_carret_back_one_character () { 586 Paragraph paragraph; 587 int index, last_index; 588 unichar c; 589 590 return_if_fail (0 <= carret.paragraph < paragraphs.size); 591 paragraph = paragraphs.get (carret.paragraph); 592 593 index = 0; 594 last_index = -1; 595 596 while (paragraph.text.get_next_char (ref index, out c) && index < carret.character_index) { 597 last_index = index; 598 } 599 600 if (last_index <= 0 && carret.paragraph > 0) { 601 carret.paragraph--; 602 603 return_if_fail (0 <= carret.paragraph < paragraphs.size); 604 paragraph = paragraphs.get (carret.paragraph); 605 carret.character_index = paragraph.text_length; 606 607 if (paragraph.text.has_suffix ("\n")) { 608 carret.character_index -= "\n".length; 609 } 610 611 c = ' '; 612 } else if (last_index > 0) { 613 carret.character_index = last_index; 614 } else { 615 carret.character_index = 0; 616 c = ' '; 617 } 618 619 return_if_fail (0 <= carret.paragraph < paragraphs.size); 620 621 return c; 622 } 623 624 public void move_carret_next_row () { 625 double nr = font_size; 626 627 if (carret.desired_y + 2 * font_size >= allocation.height) { 628 scroll (2 * font_size); 629 nr = -font_size; 630 } 631 632 if (carret.desired_y + nr < widget_y + height - padding) { 633 carret = get_carret_at (carret.desired_x - widget_x - padding, carret.desired_y + nr); 634 } 635 } 636 637 public void move_carret_to_end_of_line () { 638 carret = get_carret_at (widget_x + padding + width, carret.desired_y, false); 639 } 640 641 public void move_carret_to_beginning_of_line () { 642 carret = get_carret_at (0, carret.desired_y, false); 643 } 644 645 646 public void move_carret_previous_row () { 647 double nr = -font_size; 648 649 if (carret.desired_y - 2 * font_size < 0) { 650 scroll (-2 * font_size); 651 nr = font_size; 652 } 653 654 if (carret.desired_y + nr > widget_y + padding) { 655 carret = get_carret_at (carret.desired_x, carret.desired_y + nr); 656 } 657 } 658 659 public bool has_selection () { 660 return show_selection && selection_is_visible (); 661 } 662 663 private bool selection_is_visible () { 664 return carret.paragraph != selection_end.paragraph || carret.character_index != selection_end.character_index; 665 } 666 667 public void insert_text (string t) { 668 string s; 669 Paragraph paragraph; 670 TextUndoItem ui; 671 Gee.ArrayList<string> pgs; 672 bool u = false; 673 674 pgs = new Gee.ArrayList<string> (); 675 676 if (single_line) { 677 s = t.replace ("\n", "").replace ("\r", ""); 678 pgs.add (s); 679 } else { 680 if (t.last_index_of ("\n") > 0) { 681 string[] parts = t.split ("\n"); 682 int i; 683 for (i = 0; i < parts.length -1; i++) { 684 pgs.add (parts[i]); 685 pgs.add ("\n"); 686 } 687 688 pgs.add (parts[parts.length - 1]); 689 690 if (t.has_suffix ("\n")) { 691 pgs.add ("\n"); 692 } 693 } else { 694 s = t; 695 pgs.add (s); 696 } 697 } 698 699 if (has_selection () && show_selection) { 700 ui = delete_selected_text (); 701 u = true; 702 703 if (paragraphs.size == 0) { 704 paragraphs.add (new Paragraph ("", font_size, 0)); 705 } 706 } else { 707 ui = new TextUndoItem (carret); 708 } 709 710 return_if_fail (0 <= carret.paragraph < paragraphs.size); 711 paragraph = paragraphs.get (carret.paragraph); 712 713 if (pgs.size > 0) { 714 if (!u) { 715 ui.edited.add (paragraph.copy ()); 716 } 717 718 string first = pgs.get (0); 719 720 string end; 721 string nt = paragraph.text.substring (0, carret.character_index); 722 723 nt += first; 724 end = paragraph.text.substring (carret.character_index); 725 726 paragraph.set_text (nt); 727 728 int paragraph_index = carret.paragraph; 729 Paragraph next_paragraph = paragraph; 730 for (int i = 1; i < pgs.size; i++) { 731 paragraph_index++; 732 string next = pgs.get (i); 733 next_paragraph = new Paragraph (next, font_size, paragraph_index); 734 paragraphs.insert (paragraph_index, next_paragraph); 735 ui.added.add (next_paragraph); 736 u = true; 737 } 738 739 carret.paragraph = paragraph_index; 740 carret.character_index = next_paragraph.text.length; 741 742 next_paragraph.set_text (next_paragraph.text + end); 743 } 744 745 if (u) { 746 undo_items.add (ui); 747 redo_items.clear (); 748 } 749 750 update_paragraph_index (); 751 layout (); 752 753 text_changed (get_text ()); 754 show_selection = false; 755 } 756 757 public string get_text () { 758 StringBuilder sb = new StringBuilder (); 759 760 generate_all_paragraphs (); 761 762 foreach (Paragraph p in paragraphs) { 763 sb.append (p.text); 764 } 765 766 return sb.str; 767 } 768 769 Carret get_carret_at (double click_x, double click_y, bool check_boundaries = true) { 770 int i = 0; 771 double tx, ty; 772 double p; 773 string w; 774 int ch_index; 775 double min_d = double.MAX; 776 Carret c = new Carret (); 777 double dt; 778 779 c.paragraph = -1; 780 c.desired_x = click_x; 781 c.desired_y = click_y; 782 783 foreach (Paragraph paragraph in paragraphs) { 784 if (!check_boundaries || paragraph.text_is_on_screen (allocation, widget_y)) { 785 ch_index = 0; 786 787 if (paragraph.start_y + widget_y - font_size <= click_y <= paragraph.end_y + widget_y + font_size) { 788 foreach (Text next_word in paragraph.words) { 789 double tt_click = click_y - widget_y - padding + font_size; // - next_word.get_baseline_to_bottom (); //- font_size + next_word.get_baseline_to_bottom (); 790 791 w = next_word.text; 792 if (next_word.widget_y <= tt_click <= next_word.widget_y + font_size) { 793 Theme.text_color (next_word, "Foreground 1"); 794 795 p = next_word.get_sidebearing_extent (); 796 797 if ((next_word.widget_y <= tt_click <= next_word.widget_y + font_size) 798 && (next_word.widget_x + widget_x <= click_x <= next_word.widget_x + widget_x + padding + next_word.get_sidebearing_extent ())) { 799 800 tx = widget_x + next_word.widget_x + padding; 801 ty = widget_y + next_word.widget_y + padding; 802 803 next_word.iterate ((glyph, kerning, last) => { 804 double cw; 805 int ci; 806 double d; 807 string gc = (!) glyph.get_unichar ().to_string (); 808 809 d = Math.fabs (click_x - tx); 810 811 if (d <= min_d) { 812 min_d = d; 813 c.character_index = ch_index; 814 c.paragraph = i; 815 } 816 817 cw = (glyph.get_width ()) * next_word.get_scale () + kerning; 818 ci = gc.length; 819 820 tx += cw; 821 ch_index += ci; 822 }); 823 824 dt = Math.fabs (click_x - (tx + widget_x + padding)); 825 if (dt < min_d) { 826 min_d = dt; 827 c.character_index = ch_index; 828 c.paragraph = i; 829 } 830 } else { 831 dt = Math.fabs (click_x - (next_word.widget_x + widget_x + padding + next_word.get_sidebearing_extent ())); 832 833 if (dt < min_d) { 834 min_d = dt; 835 c.character_index = ch_index + w.length; 836 837 if (w.has_suffix ("\n")) { 838 c.character_index -= "\n".length; 839 } 840 841 c.paragraph = i; 842 } 843 844 ch_index += w.length; 845 } 846 } else { 847 ch_index += w.length; 848 } 849 } 850 } 851 } 852 i++; 853 } 854 855 if (unlikely (c.paragraph < 0)) { 856 c.paragraph = paragraphs.size > 0 ? paragraphs.size - 1 : 0; 857 c.character_index = paragraphs.size > 0 ? paragraphs.get (c.paragraph).text.length : 0; 858 } 859 860 store_undo_state_at_next_event = true; 861 862 return c; 863 } 864 865 /** @return offset to click in text. */ 866 public void layout () { 867 double p; 868 double tx, ty; 869 string w; 870 double xmax = 0; 871 int i = 0; 872 double dd; 873 874 tx = 0; 875 ty = font_size; 876 877 if (allocation.width <= 0 || allocation.height <= 0) { 878 warning ("Parent widget allocation is not set."); 879 } 880 881 for (i = paragraphs.size - 1; i >= 0 && paragraphs.size > 1; i--) { 882 if (unlikely (paragraphs.get (i).is_empty ())) { 883 warning ("Empty paragraph."); 884 paragraphs.remove_at (i); 885 update_paragraph_index (); 886 } 887 } 888 889 i = 0; 890 foreach (Paragraph paragraph in paragraphs) { 891 if (paragraph.need_layout 892 || (paragraph.text_area_width != width 893 && paragraph.text_is_on_screen (allocation, widget_y))) { 894 895 paragraph.start_y = ty; 896 paragraph.start_x = tx; 897 898 paragraph.cached_surface = null; 899 900 foreach (Text next_word in paragraph.words) { 901 next_word.set_font_size (font_size); 902 903 w = next_word.text; 904 p = next_word.get_sidebearing_extent (); 905 906 if (unlikely (p == 0)) { 907 warning (@"Zero width word: $(w)"); 908 } 909 910 if (w == "") { 911 break; 912 } 913 914 if (w == "\n") { 915 next_word.widget_x = tx; 916 next_word.widget_y = ty; 917 918 tx = 0; 919 ty += next_word.font_size; 920 } else { 921 if (!single_line) { 922 if (tx + p + 2 * padding > width || w == "\n") { 923 tx = 0; 924 ty += next_word.font_size; 925 } 926 } 927 928 if (tx + p > xmax) { 929 xmax = tx + p; 930 } 931 932 next_word.widget_x = tx; 933 next_word.widget_y = ty; 934 935 if (w != "\n") { 936 tx += p; 937 } 938 } 939 } 940 941 if (tx > xmax) { 942 xmax = tx; 943 } 944 945 paragraph.text_area_width = width; 946 paragraph.width = xmax; 947 paragraph.end_x = tx; 948 paragraph.end_y = ty; 949 paragraph.need_layout = false; 950 } 951 952 if (xmax > width) { 953 break; 954 } 955 956 tx = paragraph.end_x; 957 ty = paragraph.end_y; 958 i++; 959 } 960 961 if (xmax > width) { 962 this.width = xmax + 2 * padding; 963 layout (); 964 return; 965 } 966 967 this.height = fmax (min_height, ty + 2 * padding); 968 969 if (last_paragraph != DONE) { 970 this.height = (text_length / (double) last_paragraph) * ty + 2 * padding; // estimate height 971 } 972 973 if (ty + widget_y < allocation.height && last_paragraph != DONE) { 974 generate_paragraphs (); 975 layout (); 976 return; 977 } 978 979 ty = font_size; 980 tx = 0; 981 982 foreach (Paragraph paragraph in paragraphs) { 983 dd = ty - paragraph.start_y; 984 985 if (dd != 0) { 986 paragraph.start_y += dd; 987 paragraph.end_y += dd; 988 foreach (Text word in paragraph.words) { 989 word.widget_y += dd; 990 } 991 } 992 993 ty = paragraph.end_y; 994 } 995 } 996 997 public void button_press (uint button, double x, double y) { 998 if (is_over (x, y)) { 999 carret = get_carret_at (x, y); 1000 selection_end = carret.copy (); 1001 update_selection = true; 1002 } 1003 } 1004 1005 public void button_release (uint button, double x, double y) { 1006 update_selection = false; 1007 show_selection = selection_is_visible (); 1008 } 1009 1010 public bool motion (double x, double y) { 1011 if (update_selection) { 1012 selection_end = get_carret_at (x, y); 1013 show_selection = selection_is_visible (); 1014 } 1015 1016 return update_selection; 1017 } 1018 1019 public override void draw (Context cr) { 1020 Text word; 1021 double tx, ty; 1022 string w; 1023 double scale; 1024 double width; 1025 double x = widget_x; 1026 double y = widget_y; 1027 Carret selection_start, selection_stop; 1028 double carret_x; 1029 double carret_y; 1030 1031 layout (); 1032 1033 if (draw_border) { 1034 // background 1035 cr.save (); 1036 cr.set_line_width (1); 1037 Theme.color (cr, "Text Area Background"); 1038 draw_rounded_rectangle (cr, x, y, this.width, this.height - padding, padding); 1039 cr.fill (); 1040 cr.restore (); 1041 1042 // border 1043 cr.save (); 1044 cr.set_line_width (1); 1045 Theme.color (cr, "Foreground 1"); 1046 draw_rounded_rectangle (cr, x, y, this.width, this.height - padding, padding); 1047 cr.stroke (); 1048 cr.restore (); 1049 } 1050 1051 cr.save (); 1052 1053 word = new Text (); 1054 word.use_cache (use_cache); 1055 1056 width = this.width - padding; 1057 x += padding; 1058 scale = word.get_scale (); 1059 y += font_size; 1060 1061 // draw selection background 1062 if (has_selection ()) { 1063 tx = 0; 1064 ty = 0; 1065 1066 selection_start = get_selection_start (); 1067 selection_stop = get_selection_stop (); 1068 1069 cr.save (); 1070 Theme.color (cr, "Highlighted 1"); 1071 1072 for (int i = selection_start.paragraph; i <= selection_stop.paragraph; i++) { 1073 return_if_fail (0 <= i < paragraphs.size); 1074 Paragraph pg = paragraphs.get (i); 1075 1076 if (pg.text_is_on_screen (allocation, widget_y)) { 1077 int char_index = 0; 1078 1079 foreach (Text next_word in pg.words) { 1080 double cw = next_word.get_sidebearing_extent (); 1081 bool paint_background = false; 1082 bool partial_start = false; 1083 bool partial_stop = false; 1084 int wl; 1085 1086 w = next_word.text; 1087 wl = w.length; 1088 scale = next_word.get_scale (); 1089 1090 if (selection_start.paragraph == selection_stop.paragraph) { 1091 partial_start = true; 1092 partial_stop = true; 1093 } else if (selection_start.paragraph < i < selection_stop.paragraph) { 1094 paint_background = true; 1095 } else if (selection_start.paragraph == i) { 1096 paint_background = true; 1097 partial_start = true; 1098 } else if (selection_stop.paragraph == i) { 1099 paint_background = char_index + wl < selection_stop.character_index; 1100 partial_stop = !paint_background; 1101 } 1102 1103 if (paint_background && !(partial_start || partial_stop)) { 1104 double selection_y = widget_y + next_word.widget_y + scale * -next_word.font.bottom_limit - font_size; 1105 cr.rectangle (widget_x + padding + next_word.widget_x - 1, selection_y, cw + 1, font_size); 1106 cr.fill (); 1107 } 1108 1109 if (partial_start || partial_stop) { 1110 int index = char_index; 1111 double bx = widget_x + padding + next_word.widget_x + (partial_start ? 0 : 1); 1112 1113 next_word.iterate ((glyph, kerning, last) => { 1114 double cwi; 1115 int ci; 1116 bool draw = (index >= selection_start.character_index && partial_start && !partial_stop) 1117 || (index < selection_stop.character_index && !partial_start && partial_stop) 1118 || (selection_start.character_index <= index < selection_stop.character_index && partial_start && partial_stop); 1119 1120 cwi = (glyph.get_width ()) * next_word.get_scale () + kerning; 1121 1122 if (draw) { 1123 double selection_y = widget_y + next_word.widget_y + scale * -next_word.font.bottom_limit - font_size; 1124 cr.rectangle (bx - 1, selection_y, cwi + 1, font_size); 1125 cr.fill (); 1126 } 1127 1128 bx += cwi; 1129 ci = ((!) glyph.get_unichar ().to_string ()).length; 1130 index += ci; 1131 }); 1132 } 1133 1134 char_index += w.length; 1135 } 1136 } 1137 } 1138 1139 cr.restore (); 1140 } 1141 1142 tx = 0; 1143 ty = 0; 1144 1145 int first_visible = 0; 1146 int last_visible; 1147 int paragraphs_size = paragraphs.size; 1148 while (first_visible < paragraphs_size) { 1149 if (paragraphs.get (first_visible).text_is_on_screen (allocation, widget_y)) { 1150 break; 1151 } 1152 first_visible++; 1153 } 1154 1155 last_visible = first_visible; 1156 while (last_visible < paragraphs_size) { 1157 if (!paragraphs.get (last_visible).text_is_on_screen (allocation, widget_y)) { 1158 last_visible++; 1159 break; 1160 } 1161 last_visible++; 1162 } 1163 1164 if (paragraphs_size == 0) { 1165 if (carret_is_visible) { 1166 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding); 1167 } 1168 1169 return; 1170 } 1171 1172 Context cc; // cached context 1173 Paragraph paragraph; 1174 paragraph = paragraphs.get (0); 1175 1176 tx = paragraph.start_x; 1177 ty = paragraph.start_y; 1178 1179 if (paragraphs.size > 0 && paragraphs.get (0).words.size > 0) { 1180 Text t = paragraphs.get (0).words.get (0); 1181 Theme.text_color (t, "Foreground 1"); 1182 } 1183 1184 for (int i = first_visible; i < last_visible; i++) { 1185 paragraph = paragraphs.get (i); 1186 1187 tx = paragraph.start_x; 1188 ty = paragraph.start_y; 1189 1190 if (paragraph.cached_surface == null) { 1191 paragraph.cached_surface = new Surface.similar (cr.get_target (), Cairo.Content.COLOR_ALPHA, (int) width + 2, paragraph.get_height () + (int) font_size + 2); 1192 cc = new Context ((!) paragraph.cached_surface); 1193 1194 foreach (Text next_word in paragraph.words) { 1195 Theme.text_color (next_word, "Foreground 1"); 1196 1197 if (next_word.text != "\n") { 1198 next_word.draw_at_top (cc, next_word.widget_x, next_word.widget_y - ty); 1199 } 1200 } 1201 } 1202 1203 if (likely (paragraph.cached_surface != null)) { 1204 // FIXME: subpixel offset in text area 1205 cr.set_source_surface ((!) paragraph.cached_surface, (int) (x + tx), (int) (widget_y + paragraph.start_y - font_size + padding)); 1206 cr.paint (); 1207 } else { 1208 warning ("No paragraph image."); 1209 } 1210 } 1211 1212 if (carret_is_visible) { 1213 get_carret_position (carret, out carret_x, out carret_y); 1214 1215 if (carret_y < 0) { 1216 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding); 1217 } else { 1218 draw_carret_at (cr, carret_x, carret_y); 1219 } 1220 } 1221 1222 if (has_selection ()) { 1223 get_carret_position (selection_end, out carret_x, out carret_y); 1224 1225 if (carret_y < 0) { 1226 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding); 1227 } else { 1228 draw_carret_at (cr, carret_x, carret_y); 1229 } 1230 } 1231 } 1232 1233 void get_carret_position (Carret carret, out double carret_x, out double carret_y) { 1234 Paragraph paragraph; 1235 double tx; 1236 double ty; 1237 int ch_index; 1238 int wl; 1239 double pos_x, pos_y; 1240 1241 ch_index = 0; 1242 1243 carret_x = -1; 1244 carret_y = -1; 1245 1246 return_if_fail (0 <= carret.paragraph < paragraphs.size); 1247 paragraph = paragraphs.get (carret.paragraph); 1248 1249 pos_x = -1; 1250 pos_y = -1; 1251 1252 foreach (Text next_word in paragraph.words) { 1253 string w = next_word.text; 1254 wl = w.length; 1255 1256 if (carret.character_index == ch_index) { 1257 pos_x = next_word.widget_x + widget_x + padding; 1258 pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom (); 1259 } else if (carret.character_index >= ch_index + wl) { 1260 pos_x = next_word.widget_x + next_word.get_sidebearing_extent () + widget_x + padding; 1261 pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom (); 1262 1263 if (next_word.text.has_suffix ("\n")) { 1264 pos_x = widget_x + padding; 1265 pos_y += next_word.font_size; 1266 } 1267 } else if (ch_index < carret.character_index <= ch_index + wl) { 1268 tx = widget_x + next_word.widget_x; 1269 ty = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom (); 1270 1271 if (carret.character_index <= ch_index) { 1272 pos_x = widget_x + padding; 1273 pos_y = ty; 1274 } 1275 1276 next_word.iterate ((glyph, kerning, last) => { 1277 double cw; 1278 int ci; 1279 1280 cw = (glyph.get_width ()) * next_word.get_scale () + kerning; 1281 ci = ((!) glyph.get_unichar ().to_string ()).length; 1282 1283 if (ch_index < carret.character_index <= ch_index + ci) { 1284 pos_x = tx + cw + padding; 1285 pos_y = ty; 1286 1287 if (glyph.get_unichar () == '\n') { 1288 pos_x = widget_x + padding; 1289 pos_y += next_word.font_size; 1290 } 1291 } 1292 1293 tx += cw; 1294 ch_index += ci; 1295 }); 1296 } 1297 1298 ch_index += wl; 1299 } 1300 1301 carret_x = pos_x; 1302 carret_y = pos_y; 1303 } 1304 1305 void draw_carret_at (Context cr, double x, double y) { 1306 cr.save (); 1307 cr.set_source_rgba (0, 0, 0, 0.5); 1308 cr.set_line_width (1); 1309 cr.move_to (x, y); 1310 cr.line_to (x, y - font_size); 1311 cr.stroke (); 1312 cr.restore (); 1313 } 1314 1315 public void store_undo_edit_state () { 1316 TextUndoItem ui = new TextUndoItem (carret); 1317 ui.edited.add (get_current_paragraph ().copy ()); 1318 undo_items.add (ui); 1319 redo_items.clear (); 1320 } 1321 1322 public void redo () { 1323 TextUndoItem i; 1324 TextUndoItem undo_item; 1325 1326 if (redo_items.size > 0) { 1327 i = redo_items.get (redo_items.size - 1); 1328 1329 undo_item = new TextUndoItem (i.carret); 1330 1331 i.deleted.sort ((a, b) => { 1332 Paragraph pa = (Paragraph) a; 1333 Paragraph pb = (Paragraph) b; 1334 return pb.index - pa.index; 1335 }); 1336 1337 i.added.sort ((a, b) => { 1338 Paragraph pa = (Paragraph) a; 1339 Paragraph pb = (Paragraph) b; 1340 return pa.index - pb.index; 1341 }); 1342 1343 foreach (Paragraph p in i.deleted) { 1344 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1345 warning ("Paragraph not found."); 1346 } else { 1347 undo_item.deleted.add (p.copy ()); 1348 paragraphs.remove_at (p.index); 1349 } 1350 } 1351 1352 foreach (Paragraph p in i.added) { 1353 if (p.index == paragraphs.size) { 1354 paragraphs.add (p.copy ()); 1355 } else { 1356 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1357 warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)"); 1358 } else { 1359 undo_item.added.add (paragraphs.get (p.index).copy ()); 1360 paragraphs.insert (p.index, p.copy ()); 1361 } 1362 } 1363 } 1364 1365 foreach (Paragraph p in i.edited) { 1366 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1367 warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)"); 1368 return; 1369 } 1370 1371 undo_item.edited.add (paragraphs.get (p.index).copy ()); 1372 paragraphs.set (p.index, p.copy ()); 1373 } 1374 1375 redo_items.remove_at (redo_items.size - 1); 1376 undo_items.add (undo_item); 1377 1378 carret = i.carret.copy (); 1379 layout (); 1380 } 1381 } 1382 1383 public void undo () { 1384 TextUndoItem i; 1385 TextUndoItem redo_item; 1386 1387 if (undo_items.size > 0) { 1388 i = undo_items.get (undo_items.size - 1); 1389 redo_item = new TextUndoItem (i.carret); 1390 1391 i.deleted.sort ((a, b) => { 1392 Paragraph pa = (Paragraph) a; 1393 Paragraph pb = (Paragraph) b; 1394 return pa.index - pb.index; 1395 }); 1396 1397 i.added.sort ((a, b) => { 1398 Paragraph pa = (Paragraph) a; 1399 Paragraph pb = (Paragraph) b; 1400 return pb.index - pa.index; 1401 }); 1402 1403 foreach (Paragraph p in i.added) { 1404 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1405 warning ("Paragraph not found."); 1406 } else { 1407 redo_item.added.add (paragraphs.get (p.index).copy ()); 1408 paragraphs.remove_at (p.index); 1409 } 1410 } 1411 1412 foreach (Paragraph p in i.deleted) { 1413 if (p.index == paragraphs.size) { 1414 paragraphs.add (p.copy ()); 1415 } else { 1416 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1417 warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)"); 1418 } else { 1419 redo_item.deleted.add (p.copy ()); 1420 paragraphs.insert (p.index, p.copy ()); 1421 } 1422 } 1423 } 1424 1425 foreach (Paragraph p in i.edited) { 1426 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1427 warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)"); 1428 return; 1429 } 1430 1431 redo_item.edited.add (paragraphs.get (p.index).copy ()); 1432 paragraphs.set (p.index, p.copy ()); 1433 } 1434 1435 undo_items.remove_at (undo_items.size - 1); 1436 redo_items.add (redo_item); 1437 1438 carret = i.carret.copy (); 1439 layout (); 1440 } 1441 } 1442 1443 public void set_editable (bool editable) { 1444 this.editable = editable; 1445 } 1446 1447 public class TextUndoItem : GLib.Object { 1448 public Carret carret; 1449 public Gee.ArrayList<Paragraph> added = new Gee.ArrayList<Paragraph> (); 1450 public Gee.ArrayList<Paragraph> edited = new Gee.ArrayList<Paragraph> (); 1451 public Gee.ArrayList<Paragraph> deleted = new Gee.ArrayList<Paragraph> (); 1452 1453 public TextUndoItem (Carret c) { 1454 carret = c.copy (); 1455 } 1456 } 1457 1458 public class Paragraph : GLib.Object { 1459 public double end_x = -10000; 1460 public double end_y = -10000; 1461 1462 public double start_x = -10000; 1463 public double start_y = -10000; 1464 1465 public double width = -10000; 1466 public double text_area_width = -10000; 1467 1468 public string text; 1469 1470 public Gee.ArrayList<Text> words { 1471 get { 1472 if (words_in_paragraph.size == 0) { 1473 generate_words (); 1474 } 1475 1476 return words_in_paragraph; 1477 } 1478 } 1479 1480 private Gee.ArrayList<Text> words_in_paragraph = new Gee.ArrayList<Text> (); 1481 1482 public int text_length; 1483 1484 public bool need_layout = true; 1485 1486 public Surface? cached_surface = null; 1487 1488 double font_size; 1489 1490 public int index; 1491 1492 public Paragraph (string text, double font_size, int index) { 1493 this.index = index; 1494 this.font_size = font_size; 1495 set_text (text); 1496 } 1497 1498 public Paragraph copy () { 1499 Paragraph p = new Paragraph (text.dup (), font_size, index); 1500 p.need_layout = true; 1501 return p; 1502 } 1503 1504 public bool is_empty () { 1505 return text == ""; 1506 } 1507 1508 public void set_text (string t) { 1509 this.text = t; 1510 text_length = t.length; 1511 need_layout = true; 1512 words.clear (); 1513 cached_surface = null; 1514 } 1515 1516 public int get_height () { 1517 return (int) (end_y - start_y) + 1; 1518 } 1519 1520 public int get_width () { 1521 return (int) width + 1; 1522 } 1523 1524 public bool text_is_on_screen (WidgetAllocation alloc, double widget_y) { 1525 bool v = (0 <= start_y + widget_y <= alloc.height) 1526 || (0 <= end_y + widget_y <= alloc.height) 1527 || (start_y + widget_y <= 0 && alloc.height <= end_y + widget_y); 1528 return v; 1529 } 1530 1531 private void generate_words () { 1532 string w; 1533 int p = 0; 1534 bool carret_at_word_end = false; 1535 Text word; 1536 int carret = 0; 1537 int iter_pos = 0; 1538 1539 return_if_fail (words_in_paragraph.size == 0); 1540 1541 while (p < text_length) { 1542 w = get_next_word (out carret_at_word_end, ref iter_pos, carret); 1543 1544 if (w == "") { 1545 break; 1546 } 1547 1548 word = new Text (w, font_size); 1549 words_in_paragraph.add (word); 1550 } 1551 } 1552 1553 string get_next_word (out bool carret_at_end_of_word, ref int iter_pos, int carret) { 1554 int i; 1555 int ni; 1556 int pi; 1557 string n; 1558 int nl; 1559 1560 carret_at_end_of_word = false; 1561 1562 if (iter_pos >= text_length) { 1563 carret_at_end_of_word = true; 1564 return "".dup (); 1565 } 1566 1567 if (text.get_char (iter_pos) == '\n') { 1568 iter_pos += "\n".length; 1569 carret_at_end_of_word = (iter_pos == carret); 1570 return "\n".dup (); 1571 } 1572 1573 i = text.index_of (" ", iter_pos); 1574 pi = i + " ".length; 1575 1576 ni = text.index_of ("\t", iter_pos); 1577 if (ni != -1 && ni < pi || i == -1) { 1578 i = ni; 1579 pi = i + "\t".length; 1580 } 1581 1582 ni = text.index_of ("\n", iter_pos); 1583 if (ni != -1 && ni < pi || i == -1) { 1584 i = ni; 1585 pi = i; 1586 } 1587 1588 if (iter_pos + iter_pos - pi > text_length || i == -1) { 1589 n = text.substring (iter_pos); 1590 } else { 1591 n = text.substring (iter_pos, pi - iter_pos); 1592 } 1593 1594 nl = n.length; 1595 if (iter_pos < carret < iter_pos + nl) { 1596 n = text.substring (iter_pos, carret - iter_pos); 1597 nl = n.length; 1598 carret_at_end_of_word = true; 1599 } 1600 1601 iter_pos += nl; 1602 1603 if (iter_pos == carret) { 1604 carret_at_end_of_word = true; 1605 } 1606 1607 return n; 1608 } 1609 } 1610 1611 public class Carret : GLib.Object { 1612 1613 public int paragraph = 0; 1614 1615 public int character_index { 1616 get { 1617 return ci; 1618 } 1619 1620 set { 1621 ci = value; 1622 } 1623 } 1624 1625 private int ci = 0; 1626 1627 public double desired_x = 0; 1628 public double desired_y = 0; 1629 1630 public Carret () { 1631 } 1632 1633 public void print () { 1634 stdout.printf (@"paragraph: $paragraph, character_index: $character_index\n"); 1635 } 1636 1637 public Carret copy () { 1638 Carret c = new Carret (); 1639 1640 c.paragraph = paragraph; 1641 c.character_index = character_index; 1642 1643 c.desired_x = desired_x; 1644 c.desired_y = desired_y; 1645 1646 return c; 1647 } 1648 } 1649 } 1650 1651 } 1652