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