.
1 /*
2 Copyright (C) 2012 2013 2014 2015 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 using Gee;
18
19 namespace BirdFont {
20
21 public class Glyph : FontDisplay {
22 // Background image
23 BackgroundImage? background_image = null;
24 bool background_image_visible = true;
25
26 // Glyph zoom level
27 public double view_zoom = 0.1;
28
29 public double view_offset_x = 0;
30 public double view_offset_y = 0;
31 Gee.ArrayList<ZoomView> zoom_list = new Gee.ArrayList<ZoomView> ();
32 int zoom_list_index = 0;
33
34 // Paths
35 public Gee.ArrayList<Path> path_list = new Gee.ArrayList<Path> ();
36 public Gee.ArrayList<Path> active_paths = new Gee.ArrayList<Path> ();
37
38 // The point where edit event begun
39 double pointer_begin_x = 0;
40 double pointer_begin_y = 0;
41
42 // Tap last tap event position in pixels
43 int last_tap0_y = 0;
44 int last_tap0_x = 0;
45 int last_tap1_y = 0;
46 int last_tap1_x = 0;
47 double zoom_distance = 0;
48 bool change_view;
49
50 bool ignore_input = false;
51
52 // Current pointer position
53 public double motion_x = 0;
54 public double motion_y = 0;
55
56 // Zoom area
57 double zoom_x1 = 0;
58 double zoom_y1 = 0;
59 double zoom_x2 = 0;
60 double zoom_y2 = 0;
61 bool zoom_area_is_visible = false;
62
63 bool view_is_moving = false;
64 public double move_offset_x = 0;
65 public double move_offset_y = 0;
66 bool move_canvas = false;
67
68 public WidgetAllocation allocation = new WidgetAllocation ();
69
70 public unichar unichar_code = 0; // FIXME: name and unichar should be moved to to glyph collection
71 public string name;
72
73 public double left_limit;
74 public double right_limit;
75
76 // x-height, lsb, etc.
77 public Gee.ArrayList<Line> vertical_help_lines = new Gee.ArrayList<Line> ();
78 public Gee.ArrayList<Line> horizontal_help_lines = new Gee.ArrayList<Line> ();
79 bool show_help_lines = true;
80 bool xheight_lines_visible = false;
81 bool margin_boundaries_visible = false;
82 string new_guide_name = "";
83
84 Gee.ArrayList<Glyph> undo_list = new Gee.ArrayList<Glyph> ();
85 Gee.ArrayList<Glyph> redo_list = new Gee.ArrayList<Glyph> ();
86
87 string glyph_sequence = "";
88 bool open = true;
89
90 public static Glyph? background_glyph = null;
91
92 bool empty = false;
93
94 /** Id in the version list. */
95 public int version_id = 1;
96
97 /** Cache quadratic form on export. */
98 GlyfData? ttf_data = null;
99
100 Line left_line;
101 Line right_line;
102
103 /** Cache for Cairo rendering */
104 HashMap<int64?, Surface> glyph_cache = new HashMap<int64?, Surface> ((v) => {
105 int? t = (int?) v;
106 int i = (!) t;
107
108 if (i == 0) {
109 return 0;
110 }
111
112 return (int) (0xFFFFFFFF & i);
113 }, (a, b) => {
114 int64? i1;
115 int64? i2;
116
117 i1 = (int64?) a;
118 i2 = (int64?) b;
119
120 if (i1 == null || i1 == null) {
121 return false;
122 }
123
124 return (!) i1 == (!) i2;
125 });
126
127 public Glyph (string name, unichar unichar_code = 0) {
128 this.name = name;
129 this.unichar_code = unichar_code;
130
131 path_list.add (new Path ());
132
133 add_help_lines ();
134
135 left_limit = -28;
136 right_limit = 28;
137 }
138
139 public Glyph.no_lines (string name, unichar unichar_code = 0) {
140 this.name = name;
141 this.unichar_code = unichar_code;
142
143 path_list.add (new Path ());
144 }
145
146 public GlyfData get_ttf_data () {
147 if (ttf_data == null) {
148 ttf_data = new GlyfData (this);
149 }
150
151 return (!) ttf_data;
152 }
153
154 public PathList get_quadratic_paths () {
155 PointConverter pc;
156 PathList pl;
157
158 pl = new PathList ();
159 foreach (Path p in path_list) {
160 pc = new PointConverter (p);
161 pl.add (pc.get_quadratic_path ());
162 }
163
164 return pl;
165 }
166
167 public override void close () {
168 undo_list.clear ();
169 redo_list.clear ();
170 }
171
172 public void set_empty_ttf (bool e) {
173 empty = e;
174 }
175
176 public bool is_empty_ttf () {
177 return empty;
178 }
179
180 public void clear_active_paths () {
181 active_paths.clear ();
182 }
183
184 public void add_active_path (Path? p) {
185 Path path;
186 if (p != null) {
187 path = (!) p;
188
189 if (Toolbox.get_move_tool ().is_selected ()) {
190 Toolbox.set_object_stroke (path.stroke);
191 }
192
193 if (!active_paths.contains (path)) {
194 active_paths.add (path);
195 }
196 }
197 }
198
199 public void delete_background () {
200 store_undo_state ();
201 background_image = null;
202 GlyphCanvas.redraw ();
203 }
204
205 public Path? get_active_path () {
206 return_val_if_fail (active_paths.size > 0, null);
207 return active_paths.get (active_paths.size - 1);
208 }
209
210 public void boundaries (out double x1, out double y1, out double x2, out double y2) {
211 if (path_list.size == 0) {
212 x1 = 0;
213 y1 = 0;
214 x2 = 0;
215 y2 = 0;
216 return;
217 }
218
219 x1 = double.MAX;
220 x2 = double.MIN;
221 y1 = double.MAX;
222 y2 = double.MIN;
223
224 foreach (Path p in path_list) {
225 p.update_region_boundaries ();
226
227 if (p.points.size > 0) {
228 if (p.xmin < x1) {
229 x1 = p.xmin;
230 }
231
232 if (p.xmax > x2) {
233 x2 = p.xmax;
234 }
235
236 if (p.ymin < y1) {
237 y1 = p.ymin;
238 }
239
240 if (p.ymax > y2) {
241 y2 = p.ymax;
242 }
243 }
244 }
245 }
246
247 public void selection_boundaries (out double x, out double y, out double w, out double h) {
248 double px, py, px2, py2;
249
250 px = 10000;
251 py = 10000;
252 px2 = -10000;
253 py2 = -10000;
254
255 foreach (Path p in active_paths) {
256 if (p.xmin < px) {
257 px = p.xmin;
258 }
259
260 if (p.ymin < py) {
261 py = p.ymin;
262 }
263
264 if (p.xmax > px2) {
265 px2 = p.xmax;
266 }
267
268 if (p.ymax > py2) {
269 py2 = p.ymax;
270 }
271 }
272
273 if (px2 == -10000 || px == 10000) {
274 warning (@"No box for selected paths. ($(active_paths.size))");
275 px = 0;
276 py = 0;
277 px2 = 0;
278 py2 = 0;
279 }
280
281 x = px;
282 y = py2;
283 w = px2 - px;
284 h = py2 - py;
285 }
286
287 /** @return centrum pixel for x coordinates. */
288 public static double xc () {
289 double c = MainWindow.get_current_glyph ().allocation.width / 2.0;
290 return c;
291 }
292
293 /** @return centrum pixel for y coordinates. */
294 public static double yc () {
295 double c = MainWindow.get_current_glyph ().allocation.height / 2.0;
296 return c;
297 }
298
299 /** @return 1/view_zoom */
300 public static double ivz () {
301 return 1 / MainWindow.get_current_glyph ().view_zoom;
302 }
303
304 public void resized (WidgetAllocation alloc) {
305 double a, b, c, d;
306
307 a = Glyph.path_coordinate_x (0);
308 b = Glyph.path_coordinate_y (0);
309
310 this.allocation = alloc;
311
312 c = Glyph.path_coordinate_x (0);
313 d = Glyph.path_coordinate_y (0);
314
315 view_offset_x -= c - a;
316 view_offset_y -= b - d;
317 }
318
319 public void set_background_image (BackgroundImage? b) {
320 BackgroundImage bg;
321
322 if (b == null) {
323 background_image = null;
324 } else {
325 bg = (!) b;
326 background_image = bg;
327 }
328
329 BirdFont.get_current_font ().touch ();
330 }
331
332 public BackgroundImage? get_background_image () {
333 return background_image;
334 }
335
336 public override void scroll_wheel_up (double x, double y) {
337 if (KeyBindings.has_alt ()) {
338 zoom_in_at_point (x, y);
339 } else if (KeyBindings.has_ctrl ()) {
340 view_offset_x -= 10 / view_zoom;
341 } else {
342 view_offset_y -= 10 / view_zoom;
343 }
344
345 redraw_area (0, 0, allocation.width, allocation.height);
346 }
347
348 public override void scroll_wheel_down (double x, double y) {
349 if (KeyBindings.has_alt ()) {
350 zoom_out_at_point (x, y);
351 } else if (KeyBindings.has_ctrl ()) {
352 view_offset_x += 10 / view_zoom;
353 } else {
354 view_offset_y += 10 / view_zoom;
355 }
356
357 redraw_area (0, 0, allocation.width, allocation.height);
358 }
359
360 public void add_path (Path p) {
361 path_list.add (p);
362 }
363
364 public override void selected_canvas () {
365 TimeoutSource input_delay;
366
367 ttf_data = null; // recreate quatradic path on export
368
369 ignore_input = true; // make sure that tripple clicks in overview are ignored
370
371 input_delay = new TimeoutSource (250);
372 input_delay.set_callback(() => {
373 ignore_input = false;
374 return false;
375 });
376 input_delay.attach (null);
377
378 add_help_lines ();
379 KeyBindings.set_require_modifier (false);
380 glyph_sequence = Preferences.get ("glyph_sequence");
381
382 GridTool.update_lines ();
383
384 if (!is_null (MainWindow.native_window)) {
385 MainWindow.native_window.set_scrollbar_size (0);
386 }
387
388 update_zoom_bar ();
389 }
390
391 void update_zoom_bar () {
392 Toolbox.drawing_tools.zoom_bar.set_zoom ((view_zoom - 1) / 20);
393 }
394
395 public void remove_lines () {
396 vertical_help_lines.clear ();
397 horizontal_help_lines.clear ();
398 }
399
400 public void add_help_lines () {
401 remove_lines ();
402
403 return_if_fail (!is_null (BirdFont.get_current_font ()));
404
405 double bgt = BirdFont.get_current_font ().top_limit;
406 Line top_margin_line = new Line ("top margin", bgt, false);
407 top_margin_line.set_color (0.7, 0, 0, 0.5);
408 top_margin_line.position_updated.connect ((pos) => {
409 BirdFont.get_current_font ().top_limit = pos;
410 });
411
412 double thp = BirdFont.get_current_font ().top_position;
413 Line top_line = new Line ("top", thp, false);
414 top_line.position_updated.connect ((pos) => {
415 Font f = BirdFont.get_current_font ();
416 f.top_position = pos;
417 });
418
419 double xhp = BirdFont.get_current_font ().xheight_position;
420 Line xheight_line = new Line ("x-height", xhp, false);
421 xheight_line.set_color (120 / 255.0, 68 / 255.0, 120 / 255.0, 120 / 255.0);
422 xheight_line.dashed = true;
423 xheight_line.position_updated.connect ((pos) => {
424 Font f = BirdFont.get_current_font ();
425 f.xheight_position = pos;
426 });
427
428 double xbl = BirdFont.get_current_font ().base_line;
429 Line base_line = new Line ("baseline", xbl, false);
430 base_line.position_updated.connect ((pos) => {
431 Font f = BirdFont.get_current_font ();
432 f.base_line = pos;
433 });
434
435 double bp = BirdFont.get_current_font ().bottom_position;
436 Line bottom_line = new Line ("bottom", bp, false);
437 bottom_line.set_color (33 / 255.0, 68 / 255.0, 120 / 255.0, 166 / 255.0);
438 bottom_line.position_updated.connect ((pos) => {
439 BirdFont.get_current_font ().bottom_position = pos;
440 });
441
442 double bgb = BirdFont.get_current_font ().bottom_limit;
443 Line bottom_margin_line = new Line ("bottom margin", bgb, false);
444 bottom_margin_line.set_color (0.7, 0, 0, 0.5);
445 bottom_margin_line.position_updated.connect ((pos) => {
446 BirdFont.get_current_font ().bottom_limit = pos;
447 });
448
449 left_line = new Line ("left", left_limit, true);
450 left_line.position_updated.connect ((pos) => {
451 double x1, y1, x2, y2;
452
453 left_limit = pos;
454 update_other_spacing_classes ();
455
456 boundaries (out x1, out y1, out x2, out y2);
457 left_line.set_metrics (x1 - pos);
458 });
459 left_line.position_updated (left_limit);
460
461 right_line = new Line ("right", right_limit, true);
462 right_line.position_updated.connect ((pos) => {
463 double x1, y1, x2, y2;
464
465 right_limit = pos;
466 update_other_spacing_classes ();
467
468 boundaries (out x1, out y1, out x2, out y2);
469 right_line.set_metrics (pos - x2);
470 });
471 right_line.position_updated (right_limit);
472
473 // lists of lines are sorted and lines are added only if
474 // they are relevant for a particular glyph.
475
476 // left to right
477 add_line (left_line);
478 add_line (right_line);
479
480 bool glyph_has_top = has_top_line ();
481
482 // top to bottom
483 add_line (top_margin_line);
484 top_margin_line.set_visible (margin_boundaries_visible);
485
486 add_line (top_line);
487 top_line.set_visible (glyph_has_top || xheight_lines_visible);
488
489 add_line (xheight_line);
490 xheight_line.set_visible (!glyph_has_top || xheight_lines_visible);
491
492 add_line (base_line);
493
494 add_line (bottom_line);
495 bottom_line.set_visible (CharDatabase.has_descender (unichar_code) || xheight_lines_visible);
496
497 add_line (bottom_margin_line);
498 bottom_margin_line.set_visible (margin_boundaries_visible);
499
500 foreach (Line guide in BirdFont.get_current_font ().custom_guides) {
501 add_line (guide);
502 }
503 }
504
505 public double get_left_side_bearing () {
506 double x1, y1, x2, y2;
507 boundaries (out x1, out y1, out x2, out y2);
508 return x1 - left_limit;
509 }
510
511 public double get_right_side_bearing () {
512 double x1, y1, x2, y2;
513 boundaries (out x1, out y1, out x2, out y2);
514 return right_limit - x2;
515 }
516
517 bool has_top_line () {
518 return !unichar_code.islower () || CharDatabase.has_ascender (unichar_code);
519 }
520
521 public bool get_show_help_lines () {
522 return show_help_lines;
523 }
524
525 /** Show both x-height and top lines. */
526 public bool get_xheight_lines_visible () {
527 return xheight_lines_visible;
528 }
529
530 /** Show both x-height and top lines. */
531 public void set_xheight_lines_visible (bool x) {
532 xheight_lines_visible = x;
533 add_help_lines ();
534 }
535
536 public void set_margin_lines_visible (bool m) {
537 margin_boundaries_visible = m;
538 add_help_lines ();
539 }
540
541 public bool get_margin_lines_visible () {
542 return margin_boundaries_visible;
543 }
544
545 public void remove_empty_paths () {
546 foreach (Path p in path_list) {
547 if (p.points.size < 2) {
548 delete_path (p);
549 remove_empty_paths ();
550 return;
551 }
552 }
553 }
554
555 public void delete_path (Path p) requires (path_list.size > 0) {
556 path_list.remove (p);
557 }
558
559 public string get_svg_data () {
560 return Svg.to_svg_glyph (this);
561 }
562
563 public int get_height () {
564 Font f = BirdFont.get_current_font ();
565 return (int) Math.fabs (f.top_limit - f.bottom_limit);
566 }
567
568 public double get_width () {
569 return Math.fabs (right_limit - left_limit);
570 }
571
572 public unichar get_unichar () {
573 return unichar_code;
574 }
575
576 public string get_unichar_string () {
577 string? s = (!)get_unichar ().to_string ();
578
579 if (unlikely (s == null)) {
580 warning ("Invalid character.");
581 return "".dup ();
582 }
583
584 return (!) s;
585 }
586
587 public void redraw_help_lines () {
588 redraw_area (0, 0, allocation.width, allocation.height);
589 }
590
591 public void set_show_help_lines (bool hl) {
592 show_help_lines = hl;
593 }
594
595 private void add_line (Line line) {
596 if (line.is_vertical ()) {
597 vertical_help_lines.add (line);
598 } else {
599 horizontal_help_lines.add (line);
600 }
601
602 sort_help_lines ();
603
604 line.queue_draw_area.connect ((x, y, w, h) => {
605 this.redraw_area (x, y, w, h);
606 });
607 }
608
609 public void sort_help_lines () {
610 vertical_help_lines.sort ((a, b) => {
611 Line first, next;
612 first = (Line) a;
613 next = (Line) b;
614 return (int) (first.get_pos () - next.get_pos ());
615 });
616
617 horizontal_help_lines.sort ((a, b) => {
618 Line first, next;
619 first = (Line) a;
620 next = (Line) b;
621 return (int) (first.get_pos () - next.get_pos ());
622 });
623 }
624
625 public override string get_name () {
626 return name;
627 }
628
629 public override string get_label () {
630 return name;
631 }
632
633 private void help_line_event (int x, int y) {
634 bool m = false;
635
636 foreach (Line line in vertical_help_lines) {
637 if (!m && line.event_move_to (x, y, allocation)) {
638 m = true;
639 }
640 }
641
642 foreach (Line line in horizontal_help_lines) {
643 if (!m && line.event_move_to (x, y, allocation)) {
644 m = true;
645 }
646 }
647 }
648
649 public override void key_release (uint keyval) {
650 Tool t;
651 t = MainWindow.get_toolbox ().get_current_tool ();
652 t.key_release_action (t, keyval);
653
654 if (keyval == (uint)' ') {
655 move_canvas = false;
656 }
657 }
658
659 public override void key_press (uint keyval) {
660 Tool t = MainWindow.get_toolbox ().get_current_tool ();
661 t.key_press_action (t, keyval);
662
663 if (keyval == (uint)' ') {
664 move_canvas = true;
665 }
666
667 switch (keyval) {
668 case Key.NUM_PLUS:
669 zoom_in ();
670 break;
671 case Key.NUM_MINUS:
672 zoom_out ();
673 break;
674 }
675 }
676
677 /** Delete edit point from path.
678 * @return false if no points was deleted
679 */
680 public bool process_deleted () {
681 Gee.ArrayList<Path> deleted_paths = new Gee.ArrayList<Path> ();
682 foreach (Path p in path_list) {
683 if (p.points.size > 0) {
684 if (process_deleted_points_in_path (p)) {
685 return true;
686 }
687 } else {
688 deleted_paths.add (p);
689 }
690 }
691
692 foreach (Path p in deleted_paths) {
693 delete_path (p);
694 }
695
696 return false;
697 }
698
699 private bool process_deleted_points_in_path (Path p) {
700 PathList remaining_points;
701 remaining_points = p.process_deleted_points ();
702 foreach (Path path in remaining_points.paths) {
703 add_path (path);
704 path.reopen ();
705 path.create_list ();
706
707 add_active_path (path);
708 }
709
710 if (remaining_points.paths.size > 0) {
711 delete_path (p);
712 return true;
713 }
714
715 return false;
716 }
717
718 public override void motion_notify (double x, double y) {
719 Tool t = MainWindow.get_toolbox ().get_current_tool ();
720
721 if (view_is_moving) {
722 move_view_offset (x, y);
723 return;
724 }
725
726 help_line_event ((int) x, (int) y);
727 t.move_action (t, (int) x, (int) y);
728
729 motion_x = x * ivz () - xc () + view_offset_x;
730 motion_y = yc () - y * ivz () - view_offset_y;
731 }
732
733 public override void button_release (int button, double ex, double ey) {
734 bool line_moving = false;
735 view_is_moving = false;
736
737 foreach (Line line in get_all_help_lines ()) {
738 if (!line.set_move (false)) {
739 line_moving = true;
740 }
741 }
742
743 if (!line_moving) {
744 Tool t = MainWindow.get_toolbox ().get_current_tool ();
745 t.release_action (t, (int) button, (int) ex, (int) ey);
746 }
747
748 update_view ();
749 }
750
751 private Gee.ArrayList<Line> get_all_help_lines () {
752 Gee.ArrayList<Line> all_lines = new Gee.ArrayList<Line> ();
753
754 foreach (Line l in vertical_help_lines) {
755 all_lines.add (l);
756 }
757
758 foreach (Line l in horizontal_help_lines) {
759 all_lines.add (l);
760 }
761
762 if (GridTool.is_visible ()) {
763 foreach (Line l in GridTool.get_vertical_lines ()) {
764 all_lines.add (l);
765 }
766
767 foreach (Line l in GridTool.get_horizontal_lines ()) {
768 all_lines.add (l);
769 }
770 }
771
772 return all_lines;
773 }
774
775 public void update_view () {
776 GlyphCanvas.redraw ();
777 }
778
779 public override void double_click (uint button, double ex, double ey) {
780 Tool t = MainWindow.get_toolbox ().get_current_tool ();
781 t.double_click_action (t, (int) button, (int) ex, (int) ey);
782 }
783
784 public override void button_press (uint button, double ex, double ey) {
785 bool moving_lines = false;
786
787 pointer_begin_x = ex;
788 pointer_begin_y = ey;
789
790 foreach (Line line in horizontal_help_lines) {
791 if (!moving_lines && line.is_visible () && line.button_press (button)) {
792 moving_lines = true;
793 }
794 }
795
796 foreach (Line line in vertical_help_lines) {
797 if (!moving_lines && line.is_visible () && line.button_press (button)) {
798 moving_lines = true;
799 }
800 }
801
802 if (moving_lines) {
803 return;
804 }
805
806 if (move_canvas || DrawingTools.move_canvas.is_selected ()) {
807 view_is_moving = true;
808 move_offset_x = view_offset_x;
809 move_offset_y = view_offset_y;
810 } else {
811 Tool t = MainWindow.get_toolbox ().get_current_tool ();
812 t.press_action (t, (int) button, (int) ex, (int) ey);
813 }
814 }
815
816 /** Add new points to this path. */
817 public void set_active_path (Path p) {
818 path_list.remove (p);
819 path_list.add (p);
820 p.reopen ();
821 }
822
823 /** Insert new edit point for current path on the appropriate zoom
824 * level.
825 */
826 public PointSelection add_new_edit_point (int x, int y) {
827 return insert_edit_point (x, y);
828 }
829
830 /** Move view port centrum to this coordinate. */
831 public void set_center (double x, double y) {
832 x -= allocation.width / 2.0;
833 y -= allocation.height / 2.0;
834
835 view_offset_x += x / view_zoom;
836 view_offset_y += y / view_zoom;
837 }
838
839 public void set_zoom_from_area () {
840 double x = Math.fmin (zoom_x1, zoom_x2);
841 double y = Math.fmin (zoom_y1, zoom_y2);
842
843 double w = Math.fabs (zoom_x1 - zoom_x2);
844 double h = Math.fabs (zoom_y1 - zoom_y2);
845
846 double view_zoom_x, view_zoom_y;
847 double ivz, off;
848
849 if (Path.distance (x, x + w, y, y + h) < 7) {
850 zoom_in ();
851 } else {
852 view_offset_x += x / view_zoom;
853 view_offset_y += y / view_zoom;
854
855 if (unlikely (allocation.width == 0 || allocation.height == 0)) {
856 return;
857 }
858
859 view_zoom_x = allocation.width * view_zoom / w;
860 view_zoom_y = allocation.height * view_zoom / h;
861
862 // TODO: there is a max zoom level
863
864 if (view_zoom_x * allocation.width < view_zoom_y * allocation.height) {
865 view_zoom = view_zoom_x;
866 ivz = 1 / view_zoom;
867
868 off = (view_zoom / view_zoom_y) * allocation.height / view_zoom;
869 off = allocation.height/view_zoom - off;
870 off /= 2;
871
872 view_offset_y -= off;
873
874 } else {
875 view_zoom = view_zoom_y;
876 ivz = 1 / view_zoom;
877
878 off = (view_zoom / view_zoom_x) * allocation.width / view_zoom;
879 off = allocation.width / view_zoom - off;
880 off /= 2;
881
882 view_offset_x -= off;
883 }
884
885 zoom_area_is_visible = false;
886 store_current_view ();
887 }
888
889 update_zoom_bar ();
890 }
891
892 public void show_zoom_area (int sx, int sy, int nx, int ny) {
893 double x, y, w, h;
894
895 set_zoom_area (sx, sy, nx, ny);
896
897 zoom_area_is_visible = true;
898
899 x = Math.fmin (zoom_x1, zoom_x2) - 50;
900 y = Math.fmin (zoom_y1, zoom_y2) - 50;
901
902 w = Math.fabs (zoom_x1 - zoom_x2) + 100;
903 h = Math.fabs (zoom_y1 - zoom_y2) + 100;
904
905 redraw_area ((int)x, (int)y, (int)w, (int)h);
906 }
907
908 public void set_zoom_area (int sx, int sy, int nx, int ny) {
909 zoom_x1 = sx;
910 zoom_y1 = sy;
911 zoom_x2 = nx;
912 zoom_y2 = ny;
913 }
914
915 public static void validate_zoom () {
916 Glyph g = MainWindow.get_current_glyph ();
917 if (unlikely (g.view_zoom == 0)) {
918 warning ("Zoom is zero.");
919 g.reset_zoom ();
920
921 if (g.view_zoom == 0) {
922 g.view_zoom = 1;
923 g.view_offset_x = 0;
924 g.view_offset_y = 0;
925 }
926 }
927 }
928
929 public static double path_coordinate_x (double x) {
930 Glyph g = MainWindow.get_current_glyph ();
931 validate_zoom ();
932 return x * ivz () - xc () + g.view_offset_x;
933 }
934
935 public static int reverse_path_coordinate_x (double x) {
936 Glyph g = MainWindow.get_current_glyph ();
937 validate_zoom ();
938 return (int) Math.rint ((x - g.view_offset_x + xc ()) * g.view_zoom);
939 }
940
941 public static double precise_reverse_path_coordinate_x (double x) {
942 Glyph g = MainWindow.get_current_glyph ();
943 validate_zoom ();
944 return (x - g.view_offset_x + xc ()) * g.view_zoom;
945 }
946
947 public static double path_coordinate_y (double y) {
948 Glyph g = MainWindow.get_current_glyph ();
949 validate_zoom ();
950 return yc () - y * ivz () - g.view_offset_y;
951 }
952
953 public static int reverse_path_coordinate_y (double y) {
954 Glyph g = MainWindow.get_current_glyph ();
955 validate_zoom ();
956 y = Math.rint ((y + g.view_offset_y - yc ()) * g.view_zoom);
957 return (int) (-y);
958 }
959
960 public static double precise_reverse_path_coordinate_y (double y) {
961 Glyph g = MainWindow.get_current_glyph ();
962 validate_zoom ();
963 y = (y + g.view_offset_y - yc ()) * g.view_zoom;
964 return -y;
965 }
966
967 public bool select_path (double x, double y) {
968 Path? p = null;
969 bool found = false;
970
971 foreach (Path pt in path_list) {
972 if (pt.is_over (x, y)) {
973 p = pt;
974 found = true;
975 }
976 }
977
978 if (!KeyBindings.has_shift ()) {
979 clear_active_paths ();
980 }
981
982 add_active_path (p);
983
984 return found;
985 }
986
987 public bool is_over_selected_path (double x, double y) {
988 foreach (Path pt in active_paths) {
989 if (pt.is_over (x, y)) {
990 return true;
991 }
992 }
993 return false;
994 }
995
996 public void queue_redraw_path (Path path) {
997 redraw_path (path.xmin, path.ymin, path.xmax, path.ymax);
998 }
999
1000 private void redraw_path (double xmin, double ymin, double xmax, double ymax) {
1001 int yc = (int)(allocation.height / 2.0);
1002
1003 double yta = yc - ymin - view_offset_y;
1004 double ytb = yc - ymax - view_offset_y;
1005
1006 double xta = -view_offset_x - xmin;
1007 double xtb = -view_offset_x - xmax;
1008
1009 redraw_area ((int)xtb - 10, (int)yta - 10, (int)(xtb - xta) + 10, (int) (yta - ytb) + 10);
1010 }
1011
1012 public Path get_closeset_path (double x, double y) {
1013 double d;
1014 EditPoint ep = new EditPoint ();
1015
1016 Path min_point = new Path ();
1017 double min_distance = double.MAX;
1018
1019 double xt = path_coordinate_x (x);
1020 double yt = path_coordinate_y (y);
1021
1022 foreach (Path p in path_list) {
1023 if (p.is_over (xt, yt)) {
1024 return p;
1025 }
1026 }
1027
1028 foreach (Path p in path_list) {
1029 if (p.points.size == 0) continue;
1030
1031 p.get_closest_point_on_path (ep, xt, yt);
1032 d = Math.pow (ep.x - xt, 2) + Math.pow (ep.y - yt, 2);
1033
1034 if (d < min_distance) {
1035 min_distance = d;
1036 min_point = p;
1037 }
1038
1039 }
1040
1041 // a path without any editpoints
1042 if (path_list.size > 0) {
1043 return path_list.get (0);
1044 }
1045
1046 if (unlikely (min_distance == double.MAX)) {
1047 warning (@"No path found in path_list. Length: $(path_list.size)");
1048 }
1049
1050 return min_point;
1051 }
1052
1053 private PointSelection insert_edit_point (double x, double y) {
1054 double xt, yt;
1055 Path np;
1056 EditPoint inserted;
1057
1058 if (path_list.size == 0) {
1059 np = new Path ();
1060 path_list.add (np);
1061 }
1062
1063 xt = path_coordinate_x (x);
1064 yt = path_coordinate_y (y);
1065
1066 return_val_if_fail (path_list.size > 0, new PointSelection.empty ());
1067
1068 if (path_list.get (path_list.size - 1).is_open ()) {
1069 np = path_list.get (path_list.size - 1);
1070 np.add (xt, yt);
1071 } else {
1072 np = new Path ();
1073 path_list.add (np);
1074 np.add (xt, yt);
1075
1076 if (DrawingTools.pen_tool.is_selected ()) {
1077 np.set_stroke (PenTool.path_stroke_width);
1078 }
1079 }
1080
1081 clear_active_paths ();
1082 add_active_path (np);
1083
1084 inserted = np.points.get (np.points.size - 1);
1085
1086 return new PointSelection (inserted, np);
1087 }
1088
1089 public void move_selected_edit_point_coordinates (EditPoint selected_point, double xt, double yt) {
1090 double x, y;
1091
1092 BirdFont.get_current_font ().touch ();
1093
1094 x = reverse_path_coordinate_x (xt);
1095 y = reverse_path_coordinate_y (yt);
1096
1097 // redraw control point
1098 redraw_area ((int)(x - 4*view_zoom), (int)(y - 4*view_zoom), (int)(x + 3*view_zoom), (int)(y + 3*view_zoom));
1099
1100 // update position of selected point
1101 selected_point.set_position (xt, yt);
1102
1103 if (view_zoom >= 2) {
1104 redraw_area (0, 0, allocation.width, allocation.height);
1105 } else {
1106 redraw_last_stroke (x, y);
1107 }
1108 }
1109
1110 public void move_selected_edit_point (EditPoint selected_point, double x, double y) {
1111 double xt = path_coordinate_x (x);
1112 double yt = path_coordinate_y (y);
1113 move_selected_edit_point_coordinates (selected_point, xt, yt);
1114 }
1115
1116 public void redraw_segment (EditPoint a, EditPoint b) {
1117 double margin = 10;
1118 double x = Math.fmin (reverse_path_coordinate_x (a.x), reverse_path_coordinate_x (b.x)) - margin;
1119 double y = Math.fmin (reverse_path_coordinate_y (a.y), reverse_path_coordinate_y(b.y)) - margin;
1120 double w = Math.fabs (reverse_path_coordinate_x (a.x) - reverse_path_coordinate_x(b.x)) + 2 * margin;
1121 double h = Math.fabs (reverse_path_coordinate_y (a.y) - reverse_path_coordinate_y (b.y)) + 2 * margin;
1122
1123 redraw_area ((int)x, (int)y, (int)w, (int)h);
1124 }
1125
1126 private void redraw_last_stroke (double x, double y) {
1127 // redraw line, if we have more than one new point on path
1128 double px = 0;
1129 double py = 0;
1130 int tw = 0;
1131 int th = 0;
1132
1133 double xc = (allocation.width / 2.0);
1134
1135 if (active_paths.size == 0) {
1136 return;
1137 }
1138
1139 foreach (Path path in active_paths) {
1140 EditPoint p;
1141 EditPoint pl = path.get_last_point ();
1142
1143 if (pl.prev != null) {
1144 p = pl.get_prev ();
1145
1146 px = p.x + xc;
1147 py = p.y - xc;
1148
1149 tw = (px > x) ? (int) (px - x) : (int) (x - px);
1150 th = (py > y) ? (int) (py - y) : (int) (y - py);
1151
1152 if (px > x) px -= tw + 60;
1153 if (py > y) py -= th + 60;
1154
1155 } else {
1156 px = x - 60;
1157 py = y - 60;
1158 tw = 0;
1159 th = 0;
1160 }
1161 }
1162
1163 redraw_area ((int)px - 20, (int)py - 20, tw + 120, th + 120);
1164 }
1165
1166 public Path? get_last_path ()
1167 ensures (result != null)
1168 {
1169 return_val_if_fail (path_list.size > 0, null);
1170 return path_list.get (path_list.size - 1);
1171 }
1172
1173 public bool has_active_path () {
1174 return active_paths.size > 0;
1175 }
1176
1177 public bool is_open () {
1178 return open;
1179 }
1180
1181 /** Close all editable paths and return false if no path have been closed. */
1182 public bool close_path () {
1183 bool r = false;
1184
1185 foreach (var p in path_list) {
1186 if (p.is_editable ()) {
1187 r = true;
1188 p.set_editable (false);
1189 }
1190
1191 if (p.is_open ()) {
1192 p.convert_path_ending_to_line ();
1193 }
1194 }
1195
1196 redraw_area (0, 0, allocation.width, allocation.height);
1197 open = false;
1198 return r;
1199 }
1200
1201 public void open_path () {
1202 clear_active_paths ();
1203
1204 foreach (Path p in path_list) {
1205 p.set_editable (true);
1206 p.recalculate_linear_handles ();
1207 }
1208
1209 open = true;
1210 redraw_area (0, 0, allocation.width, allocation.height);
1211 }
1212
1213 public void redraw_path_region (Path p) {
1214 int x, y, w, h;
1215
1216 p.update_region_boundaries ();
1217
1218 x = reverse_path_coordinate_x (p.xmin);
1219 y = reverse_path_coordinate_x (p.xmin);
1220 w = reverse_path_coordinate_x (p.xmax) - x;
1221 h = reverse_path_coordinate_x (p.ymax) - y; // FIXME: redraw path
1222
1223 redraw_area (x, y, w, h);
1224 }
1225
1226 public Line get_line (string name) {
1227 foreach (Line line in vertical_help_lines) {
1228 if (likely (line.get_label () == name)) {
1229 return line;
1230 }
1231 }
1232
1233 foreach (Line line in horizontal_help_lines) {
1234 if (likely (line.get_label () == name)) {
1235 return line;
1236 }
1237 }
1238
1239 warning (@"No line with label $name found");
1240 return new Line ("Err");
1241 }
1242
1243 public override void zoom_in () {
1244 set_zoom_area (10, 10, allocation.width - 10, allocation.height - 10);
1245 set_zoom_from_area ();
1246 update_view ();
1247 update_zoom_bar ();
1248 }
1249
1250 public override void zoom_out () {
1251 double w = allocation.width;
1252 int n = (int) (10 * ((w - 10) / allocation.width));
1253 set_zoom_area (-n, -n, allocation.width + n, allocation.height + n);
1254 set_zoom_from_area ();
1255 update_view ();
1256 update_zoom_bar ();
1257 }
1258
1259 public override void zoom_max () {
1260 default_zoom ();
1261 update_zoom_bar ();
1262 }
1263
1264 public override void zoom_min () {
1265 double ax = 1000;
1266 double ay = 1000;
1267 double bx = -1000;
1268 double by = -1000;
1269
1270 int iax, iay, ibx, iby;
1271
1272 reset_zoom ();
1273
1274 foreach (var p in path_list) {
1275 p.update_region_boundaries ();
1276
1277 if (p.points.size > 2) {
1278 if (p.xmin < ax) ax = p.xmin;
1279 if (p.ymin < ay) ay = p.ymin;
1280 if (p.xmax > bx) bx = p.xmax;
1281 if (p.ymax > by) by = p.ymax;
1282 }
1283 }
1284
1285 if (ax == 1000) return; // empty page
1286
1287 iax = (int) ((ax + view_offset_x + allocation.width / 2.0) * view_zoom);
1288 iay = (int) ((-ay + view_offset_y + allocation.height / 2.0) * view_zoom);
1289 ibx = (int) ((bx + view_offset_x + allocation.width / 2.0) * view_zoom);
1290 iby = (int) ((-by + view_offset_y + allocation.height / 2.0) * view_zoom);
1291
1292 show_zoom_area (iax, iay, ibx, iby); // set this later on button release
1293 set_zoom_from_area ();
1294 zoom_out (); // add some margin
1295
1296 redraw_area (0, 0, allocation.width, allocation.height);
1297 update_zoom_bar ();
1298 }
1299
1300 public override void store_current_view () {
1301 ZoomView n;
1302
1303 if (zoom_list_index + 1 < zoom_list.size) {
1304 n = zoom_list.get (zoom_list_index);
1305 while (n != zoom_list.get (zoom_list.size - 1)) {
1306 zoom_list.remove_at (zoom_list.size - 1);
1307 }
1308 }
1309
1310 zoom_list.add (new ZoomView (view_offset_x, view_offset_y, view_zoom, allocation));
1311 zoom_list_index = (int) zoom_list.size - 1;
1312
1313 if (zoom_list.size > 50) {
1314 zoom_list.remove_at (0);
1315 }
1316 }
1317
1318 public override void restore_last_view () {
1319 if (zoom_list.size == 0 || zoom_list_index - 1 < 0 || zoom_list.size == 0) {
1320 return;
1321 }
1322
1323 zoom_list_index--;
1324
1325 ZoomView z = zoom_list.get (zoom_list_index);
1326
1327 view_offset_x = z.x;
1328 view_offset_y = z.y;
1329 view_zoom = z.zoom;
1330 allocation = z.allocation;
1331
1332 update_zoom_bar ();
1333 }
1334
1335 public override void next_view () {
1336 ZoomView z;
1337
1338 if (zoom_list.size == 0 || zoom_list_index + 1 >= zoom_list.size) {
1339 return;
1340 }
1341
1342 zoom_list_index++;
1343
1344 z = zoom_list.get (zoom_list_index);
1345
1346 view_offset_x = z.x;
1347 view_offset_y = z.y;
1348 view_zoom = z.zoom;
1349 allocation = z.allocation;
1350
1351 update_zoom_bar ();
1352 }
1353
1354 public override void reset_zoom () {
1355 view_offset_x = 0;
1356 view_offset_y = 0;
1357
1358 set_zoom (1);
1359
1360 store_current_view ();
1361 update_zoom_bar ();
1362 }
1363
1364 /** Get x-height or top line. */
1365 public Line get_upper_line () {
1366 if (has_top_line () || xheight_lines_visible) {
1367 return get_line ("top");
1368 }
1369
1370 return get_line ("x-height");
1371 }
1372
1373 /** Get base line. */
1374 public Line get_lower_line () {
1375 return get_line ("baseline");
1376 }
1377
1378 /** Set default zoom. See default_zoom. */
1379 public void set_default_zoom () {
1380 int bottom = 0;
1381 int top = 0;
1382 int left = 0;
1383 int right = 0;
1384
1385 return_if_fail (vertical_help_lines.size != 0);
1386 return_if_fail (horizontal_help_lines.size != 0);
1387
1388 reset_zoom ();
1389
1390 bottom = get_lower_line ().get_position_pixel ();
1391 top = get_upper_line ().get_position_pixel ();
1392
1393 left = vertical_help_lines.get (vertical_help_lines.size - 1).get_position_pixel ();
1394 right = vertical_help_lines.get (0).get_position_pixel ();
1395
1396 set_zoom_area (left + 10, top - 10, right - 10, bottom + 10);
1397 set_zoom_from_area ();
1398 }
1399
1400 /** Set default zoom and redraw canvas. */
1401 public void default_zoom () {
1402 set_default_zoom ();
1403 update_view ();
1404 }
1405
1406 public bool is_empty () {
1407 foreach (Path p in path_list) {
1408 if (p.points.size > 0) {
1409 return false;
1410 }
1411 }
1412
1413 return true;
1414 }
1415
1416 public void set_zoom (double z)
1417 requires (z > 0)
1418 {
1419 view_zoom = z;
1420 }
1421
1422 public void set_background_visible (bool visibility) {
1423 background_image_visible = visibility;
1424 }
1425
1426 public bool get_background_visible () {
1427 return background_image_visible;
1428 }
1429
1430 private void draw_coordinate (Context cr) {
1431 cr.set_source_rgba (0.5, 0.5, 0.5, 1);
1432 cr.set_font_size (12);
1433 cr.move_to (0, 10);
1434 cr.show_text (@"($motion_x, $motion_y)");
1435 cr.stroke ();
1436 }
1437
1438 /** Draw filled paths. */
1439 public void draw_paths (Context cr) {
1440 PathList stroke;
1441
1442 cr.save ();
1443 cr.new_path ();
1444 foreach (Path p in path_list) {
1445 if (p.stroke > 0) {
1446 stroke = StrokeTool.get_stroke (p, p.stroke);
1447 draw_path_list (stroke, cr, Color.black ());
1448 } else {
1449 p.draw_path (cr, this, Color.black ());
1450 }
1451 }
1452 cr.fill ();
1453 cr.restore ();
1454 }
1455
1456 public void draw_path (Context cr) {
1457 if (is_open () && Path.fill_open_path) {
1458 cr.save ();
1459 cr.new_path ();
1460 foreach (Path p in path_list) {
1461 if (p.stroke > 0) {
1462 draw_path_list (StrokeTool.get_stroke (p, p.stroke), cr, get_path_fill_color ());
1463 }
1464
1465 p.draw_path (cr, this, get_path_fill_color ());
1466 }
1467 cr.fill ();
1468 cr.restore ();
1469 }
1470
1471 if (is_open ()) {
1472 cr.save ();
1473 cr.new_path ();
1474 foreach (Path p in path_list) {
1475 if (p.stroke > 0) {
1476 draw_outline_for_paths (StrokeTool.get_stroke (p, p.stroke), cr);
1477 }
1478
1479 p.draw_outline (cr);
1480 p.draw_edit_points (cr);
1481 }
1482 cr.restore ();
1483 }
1484
1485 if (!is_open ()) {
1486 // This was good for testing but it is way too slow:
1487 // Svg.draw_svg_path (cr, get_svg_data (), Glyph.xc () + left, Glyph.yc () - baseline);
1488
1489 cr.save ();
1490 cr.new_path ();
1491 foreach (Path p in path_list) {
1492 if (p.stroke == 0) {
1493 p.draw_path (cr, this, Color.black ());
1494 } else {
1495 draw_path_list (StrokeTool.get_stroke (p, p.stroke), cr, Color.black ());
1496 }
1497 }
1498 cr.close_path ();
1499 cr.fill ();
1500 cr.restore ();
1501
1502 foreach (Path p in active_paths) {
1503 cr.save ();
1504 cr.new_path ();
1505 if (p.stroke == 0) {
1506 p.draw_path (cr, this);
1507 } else {
1508 draw_path_list (StrokeTool.get_stroke (p, p.stroke), cr);
1509 }
1510 cr.close_path ();
1511 cr.fill ();
1512 cr.restore ();
1513 }
1514 }
1515 }
1516
1517 private Color get_path_fill_color () {
1518 return new Color (Path.fill_color_r, Path.fill_color_g, Path.fill_color_b, Path.fill_color_a);
1519 }
1520
1521 private void draw_outline_for_paths (PathList pl, Context cr) {
1522 foreach (Path p in pl.paths) {
1523 p.draw_outline (cr);
1524 }
1525 }
1526
1527 private void draw_path_list (PathList pl, Context cr, Color? c = null) {
1528 foreach (Path p in pl.paths) {
1529 p.draw_path (cr, this, c);
1530 }
1531 }
1532
1533 private void draw_zoom_area(Context cr) {
1534 cr.save ();
1535 cr.set_line_width (2.0);
1536 cr.set_source_rgba (0, 0, 1, 0.3);
1537 cr.rectangle (Math.fmin (zoom_x1, zoom_x2), Math.fmin (zoom_y1, zoom_y2), Math.fabs (zoom_x1 - zoom_x2), Math.fabs (zoom_y1 - zoom_y2));
1538 cr.stroke ();
1539 cr.restore ();
1540 }
1541
1542 private void draw_background_color (Context cr, double opacity) {
1543 cr.save ();
1544 cr.rectangle (0, 0, allocation.width, allocation.height);
1545 cr.set_line_width (0);
1546 cr.set_source_rgba (1, 1, 1, opacity);
1547 cr.fill ();
1548 cr.stroke ();
1549 cr.restore ();
1550 }
1551
1552 private void draw_help_lines (Context cr) {
1553 foreach (Line line in get_all_help_lines ()) {
1554 cr.save ();
1555 line.draw (cr, allocation);
1556 cr.restore ();
1557 }
1558 }
1559
1560 public void set_allocation (WidgetAllocation a) {
1561 allocation = a;
1562 }
1563
1564 public override void draw (WidgetAllocation allocation, Context cr) {
1565 Tool tool;
1566
1567 this.allocation = allocation;
1568
1569 ImageSurface ps = new ImageSurface (Format.ARGB32, allocation.width, allocation.height);
1570 Context cmp = new Context (ps);
1571
1572 cr.save ();
1573 draw_background_color (cr, 1);
1574 cr.restore ();
1575
1576 if (background_image != null && background_image_visible) {
1577 ((!)background_image).draw (cr, allocation, view_offset_x, view_offset_y, view_zoom);
1578 }
1579
1580 if (unlikely (Preferences.draw_boundaries)) {
1581 foreach (Path p in path_list) {
1582 p.draw_boundaries (cr);
1583 }
1584 }
1585
1586 draw_background_glyph (allocation, cr);
1587 juxtapose (allocation, cr);
1588
1589 if (BirdFont.show_coordinates) {
1590 draw_coordinate (cmp);
1591 }
1592
1593 if (show_help_lines) {
1594 cmp.save ();
1595 draw_help_lines (cmp);
1596 cmp.restore ();
1597 }
1598
1599 if (zoom_area_is_visible) {
1600 cmp.save ();
1601 draw_zoom_area (cmp);
1602 cmp.restore ();
1603 }
1604
1605 if (!is_empty ()) {
1606 cmp.save ();
1607 cmp.scale (view_zoom, view_zoom);
1608 cmp.translate (-view_offset_x, -view_offset_y);
1609 draw_path (cmp);
1610 cmp.restore ();
1611 }
1612
1613 cmp.save ();
1614 tool = MainWindow.get_toolbox ().get_current_tool ();
1615 tool.draw_action (tool, cmp, this);
1616 cmp.restore ();
1617
1618 cr.save ();
1619 cr.set_source_surface (ps, 0, 0);
1620 cr.paint ();
1621 cr.restore ();
1622 }
1623
1624 private void zoom_in_at_point (double x, double y) {
1625 int n = -10;
1626 zoom_at_point (x, y, n);
1627 }
1628
1629 private void zoom_out_at_point (double x, double y) {
1630 int n = (int) (10.0 * ((allocation.width - 10.0) / allocation.width));
1631 zoom_at_point (x, y, n);
1632 }
1633
1634 public void zoom_tap (double distance) {
1635 int w = (int) (distance);
1636 if (distance != 0) {
1637 show_zoom_area (-w , -w, allocation.width + w, allocation.height + w);
1638 set_zoom_from_area ();
1639 }
1640 }
1641
1642 /** Zoom in @param n pixels. */
1643 private void zoom_at_point (double x, double y, int n) {
1644 double w = allocation.width;
1645 double h = allocation.height;
1646
1647 double rx = Math.fabs (w / 2 - x) / (w / 2);
1648 double ry = Math.fabs (h / 2 - y) / (h / 2);
1649
1650 int xd = (x < w / 2) ? (int) (n * rx) : (int) (-n * rx);
1651 int yd = (y < h / 2) ? (int) (n * ry) : (int) (-n * ry);
1652
1653 show_zoom_area (-n + xd, -n + yd, allocation.width + n + xd, allocation.height + n + yd);
1654 set_zoom_from_area ();
1655 }
1656
1657 private void move_view_offset (double x, double y) {
1658 view_offset_x = move_offset_x + (pointer_begin_x - x) * (1/view_zoom);
1659 view_offset_y = move_offset_y + (pointer_begin_y - y) * (1/view_zoom);
1660 redraw_area (0, 0, allocation.width, allocation.height);
1661 }
1662
1663 public void store_undo_state (bool clear_redo = false) {
1664 Glyph g = copy ();
1665 undo_list.add (g);
1666
1667 if (clear_redo) {
1668 redo_list.clear ();
1669 }
1670 }
1671
1672 public void store_redo_state () {
1673 Glyph g = copy ();
1674 redo_list.add (g);
1675 }
1676
1677 public Glyph copy () {
1678 Glyph g = new Glyph.no_lines (name, unichar_code);
1679
1680 g.left_limit = left_limit;
1681 g.right_limit = right_limit;
1682
1683 g.remove_lines ();
1684
1685 foreach (Line line in get_all_help_lines ()) {
1686 g.add_line (line.copy ());
1687 }
1688
1689 foreach (Path p in path_list) {
1690 g.add_path (p.copy ());
1691 }
1692
1693 foreach (Path p in active_paths) {
1694 g.active_paths.add (p);
1695 }
1696
1697 if (background_image != null) {
1698 g.background_image = ((!) background_image).copy ();
1699 }
1700
1701 g.empty = empty;
1702 g.open = open;
1703
1704 return g;
1705 }
1706
1707 public void reload () {
1708 Font f = BirdFont.get_current_font ();
1709
1710 if (f.has_glyph (name)) {
1711 set_glyph_data ((!) f.get_glyph (name));
1712 }
1713 }
1714
1715 public override void undo () {
1716 Glyph g;
1717
1718 if (undo_list.size == 0) {
1719 return;
1720 }
1721
1722 g = undo_list.get (undo_list.size - 1);
1723
1724 store_redo_state ();
1725 set_glyph_data (g);
1726
1727 undo_list.remove_at (undo_list.size - 1);
1728
1729 PenTool.update_selected_points ();
1730
1731 clear_active_paths ();
1732 }
1733
1734 public override void redo () {
1735 Glyph g;
1736
1737 if (redo_list.size == 0) {
1738 return;
1739 }
1740
1741 g = redo_list.get (redo_list.size - 1);
1742
1743 store_undo_state (false);
1744 set_glyph_data (g);
1745
1746 redo_list.remove_at (redo_list.size - 1);
1747
1748 PenTool.update_selected_points ();
1749
1750 clear_active_paths ();
1751 }
1752
1753 void set_glyph_data (Glyph g) {
1754 path_list.clear ();
1755
1756 foreach (Path p in g.path_list) {
1757 add_path (p);
1758 p.update_region_boundaries ();
1759 }
1760
1761 remove_lines ();
1762 foreach (Line line in g.get_all_help_lines ()) {
1763 add_line (line.copy ());
1764 }
1765
1766 add_help_lines ();
1767
1768 if (g.background_image != null) {
1769 background_image = ((!) g.background_image).copy ();
1770 }
1771
1772 clear_active_paths ();
1773 foreach (Path p in g.active_paths) {
1774 add_active_path (p);
1775 }
1776
1777 redraw_area (0, 0, allocation.width, allocation.height);
1778 }
1779
1780 public ImageSurface get_thumbnail () {
1781 ImageSurface img;
1782 Context cr;
1783 double gx, gy;
1784 double x1, x2, y1, y2;
1785 Font font = BirdFont.get_current_font ();
1786
1787 remove_empty_paths ();
1788 boundaries (out x1, out y1, out x2, out y2);
1789
1790 if (x2 - x1 < 1 || y2 - y1 < 1) { // create an empty thumbnail
1791 img = new ImageSurface (Format.ARGB32, 100, 100);
1792 } else {
1793 img = new ImageSurface (Format.ARGB32, (int) (x2 - x1), (int) (y2 - y1));
1794 }
1795
1796 gx = left_limit - x1;
1797 gy = (y2 - y1) + font.base_line + y1;
1798
1799 cr = new Context (img);
1800
1801 cr.save ();
1802 cr.set_source_rgba (0, 0, 0, 1);
1803 Svg.draw_svg_path (cr, get_svg_data (), gx, gy);
1804 cr.restore ();
1805 return img;
1806 }
1807
1808 /** Split curve in two parts and add a new point in between.
1809 * @return the new point
1810 */
1811 public void insert_new_point_on_path (double x, double y) {
1812 double min, distance;
1813 Path? p = null;
1814 Path path;
1815 EditPoint? np = null;
1816 EditPoint lep;
1817
1818 double xt;
1819 double yt;
1820
1821 xt = x * ivz () - xc () + view_offset_x;
1822 yt = yc () - y * ivz () - view_offset_y;
1823
1824 min = double.MAX;
1825
1826 foreach (Path pp in path_list) {
1827 lep = new EditPoint ();
1828 pp.get_closest_point_on_path (lep, xt, yt);
1829 distance = Math.sqrt (Math.pow (Math.fabs (xt - lep.x), 2) + Math.pow (Math.fabs (yt - lep.y), 2));
1830
1831 if (distance < min) {
1832 min = distance;
1833 p = pp;
1834 np = lep;
1835 }
1836 }
1837
1838 if (p == null) {
1839 return;
1840 }
1841
1842 path = (!) p;
1843
1844 lep = new EditPoint ();
1845 path.get_closest_point_on_path (lep, xt, yt);
1846 path.insert_new_point_on_path (lep);
1847 }
1848
1849 static bool in_range (double offset_x, double coordinate_x1, double coordinate_x2) {
1850 return coordinate_x1 <= offset_x <= coordinate_x2;
1851 }
1852
1853 public void juxtapose (WidgetAllocation allocation, Context cr) {
1854 string glyph_sequence = Preferences.get ("glyph_sequence");
1855 unichar c;
1856 Font font = BirdFont.get_current_font ();
1857 Glyph glyph = MainWindow.get_current_glyph ();
1858 Glyph juxtaposed;
1859 StringBuilder current = new StringBuilder ();
1860 int pos;
1861 string name;
1862 double x, kern;
1863 double left, baseline;
1864 string last_name;
1865
1866 double box_x1, box_x2, box_y1, box_y2;
1867 double marker_x, marker_y;
1868
1869 KerningClasses classes = font.get_kerning_classes ();
1870
1871 x = 0;
1872
1873 box_x1 = path_coordinate_x (0);
1874 box_y1 = path_coordinate_y (0);
1875 box_x2 = path_coordinate_x (allocation.width);
1876 box_y2 = path_coordinate_y (allocation.height);
1877
1878 current.append_unichar (glyph.unichar_code);
1879 pos = glyph_sequence.index_of (current.str);
1880
1881 baseline = font.base_line;;
1882 left = glyph.get_line ("left").pos;
1883
1884 x = glyph.get_width ();
1885 last_name = glyph.name;
1886 for (int i = pos + 1; i < glyph_sequence.char_count (); i++) {
1887 c = glyph_sequence.get_char (i);
1888 name = font.get_name_for_character (c);
1889 juxtaposed = (font.has_glyph (name)) ? (!) font.get_glyph (name) : font.get_space ().get_current ();
1890
1891 if (font.has_glyph (last_name) && font.has_glyph (name)) {
1892 kern = classes.get_kerning (last_name, name);
1893 } else {
1894 kern = 0;
1895 }
1896
1897 if (!juxtaposed.is_empty ()
1898 && (in_range (left + x + kern, box_x1, box_x2) // the letter is visible
1899 || in_range (left + x + kern + juxtaposed.get_width (), box_x1, box_x2))) {
1900
1901 marker_x = Glyph.xc () + left + x + kern - glyph.view_offset_x;
1902 marker_y = Glyph.yc () - baseline - glyph.view_offset_y;
1903
1904 cr.save ();
1905 cr.scale (glyph.view_zoom, glyph.view_zoom);
1906 cr.set_source_rgba (0, 0, 0, 1);
1907
1908 Svg.draw_svg_path (cr, juxtaposed.get_svg_data (), marker_x, marker_y);
1909 cr.restore ();
1910 }
1911
1912 x += juxtaposed.get_width () + kern;
1913
1914 last_name = name;
1915 }
1916
1917 x = 0;
1918 last_name = glyph.name;
1919 for (int i = pos - 1; i >= 0; i--) {
1920 c = glyph_sequence.get_char (i);
1921 name = font.get_name_for_character (c);
1922 juxtaposed = (font.has_glyph (name)) ? (!) font.get_glyph (name) : font.get_space ().get_current ();
1923
1924 if (font.has_glyph (last_name) && font.has_glyph (name)) {
1925 kern = classes.get_kerning (name, last_name);
1926 } else {
1927 kern = 0;
1928 }
1929
1930 x -= juxtaposed.get_width ();
1931 x -= kern;
1932
1933 marker_x = Glyph.xc () + left + x;
1934 marker_y = Glyph.yc () - baseline;
1935 if (!juxtaposed.is_empty ()
1936 &&(in_range (left + x, box_x1, box_x2)
1937 || in_range (left + x + juxtaposed.get_width (), box_x1, box_x2))) {
1938 cr.save ();
1939 cr.scale (glyph.view_zoom, glyph.view_zoom);
1940 cr.translate (-glyph.view_offset_x, -glyph.view_offset_y);
1941 cr.set_source_rgba (0, 0, 0, 1);
1942 Svg.draw_svg_path (cr, juxtaposed.get_svg_data (), marker_x, marker_y);
1943 cr.restore ();
1944 }
1945
1946 last_name = name;
1947 }
1948 }
1949
1950 /** @return left side bearing */
1951 public double get_lsb () {
1952 return get_line ("left").pos;
1953 }
1954
1955 /** @return bottom line */
1956 public double get_baseline () {
1957 Font font = BirdFont.get_current_font ();
1958 return font.base_line;
1959 }
1960
1961 void draw_background_glyph (WidgetAllocation allocation, Context cr) {
1962 double left, baseline, current_left;
1963 Glyph g;
1964 Font font = BirdFont.get_current_font ();
1965
1966 current_left = get_line ("left").pos;
1967
1968 if (background_glyph != null) {
1969 g = (!) background_glyph;
1970 baseline = font.base_line;
1971 left = g.get_line ("left").pos;
1972 cr.save ();
1973 cr.scale (view_zoom, view_zoom);
1974 cr.translate (-view_offset_x, -view_offset_y);
1975 cr.set_source_rgba (0.2, 0.2, 0.2, 0.5);
1976 Svg.draw_svg_path (cr, g.get_svg_data (),
1977 Glyph.xc () + left - (left - current_left) ,
1978 Glyph.yc () - baseline);
1979 cr.restore ();
1980 }
1981
1982 }
1983
1984 public string get_hex () {
1985 return Font.to_hex_code (unichar_code);
1986 }
1987
1988 public override void move_view (double x, double y) {
1989 view_offset_x += x / view_zoom;
1990 view_offset_y += y / view_zoom;
1991 GlyphCanvas.redraw ();
1992 }
1993
1994 /** Scroll or zoom from tap events. */
1995 public void change_view_event (int finger, int x, int y) {
1996 double dx, dy;
1997 double last_distance, new_distance;
1998
1999 dx = 0;
2000 dy = 0;
2001
2002 new_distance = 0;
2003
2004 if (last_tap0_y == -1 || last_tap0_x == -1 || last_tap1_y == -1 || last_tap1_x == -1) {
2005 return;
2006 }
2007
2008 if (finger == 0) {
2009 dx = last_tap0_x - x;
2010 dy = last_tap0_y - y;
2011 new_distance = Path.distance (last_tap1_x, x, last_tap1_y, y);
2012 }
2013
2014 if (finger == 1) {
2015 dx = last_tap1_x - x;
2016 dy = last_tap1_y - y;
2017 new_distance = Path.distance (last_tap0_x, x, last_tap0_y, y);
2018 }
2019
2020 last_distance = Path.distance (last_tap0_x, last_tap1_x, last_tap0_y, last_tap1_y);
2021
2022 if (zoom_distance != 0) {
2023 zoom_tap (zoom_distance - new_distance);
2024 }
2025
2026 if (finger == 1) {
2027 warning (@"dx $dx dy $dy last_tap1_x $last_tap1_x last_tap1_y $last_tap1_y x $x y $y");
2028 move_view (dx, dy);
2029 }
2030
2031 zoom_distance = new_distance;
2032 }
2033
2034 public override void tap_down (int finger, int x, int y) {
2035 TimeoutSource delay;
2036
2037 if (finger == 0) {
2038 delay = new TimeoutSource (400); // wait for second finger
2039 delay.set_callback(() => {
2040 if (!change_view && !ignore_input) {
2041 button_press (1, x, y);
2042 }
2043 return false;
2044 });
2045 delay.attach (null);
2046
2047 last_tap0_x = x;
2048 last_tap0_y = y;
2049 }
2050
2051 if (finger == 1) {
2052 change_view = true;
2053 last_tap1_x = x;
2054 last_tap1_y = y;
2055 }
2056 }
2057
2058 public override void tap_up (int finger, int x, int y) {
2059 if (finger == 0) {
2060 button_release (1, x, y);
2061
2062 last_tap0_x = -1;
2063 last_tap0_y = -1;
2064 }
2065
2066 if (finger == 1) {
2067 last_tap1_x = -1;
2068 last_tap1_y = -1;
2069
2070 change_view = false;
2071 zoom_distance = 0;
2072 }
2073 }
2074
2075 public override void tap_move (int finger, int x, int y) {
2076 if (!change_view) {
2077 motion_notify (x, y);
2078 } else {
2079 change_view_event (finger, x, y);
2080 }
2081
2082 if (finger == 0) {
2083 last_tap0_x = x;
2084 last_tap0_y = y;
2085 }
2086
2087 if (finger == 1) {
2088 last_tap1_x = x;
2089 last_tap1_y = y;
2090 }
2091 }
2092
2093 public void update_spacing_class () {
2094 Font font = BirdFont.get_current_font ();
2095 GlyphCollection? g;
2096 GlyphCollection gc;
2097 Glyph glyph;
2098
2099 foreach (string l in font.get_spacing ()
2100 .get_all_connections ((!) unichar_code.to_string ())) {
2101 if (l != (!) unichar_code.to_string ()) {
2102 g = font.get_glyph_collection (l);
2103 if (g != null) {
2104 gc = (!) g;
2105 glyph = gc.get_current ();
2106 left_limit = glyph.left_limit;
2107 right_limit = glyph.right_limit;
2108 break;
2109 }
2110 }
2111 }
2112
2113 add_help_lines ();
2114 }
2115
2116 public void update_other_spacing_classes () {
2117 Font font = BirdFont.get_current_font ();
2118 GlyphCollection? g;
2119 GlyphCollection gc;
2120 Glyph glyph;
2121
2122 foreach (string l in font.get_spacing ()
2123 .get_all_connections ((!) unichar_code.to_string ())) {
2124 if (l != (!) unichar_code.to_string ()) {
2125 g = font.get_glyph_collection (l);
2126 if (g != null) {
2127 gc = (!) g;
2128 glyph = gc.get_current ();
2129 glyph.left_limit = left_limit;
2130 glyph.right_limit = right_limit;
2131 glyph.add_help_lines ();
2132 }
2133 }
2134 }
2135 }
2136
2137 public void set_cache (int64 font_size, Surface cache) {
2138 glyph_cache.set (font_size, cache);
2139 }
2140
2141 public bool has_cache (int64 font_size) {
2142 return glyph_cache.has_key (font_size);
2143 }
2144
2145 public Surface get_cache (int64 font_size) {
2146 if (unlikely (!has_cache (font_size))) {
2147 warning ("No cache for glyph.");
2148 return new ImageSurface (Cairo.Format.ARGB32, 1, 1);
2149 }
2150
2151 return glyph_cache.get (font_size);
2152 }
2153
2154 public void add_custom_guide () {
2155 TextListener listener;
2156
2157 listener = new TextListener (t_("Guide"), "", t_("Add"));
2158
2159 listener.signal_text_input.connect ((text) => {
2160 new_guide_name = text;
2161 });
2162
2163 listener.signal_submit.connect (() => {
2164 Line guide;
2165 double position;
2166
2167 position = path_coordinate_y (allocation.height / 2.0);
2168 guide = new Line (new_guide_name, position);
2169 horizontal_help_lines.add (guide);
2170
2171 BirdFont.get_current_font ().custom_guides.add (guide);
2172
2173 MainWindow.native_window.hide_text_input ();
2174 GlyphCanvas.redraw ();
2175 });
2176
2177 MainWindow.native_window.set_text_listener (listener);
2178 }
2179 }
2180
2181 }
2182