The Birdfont Source Code


All Repositories / birdfont.git / blob – RSS feed

Glyph.vala in libbirdfont

This file is a part of the Birdfont project.

Contributing

Send patches or pull requests to johan.mattsson.m@gmail.com.
Clone this repository: git clone https://github.com/johanmattssonm/birdfont.git

Revisions

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