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