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