.
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