Clone in a terminal:
Glyph.vala
/*
	Copyright (C) 2012 - 2016 Johan Mattsson

	This library is free software; you can redistribute it and/or modify
	it under the terms of the GNU Lesser General Public License as
	published by the Free Software Foundation; either version 3 of the
	License, or (at your option) any later version.

	This library is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
	Lesser General Public License for more details.
*/

using Cairo;
using Math;
using Gee;

namespace BirdFont {

public class Glyph : FontDisplay {

	// Background image
	BackgroundImage? background_image = null;
	bool background_image_visible = true;

	// Glyph zoom level
	public double view_zoom = 0.1;

	public double view_offset_x = 0;
	public double view_offset_y = 0;
	Gee.ArrayList<ZoomView> zoom_list = new Gee.ArrayList<ZoomView> ();
	int zoom_list_index = 0;

	// The point where edit event begun
	double pointer_begin_x = 0;
	double pointer_begin_y = 0;

	// Tap last tap event position in pixels
	int last_tap0_y = 0;
	int last_tap0_x = 0;
	int last_tap1_y = 0;
	int last_tap1_x = 0;
	double zoom_distance = 0;
	bool change_view;

	bool ignore_input = false;

	// Current pointer position
	public double motion_x = 0;
	public double motion_y = 0;

	// Zoom area
	public double zoom_x1 = 0;
	public double zoom_y1 = 0;
	public double zoom_x2 = 0;
	public double zoom_y2 = 0;
	public bool zoom_area_is_visible = false;

	bool view_is_moving = false;
	public double move_offset_x = 0;
	public double move_offset_y = 0;
	bool move_canvas = false;

	public WidgetAllocation allocation = new WidgetAllocation ();

	// FIXME: name and unichar should be moved to to glyph collection
	public unichar unichar_code = 0; 
	public string name;

	public double left_limit {
		get {
			return _left_limit;
		}

		set {
			ttf_data = null;
			_left_limit = value;
		}
	}

	public double right_limit {
		get {
			return _right_limit;
		}

		set {
			ttf_data = null;
			_right_limit = value;
		}
	}

	private double _right_limit = 0;
	private double _left_limit = 0;

	// x-height, lsb, etc.
	public Gee.ArrayList<Line> vertical_help_lines = new Gee.ArrayList<Line> ();
	public Gee.ArrayList<Line> horizontal_help_lines = new Gee.ArrayList<Line> ();
	public bool show_help_lines = true;
	bool xheight_lines_visible = false;
	bool margin_boundaries_visible = false;
	string new_guide_name = "";

	Gee.ArrayList<Glyph> undo_list = new Gee.ArrayList<Glyph> ();
	Gee.ArrayList<Glyph> redo_list = new Gee.ArrayList<Glyph> ();

	string glyph_sequence = "";
	bool open = true;

	public static Glyph? background_glyph = null;

	bool empty = false;

	/** Id in the version list. */
	public int version_id = 0;

	/** Cache quadratic form on export. */
	GlyfData? ttf_data = null;

	Line left_line;
	Line right_line;

	/** Cache for Cairo surface rendering */
	HashMap<string, Surface> glyph_cache = new HashMap<string, Surface> ();

	public const double CANVAS_MIN = -10000;
	public const double CANVAS_MAX = 10000;

	public static bool show_orientation_arrow = false;
	public static double orientation_arrow_opacity = 1;

	public Layer layers = new Layer ();
	public int current_layer = 0;
	public Gee.ArrayList<Path> active_paths = new Gee.ArrayList<Path> ();
	public Gee.ArrayList<Layer> selected_groups = new Gee.ArrayList<Layer> ();

	// used if this glyph is fetched from a fallback font
	public double top_limit = 0;
	public double baseline = 0;
	public double bottom_limit = 0;

	public Surface? overview_thumbnail = null;

	public Glyph (string name, unichar unichar_code = 0) {
		this.name = name;
		this.unichar_code = unichar_code;

		add_help_lines ();

		left_limit = -28;
		right_limit = 28;
	}

	public Glyph.no_lines (string name, unichar unichar_code = 0) {
		this.name = name;
		this.unichar_code = unichar_code;
	}

	public Gee.ArrayList<Path> get_active_paths () {
		return active_paths;
	}

	public Layer get_current_layer () {
		return_val_if_fail (0 <= current_layer < layers.subgroups.size, new Layer ());
		return layers.subgroups.get (current_layer);
	}

	public void set_current_layer (Layer layer) {
		int i = 0;
		foreach (Layer l in layers.subgroups) {
			if (likely (l == layer)) {
				current_layer = i;
				return;
			}
			i++;
		}

		warning ("Layer is not added to glyph.");
	}

	public Gee.ArrayList<Path> get_visible_paths () {
		return layers.get_visible_paths ().paths;
	}

	public PathList get_visible_path_list () {
		return layers.get_visible_paths ();
	}

	public Gee.ArrayList<Path> get_paths_in_current_layer () {
		return get_current_layer ().get_all_paths ().paths;
	}

	public Gee.ArrayList<Path> get_all_paths () {
		return layers.get_all_paths ().paths;
	}

	public void add_new_layer () {
		layers.add_layer (new Layer ());
		current_layer = layers.subgroups.size - 1;
	}

	public int get_layer_index (Layer layer) {
		return layers.index_of (layer);
	}

	public GlyfData get_ttf_data () {
		if (ttf_data == null) {

			ttf_data =  new GlyfData (this);
		}

		return (!) ttf_data;
	}

	public PathList get_quadratic_paths () {
		PointConverter pc;
		PathList pl;
		PathList stroke;

		pl = new PathList ();

		foreach (Path p in get_visible_paths ()) {
			if (p.stroke > 0) {
				stroke = p.get_completed_stroke ();
				foreach (Path stroke_part in stroke.paths) {
					pc = new PointConverter (stroke_part);
					pl.add (pc.get_quadratic_path ());
				}
			} else {
				pc = new PointConverter (p);
				pl.add (pc.get_quadratic_path ());
			}
		}

		return pl;
	}

	public override void close () {
		undo_list.clear ();
		redo_list.clear ();
	}

	public void set_empty_ttf (bool e) {
		empty = e;
	}

	public bool is_empty_ttf () {
		return empty;
	}

	public void clear_active_paths () {
		selected_groups.clear ();
		active_paths.clear ();
	}

	public void add_active_path (Layer? group, Path? p) {
		Path path;
		Layer g;

		if (p != null) {
			path = (!) p;

			if (Toolbox.get_move_tool ().is_selected ()) {
				if (path.stroke > 0) {
					Toolbox.set_object_stroke (path.stroke);
				}
			}

			if (!active_paths.contains (path)) {
				active_paths.add (path);
			}
			PenTool.active_path = path;
		}

		if (group != null) {
			g = (!) group;
			if (!selected_groups.contains (g)) {
				selected_groups.add (g);
			}
		}
	}

	public void delete_background () {
		store_undo_state ();
		background_image = null;
		GlyphCanvas.redraw ();
	}

	public Path? get_active_path () {
		return_val_if_fail (active_paths.size > 0, null);
		return active_paths.get (active_paths.size - 1);
	}

	public bool boundaries (out double x1, out double y1, out double x2, out double y2) {
		var paths = get_all_paths ();

		if (paths.size == 0) {
			x1 = 0;
			y1 = 0;
			x2 = 0;
			y2 = 0;
			return false;
		}

		x1 = CANVAS_MAX;
		x2 = CANVAS_MIN;
		y1 = CANVAS_MAX;
		y2 = CANVAS_MIN;

		foreach (Path p in paths) {
			p.update_region_boundaries ();

			if (p.points.size > 1) {
				if (p.xmin < x1) {
					x1 = p.xmin;
				}

				if (p.xmax > x2) {
					x2 = p.xmax;
				}

				if (p.ymin < y1) {
					y1 = p.ymin;
				}

				if (p.ymax > y2) {
					y2 = p.ymax;
				}
			}
		}

		return x1 != double.MAX;
	}

	public void selection_boundaries (out double x, out double y, out double w, out double h) {
		double px, py, px2, py2;

		px = 10000;
		py = 10000;
		px2 = -10000;
		py2 = -10000;

		foreach (Path p in active_paths) {
			if (p.xmin < px) {
				px = p.xmin;
			}

			if (p.ymin < py) {
				py = p.ymin;
			}

			if (p.xmax > px2) {
				px2 = p.xmax;
			}

			if (p.ymax > py2) {
				py2 = p.ymax;
			}
		}

		if (px2 == -10000 || px == 10000) {
			warning (@"No box for selected paths. ($(active_paths.size))");
			px = 0;
			py = 0;
			px2 = 0;
			py2 = 0;
		}

		x = px;
		y = py2;
		w = px2 - px;
		h = py2 - py;
	}

	/** @return centrum pixel for x coordinates. */
	public static double xc () {
		double c = MainWindow.get_current_glyph ().allocation.width / 2.0;
		return c;
	}

	/** @return centrum pixel for y coordinates. */
	public static double yc () {
		double c = MainWindow.get_current_glyph ().allocation.height / 2.0;
		return c;
	}

	/** @return 1/view_zoom */
	public static double ivz () {
		return 1 / MainWindow.get_current_glyph ().view_zoom;
	}

	public void resized (WidgetAllocation alloc) {
		double a, b, c, d;

		a = Glyph.path_coordinate_x (0);
		b = Glyph.path_coordinate_y (0);

		this.allocation = alloc;

		c = Glyph.path_coordinate_x (0);
		d = Glyph.path_coordinate_y (0);

		view_offset_x -= c - a;
		view_offset_y -= b - d;
	}

	public void set_background_image (BackgroundImage? b) {
		BackgroundImage bg;

		if (b == null) {
			background_image = null;
		} else {
			bg = (!) b;
			background_image = bg;
		}

		BirdFont.get_current_font ().touch ();
	}

	public BackgroundImage? get_background_image () {
		return background_image;
	}

	public override void scroll_wheel (double x, double y,
		double pixeldelta_x, double pixeldelta_y) {

		if (pixeldelta_y > 0) {
			zoom_in_at_point (x, y, pixeldelta_y);
		} else {
			zoom_out_at_point (x, y, pixeldelta_y);
		}

		GlyphCanvas.redraw ();
	}

	public virtual void add_path (Path p) {
		if (layers.subgroups.size == 0) {
			layers.add_layer (new Layer ());
		}

		get_current_layer ().add_path (p);
	}

	public override void selected_canvas () {
		TimeoutSource input_delay;

		ttf_data = null; // recreate quatradic path on export
		overview_thumbnail = null; 

		ignore_input = true; // make sure that tripple clicks in overview are ignored

		input_delay = new TimeoutSource (250);
		input_delay.set_callback(() => {
			ignore_input = false;
			return false;
		});
		input_delay.attach (null);

		add_help_lines ();
		KeyBindings.set_require_modifier (false);
		glyph_sequence = Preferences.get ("glyph_sequence");

		GridTool.update_lines ();

		if (!is_null (MainWindow.native_window)) {
			MainWindow.scrollbar.set_size (0);
		}

		update_zoom_bar ();
		
		Font font = BirdFont.get_current_font ();
		string index = font.settings.get_setting (@"Active Layer $(get_name ())");
		
		if (index != "") {
			int i = int.parse (index);
			
			if (0 <= i < layers.subgroups.size) {
				current_layer = i;
			}
		}
		
		DrawingTools.update_layers ();
		MainWindow.get_toolbox ().update_expanders ();

		Tool current_tool = MainWindow.get_toolbox ().get_current_tool ();
		current_tool.select_action (current_tool);
	}

	void update_zoom_bar () {
		if (!is_null (Toolbox.drawing_tools)
			&& !is_null (Toolbox.drawing_tools.zoom_bar)) {
			Toolbox.drawing_tools.zoom_bar.set_zoom ((view_zoom - 1) / 20);
		}
	}

	public void remove_lines () {
		vertical_help_lines.clear ();
		horizontal_help_lines.clear ();
	}

	public void add_help_lines () {
		remove_lines ();

		return_if_fail (!is_null (BirdFont.get_current_font ()));

		double bgt = BirdFont.get_current_font ().top_limit;
		Line top_margin_line = new Line ("top margin", t_("top margin"), bgt, false);
		top_margin_line.set_color_theme ("Guide 2");
		top_margin_line.position_updated.connect ((pos) => {
			BirdFont.get_current_font ().top_limit = pos;
		});

		double thp = BirdFont.get_current_font ().top_position;
		Line top_line = new Line ("top", t_("top"), thp, false);
		top_line.position_updated.connect ((pos) => {
				Font f = BirdFont.get_current_font ();
				f.top_position = pos;
			});

		double xhp = BirdFont.get_current_font ().xheight_position;
		Line xheight_line = new Line ("x-height", t_("x-height"), xhp, false);
		xheight_line.set_color_theme ("Guide 3");
		xheight_line.dashed = true;
		xheight_line.position_updated.connect ((pos) => {
				Font f = BirdFont.get_current_font ();
				f.xheight_position = pos;
			});

		double xbl = BirdFont.get_current_font ().base_line;
		Line base_line = new Line ("baseline", t_("baseline"), xbl, false);
		base_line.position_updated.connect ((pos) => {
				Font f = BirdFont.get_current_font ();
				f.base_line = pos;
				Overview o = MainWindow.get_overview ();
				o.reset_cache ();
			});

		double bp = BirdFont.get_current_font ().bottom_position;
		Line bottom_line = new Line ("bottom", t_("bottom"), bp, false);
		bottom_line.position_updated.connect ((pos) => {
				BirdFont.get_current_font ().bottom_position = pos;
			});

		double bgb = BirdFont.get_current_font ().bottom_limit;
		Line bottom_margin_line = new Line ("bottom margin", t_("bottom margin"), bgb, false);
		bottom_margin_line.set_color_theme ("Guide 2");
		bottom_margin_line.position_updated.connect ((pos) => {
			BirdFont.get_current_font ().bottom_limit = pos;
		});

		left_line = new Line ("left", t_("left"), left_limit, true);
		left_line.lsb = true;
		left_line.position_updated.connect ((pos) => {
			left_limit = pos;
			update_other_spacing_classes ();
			left_line.set_metrics (get_left_side_bearing ());
		});
		left_line.set_metrics (get_left_side_bearing ());

		right_line = new Line ("right", t_("right"), right_limit, true);
		right_line.rsb = true;
		right_line.position_updated.connect ((pos) => {
			right_limit = pos;
			update_other_spacing_classes ();
			right_line.set_metrics (get_right_side_bearing ());
		});
		right_line.set_metrics (get_right_side_bearing ());

		// lists of lines are sorted and lines are added only if
		// they are relevant for a particular glyph.

		// left to right
		add_line (left_line);
		add_line (right_line);

		bool glyph_has_top = has_top_line ();

		// top to bottom
		add_line (top_margin_line);
		top_margin_line.set_visible (margin_boundaries_visible);

		add_line (top_line);
		top_line.set_visible (glyph_has_top || xheight_lines_visible);

		add_line (xheight_line);
		xheight_line.set_visible (!glyph_has_top || xheight_lines_visible);

		add_line (base_line);

		add_line (bottom_line);
		bottom_line.set_visible (CharDatabase.has_descender (unichar_code) || xheight_lines_visible);

		add_line (bottom_margin_line);
		bottom_margin_line.set_visible (margin_boundaries_visible);

		foreach (Line guide in BirdFont.get_current_font ().custom_guides) {
			add_line (guide);
		}
	}

	public double get_left_side_bearing () {
		double x1, y1, x2, y2;

		if (get_boundaries (out x1, out y1, out x2, out y2)) {
			return x1 - left_limit;
		} else {
			return right_limit - left_limit;
		}
	}

	public double get_right_side_bearing () {
		double x1, y1, x2, y2;

		if (get_boundaries (out x1, out y1, out x2, out y2)) {
			return right_limit - x2;
		} else {
			return right_limit - left_limit;
		}
	}

	public bool get_boundaries (out double x1, out double y1,
		out double x2, out double y2) {

		double max_x, min_x, max_y, min_y;
		PathList pl;
		var paths = get_all_paths ();

		if (paths.size == 0) {
			x1 = 0;
			y1 = 0;
			x2 = 0;
			y2 = 0;
			return false;
		}

		max_x = CANVAS_MIN;
		min_x = CANVAS_MAX;
		max_y = CANVAS_MIN;
		min_y = CANVAS_MAX;

		// FIXME: optimize
		foreach (Path p in paths) {

			if (p.stroke > 0) {
				pl = p.get_stroke_fast ();

				foreach (Path part in pl.paths) {
					boundaries_for_path (part, ref min_x, ref max_x, ref min_y, ref max_y);
				}
			} else {
				boundaries_for_path (p, ref min_x, ref max_x, ref min_y, ref max_y);
			}
		}

		x1 = min_x;
		y1 = max_y;
		x2 = max_x;
		y2 = min_y;

		return max_x != CANVAS_MIN;
	}

	void boundaries_for_path (Path p, ref double min_x, ref double max_x,
		ref double min_y, ref double max_y) {

		double max_x2, max_y2, min_x2, min_y2;

		max_x2 = max_x;
		min_x2 = min_x;
		max_y2 = max_y;
		min_y2 = min_y;

		p.all_of_path ((x, y, t) => {
			if (x > max_x2) {
				max_x2 = x;
			}

			if (y > max_y2) {
				max_y2 = y;
			}

			if (x < min_x2) {
				min_x2 = x;
			}

			if (y < min_y2) {
				min_y2 = y;
			}

			return true;
		});

		max_x = max_x2;
		min_x = min_x2;
		max_y = max_y2;
		min_y = min_y2;
	}

	bool has_top_line () {
		return !unichar_code.islower () || CharDatabase.has_ascender (unichar_code);
	}

	public bool get_show_help_lines () {
		return show_help_lines;
	}

	/** Show both x-height and top lines. */
	public bool get_xheight_lines_visible () {
		return xheight_lines_visible;
	}

	/** Show both x-height and top lines. */
	public void set_xheight_lines_visible (bool x) {
		xheight_lines_visible = x;
		add_help_lines ();
	}

	public void set_margin_lines_visible (bool m) {
		margin_boundaries_visible = m;
		add_help_lines ();
	}

	public bool get_margin_lines_visible () {
		return margin_boundaries_visible;
	}

	public void remove_empty_paths () {
		foreach (Path p in get_all_paths ()) {
			if (p.points.size < 2) {
				delete_path (p);
				remove_empty_paths ();
				return;
			}
		}
	}

	public void delete_path (Path p) {
		layers.remove_path(p);
	}

	public string get_svg_data () {
		return Svg.to_svg_glyph (this);
	}

	public int get_height () {
		Font f = BirdFont.get_current_font ();
		return (int) Math.fabs (f.top_limit - f.bottom_limit);
	}

	public double get_width () {
		return Math.fabs (right_limit - left_limit);
	}

	public unichar get_unichar () {
		return unichar_code;
	}

	public string get_unichar_string () {
		string? s = (!)get_unichar ().to_string ();

		if (unlikely (s == null)) {
			warning ("Invalid character.");
			return "".dup ();
		}

		return (!) s;
	}

	public void redraw_help_lines () {
		redraw_area (0, 0, allocation.width, allocation.height);
	}

	public void set_show_help_lines (bool hl) {
		show_help_lines = hl;
	}

	private void add_line (Line line) {
		if (line.is_vertical ()) {
			vertical_help_lines.add (line);
		} else {
			horizontal_help_lines.add (line);
		}

		sort_help_lines ();

		line.queue_draw_area.connect ((x, y, w, h) => {
			this.redraw_area (x, y, w, h);
		});
	}

	public void sort_help_lines () {
		vertical_help_lines.sort ((a, b) => {
			Line first, next;
			first = (Line) a;
			next = (Line) b;
			return (int) (first.get_pos () - next.get_pos ());
		});

		horizontal_help_lines.sort ((a, b) => {
			Line first, next;
			first = (Line) a;
			next = (Line) b;
			return (int) (first.get_pos () - next.get_pos ());
		});
	}

	public override string get_name () {
		return name;
	}

	public override string get_label () {
		return name;
	}

	private void help_line_event (int x, int y) {
		bool m = false;

		foreach (Line line in vertical_help_lines) {
			if (!m && line.event_move_to (x, y, allocation)) {
				m = true;
			}
		}

		foreach (Line line in horizontal_help_lines) {
			if (!m && line.event_move_to (x, y, allocation)) {
				m = true;
			}
		}
	}

	public override void key_release (uint keyval) {
		Tool t;
		t = MainWindow.get_toolbox ().get_current_tool ();
		t.key_release_action (t, keyval);

		if (keyval == (uint)' ') {
			move_canvas = false;
		}
	}

	public override void key_press (uint keyval) {
		Tool t = MainWindow.get_toolbox ().get_current_tool ();
		t.key_press_action (t, keyval);

		if (keyval == (uint)' ') {
			move_canvas = true;
		}

		switch (keyval) {
			case Key.NUM_PLUS:
				zoom_in ();
				break;
			case Key.NUM_MINUS:
				zoom_out ();
				break;
		}
	}

	/** Delete edit point from path.
	 * @return false if no points was deleted
	 */
	public bool process_deleted () {
		Gee.ArrayList<Path> deleted_paths = new Gee.ArrayList<Path> ();
		foreach (Path p in get_all_paths ()) {
			if (p.points.size > 0) {
				if (process_deleted_points_in_path (p)) {
					return true;
				}
			} else {
				deleted_paths.add (p);
			}
		}

		foreach (Path p in deleted_paths) {
			delete_path (p);
		}

		return false;
	}

	private bool process_deleted_points_in_path (Path p) {
		PathList remaining_points;
		remaining_points = p.process_deleted_points ();
		foreach (Path path in remaining_points.paths) {
			add_path (path);
			path.reopen ();
			path.create_list ();

			add_active_path (null, path);
		}

		if (remaining_points.paths.size > 0) {
			delete_path (p);
			return true;
		}

		return false;
	}

	public override void motion_notify (double x, double y) {
		Tool t = MainWindow.get_toolbox ().get_current_tool ();

		if (view_is_moving) {
			move_view_offset  (x, y);
			return;
		}

		help_line_event ((int) x, (int) y);
		t.move_action (t, (int) x, (int) y);

		motion_x = x * ivz () - xc () + view_offset_x;
		motion_y = yc () - y * ivz () - view_offset_y;
	}

	public override void button_release (int button, double ex, double ey) {
		bool line_moving = false;
		view_is_moving = false;

		foreach (Line line in get_all_help_lines ()) {
			if (!line.set_move (false)) {
				line_moving = true;
			}
		}

		if (!line_moving) {
			Tool t = MainWindow.get_toolbox ().get_current_tool ();
			t.release_action (t, (int) button, (int) ex, (int) ey);
		}

		update_view ();
	}

	private Gee.ArrayList<Line> get_all_help_lines () {
		Gee.ArrayList<Line> all_lines = new Gee.ArrayList<Line> ();

		foreach (Line l in vertical_help_lines) {
			all_lines.add (l);
		}

		foreach (Line l in horizontal_help_lines) {
			all_lines.add (l);
		}

		if (GridTool.is_visible ()) {
			foreach (Line l in GridTool.get_vertical_lines ()) {
				all_lines.add (l);
			}

			foreach (Line l in GridTool.get_horizontal_lines ()) {
				all_lines.add (l);
			}
		}

		return all_lines;
	}

	public void update_view () {
		GlyphCanvas.redraw ();
	}

	public override void double_click (uint button, double ex, double ey) {
		Tool t = MainWindow.get_toolbox ().get_current_tool ();
		t.double_click_action (t, (int) button, (int) ex, (int) ey);
	}

	public override void button_press (uint button, double ex, double ey) {
		bool moving_lines = false;

		pointer_begin_x = ex;
		pointer_begin_y = ey;

		foreach (Line line in horizontal_help_lines) {
			if (!moving_lines && line.is_visible () && line.button_press (button)) {
				moving_lines = true;
			}
		}

		foreach (Line line in vertical_help_lines) {
			if (!moving_lines && line.is_visible () && line.button_press (button)) {
				moving_lines = true;
			}
		}

		if (moving_lines) {
			return;
		}

		if (move_canvas 
			|| DrawingTools.move_canvas.is_selected ()
			|| (KeyBindings.has_ctrl () && KeyBindings.has_shift ())) {
			view_is_moving = true;
			move_offset_x = view_offset_x;
			move_offset_y = view_offset_y;
		} else {
			Tool t = MainWindow.get_toolbox ().get_current_tool ();
			t.press_action (t, (int) button, (int) ex, (int) ey);
		}
	}

	/** Add new points to this path. */
	public void set_active_path (Path p) {
		p.reopen ();
		clear_active_paths ();
		add_active_path (null, p);
	}

	/** Move view port centrum to this coordinate. */
	public void set_center (double x, double y) {
		x -= allocation.width / 2.0;
		y -= allocation.height / 2.0;

		view_offset_x += x / view_zoom;
		view_offset_y += y / view_zoom;
	}

	public void set_zoom_from_area () {
		double x = Math.fmin (zoom_x1, zoom_x2);
		double y = Math.fmin (zoom_y1, zoom_y2);

		double w = Math.fabs (zoom_x1 - zoom_x2);
		double h = Math.fabs (zoom_y1 - zoom_y2);

		double view_zoom_x, view_zoom_y;
		double ivz, off;

		if (move_canvas) {
			return;
		}

		if (Path.distance (x, x + w, y, y + h) < 7) {
			zoom_in ();
		} else {
			view_offset_x += x / view_zoom;
			view_offset_y += y / view_zoom;

			if (unlikely (allocation.width == 0 || allocation.height == 0)) {
				return;
			}

			view_zoom_x = allocation.width * view_zoom / w;
			view_zoom_y = allocation.height * view_zoom / h;

			// TODO: there is a max zoom level

			if (view_zoom_x * allocation.width < view_zoom_y * allocation.height) {
				view_zoom = view_zoom_x;
				ivz = 1 / view_zoom;

				off = (view_zoom / view_zoom_y) * allocation.height / view_zoom;
				off = allocation.height/view_zoom - off;
				off /= 2;

				view_offset_y -= off;

			} else {
				view_zoom = view_zoom_y;
				ivz = 1 / view_zoom;

				off = (view_zoom / view_zoom_x) * allocation.width / view_zoom;
				off = allocation.width / view_zoom - off;
				off /= 2;

				view_offset_x -= off;
			}

			zoom_area_is_visible = false;
			store_current_view ();
		}

		update_zoom_bar ();
	}

	public void show_zoom_area (int sx, int sy, int nx, int ny) {
		set_zoom_area (sx, sy, nx, ny);
		zoom_area_is_visible = true;
		GlyphCanvas.redraw ();
	}

	public void set_zoom_area (int sx, int sy, int nx, int ny) {
		zoom_x1 = sx;
		zoom_y1 = sy;
		zoom_x2 = nx;
		zoom_y2 = ny;
	}

	public static void validate_zoom () {
		Glyph g = MainWindow.get_current_glyph ();
		if (unlikely (g.view_zoom == 0)) {
			warning ("Zoom is zero.");
			g.reset_zoom ();

			if (g.view_zoom == 0) {
				g.view_zoom = 1;
				g.view_offset_x = 0;
				g.view_offset_y = 0;
			}
		}
	}

	public static double path_coordinate_x (double x) {
		Glyph g = MainWindow.get_current_glyph ();
		validate_zoom ();
		return x * ivz () - xc () + g.view_offset_x;
	}

	public static int reverse_path_coordinate_x (double x) {
		Glyph g = MainWindow.get_current_glyph ();
		validate_zoom ();
		return (int) Math.rint ((x - g.view_offset_x + xc ()) * g.view_zoom);
	}

	public static double precise_reverse_path_coordinate_x (double x) {
		Glyph g = MainWindow.get_current_glyph ();
		validate_zoom ();
		return (x - g.view_offset_x + xc ()) * g.view_zoom;
	}

	public static double path_coordinate_y (double y) {
		Glyph g = MainWindow.get_current_glyph ();
		validate_zoom ();
		return yc () - y * ivz () - g.view_offset_y;
	}

	public static int reverse_path_coordinate_y (double y) {
		Glyph g = MainWindow.get_current_glyph ();
		validate_zoom ();
		y =  Math.rint ((y + g.view_offset_y - yc ()) * g.view_zoom);
		return (int) (-y);
	}

	public static double precise_reverse_path_coordinate_y (double y) {
		Glyph g = MainWindow.get_current_glyph ();
		validate_zoom ();
		y = (y + g.view_offset_y - yc ()) * g.view_zoom;
		return -y;
	}

	public Layer? get_path_at (double x, double y) {
		Layer? group = null;
		bool found = false;

		var subgroups = get_current_layer ().subgroups;
		
		if (is_null (subgroups)) {
			warning ("Expecting a sub group.");
			return null;
		}

		foreach (Layer layer in subgroups) {
			if (is_null (layer.paths) || is_null (layer.paths.paths)) {
				warning ("No paths in get_path_at");
				continue;
			}
			
			foreach (Path pt in layer.paths.paths) {
				if (pt.is_over (x, y)) {
					found = true;
					group = layer;
				}
			}
		}

		if (!found) {
			var current_layer_paths = get_paths_in_current_layer ();
		
			if (is_null (current_layer_paths)) {
				warning ("Expecting current_layer_paths.");
				return null;		
			}
		
			foreach (Path pt in current_layer_paths) {
				if (pt.is_over (x, y)) {
					Layer layer = new Layer ();
					layer.is_counter = true;
					layer.single_path = true;
					layer.add_path (pt);
					group = layer;
				}
			}
		}

		return group;
	}

	public bool select_path (double x, double y) {
		Path? p = null;
		bool found = false;

		foreach (Path pt in get_paths_in_current_layer ()) {
			if (pt.is_over (x, y)) {
				p = pt;
				found = true;
			}
		}

		if (!KeyBindings.has_shift ()) {
			clear_active_paths ();
		}

		add_active_path (null, p);

		return found;
	}

	public bool is_over_selected_path (double x, double y) {
		foreach (Path pt in active_paths) {
			if (pt.is_over (x, y)) {
				return true;
			}
		}
		return false;
	}

	public void queue_redraw_path (Path path) {
		redraw_path (path.xmin, path.ymin, path.xmax, path.ymax);
	}

	private void redraw_path (double xmin, double ymin, double xmax, double ymax) {
		int yc = (int)(allocation.height / 2.0);

		double yta = yc - ymin - view_offset_y;
		double ytb = yc - ymax - view_offset_y;

		double xta = -view_offset_x - xmin;
		double xtb = -view_offset_x - xmax;

		redraw_area ((int)xtb - 10, (int)yta - 10, (int)(xtb - xta) + 10, (int) (yta - ytb) + 10);
	}

	public Path get_closeset_path (double x, double y) {
		double d;
		EditPoint ep = new EditPoint ();

		Path min_point = new Path ();
		double min_distance = double.MAX;

		double xt = path_coordinate_x (x);
		double yt = path_coordinate_y (y);
		var paths = get_visible_paths ();

		foreach (Path p in paths) {
			if (p.is_over (xt, yt)) {
				return p;
			}
		}

		foreach (Path p in paths) {
			if (p.points.size == 0) continue;

			p.get_closest_point_on_path (ep, xt, yt);
			d = Math.pow (ep.x - xt, 2) + Math.pow (ep.y - yt, 2);

			if (d < min_distance) {
				min_distance = d;
				min_point = p;
			}

		}

		// a path without any editpoints
		if (paths.size > 0) {
			return paths.get (0);
		}

		if (unlikely (min_distance == double.MAX)) {
			warning (@"No path found in path_list.");
		}

		return min_point;
	}

	public void move_selected_edit_point_coordinates (EditPoint selected_point, double xt, double yt) {
		double x, y;

		BirdFont.get_current_font ().touch ();

		x = reverse_path_coordinate_x (xt);
		y = reverse_path_coordinate_y (yt);

		// redraw control point
		redraw_area ((int)(x - 4*view_zoom), (int)(y - 4*view_zoom), (int)(x + 3*view_zoom), (int)(y + 3*view_zoom));

		// update position of selected point
		selected_point.set_position (xt, yt);

		if (view_zoom >= 2) {
			redraw_area (0, 0, allocation.width, allocation.height);
		} else {
			redraw_last_stroke (x, y);
		}
	}

	public void move_selected_edit_point (EditPoint selected_point, double x, double y) {
		double xt = path_coordinate_x (x);
		double yt = path_coordinate_y (y);
		move_selected_edit_point_coordinates (selected_point, xt, yt);
	}

	public void redraw_segment (EditPoint a, EditPoint b) {
		double margin = 10;
		double x = Math.fmin (reverse_path_coordinate_x (a.x), reverse_path_coordinate_x (b.x)) - margin;
		double y = Math.fmin (reverse_path_coordinate_y (a.y), reverse_path_coordinate_y(b.y)) - margin;
		double w = Math.fabs (reverse_path_coordinate_x (a.x) - reverse_path_coordinate_x(b.x)) + 2 * margin;
		double h = Math.fabs (reverse_path_coordinate_y (a.y) - reverse_path_coordinate_y (b.y)) + 2 * margin;

		redraw_area ((int)x, (int)y, (int)w, (int)h);
	}

	private void redraw_last_stroke (double x, double y) {
		// redraw line, if we have more than one new point on path
		double px = 0;
		double py = 0;
		int tw = 0;
		int th = 0;

		double xc = (allocation.width / 2.0);

		if (active_paths.size == 0) {
			return;
		}

		foreach (Path path in active_paths) {
			EditPoint p;
			EditPoint pl = path.get_last_point ();

			if (pl.prev != null) {
				p = pl.get_prev ();

				px = p.x + xc;
				py = p.y - xc;

				tw = (px > x) ? (int) (px - x) : (int) (x - px);
				th = (py > y) ? (int) (py - y) : (int) (y - py);

				if (px > x) px -= tw + 60;
				if (py > y) py -= th + 60;

			} else {
				px = x - 60;
				py = y - 60;
				tw = 0;
				th = 0;
			}
		}

		redraw_area ((int)px - 20, (int)py - 20, tw + 120, th + 120);
	}

	public bool has_active_path () {
		return active_paths.size > 0;
	}

	public bool is_open () {
		return open;
	}

	/** Close all editable paths and return false if no path have been closed. */
	public bool close_path () {
		bool r = false;

		foreach (Path p in get_all_paths ()) {
			if (p.is_editable ()) {
				r = true;
				p.set_editable (false);
			}
		}

		open = false;
		clear_active_paths ();
		GlyphCanvas.redraw ();

		MainWindow.set_cursor (NativeWindow.VISIBLE);

		return r;
	}

	public void open_path () {
		foreach (Path p in get_visible_paths ()) {
			p.set_editable (true);
			p.recalculate_linear_handles ();

			if (p.is_open () && p.points.size > 0) {
				p.get_first_point ().set_reflective_handles (false);
				p.get_first_point ().set_tie_handle (false);
				p.get_last_point ().set_reflective_handles (false);
				p.get_last_point ().set_tie_handle (false);
			}
		}

		open = true;
		redraw_area (0, 0, allocation.width, allocation.height);
	}

	public void redraw_path_region (Path p) {
		int x, y, w, h;

		p.update_region_boundaries ();

		x = reverse_path_coordinate_x (p.xmin);
		y = reverse_path_coordinate_x (p.xmin);
		w = reverse_path_coordinate_x (p.xmax) - x;
		h = reverse_path_coordinate_x (p.ymax) - y; // FIXME: redraw path

		redraw_area (x, y, w, h);
	}

	public Line get_line (string name) {
		foreach (Line line in vertical_help_lines) {
			if (likely (line.get_label () == name)) {
				return line;
			}
		}

		foreach (Line line in horizontal_help_lines) {
			if (likely (line.get_label () == name)) {
				return line;
			}
		}

		warning (@"No line with label $name found");
		return new Line ("Err");
	}

	public override void zoom_in () {
		if (move_canvas) {
			return;
		}

		set_zoom_area (10, 10, allocation.width - 10, allocation.height - 10);
		set_zoom_from_area ();
		update_view ();
		update_zoom_bar ();
	}

	public override void zoom_out () {
		double w = allocation.width;
		int n = (int) (10 * ((w - 10) / allocation.width));
		set_zoom_area (-n, -n, allocation.width + n, allocation.height + n);
		set_zoom_from_area ();
		update_view ();
		update_zoom_bar ();
	}

	public override void zoom_max () {
		default_zoom ();
		update_zoom_bar ();
	}

	public override void zoom_min () {
		double ax =  1000;
		double ay =  1000;
		double bx = -1000;
		double by = -1000;

		int iax, iay, ibx, iby;

		reset_zoom ();

		foreach (var p in get_visible_paths ()) {
			p.update_region_boundaries ();

			if (p.points.size > 2) {
				if (p.xmin < ax) ax = p.xmin;
				if (p.ymin < ay) ay = p.ymin;
				if (p.xmax > bx) bx = p.xmax;
				if (p.ymax > by) by = p.ymax;
			}
		}

		if (ax == 1000) return; // empty page

		iax = (int) ((ax + view_offset_x + allocation.width / 2.0) * view_zoom);
		iay = (int) ((-ay + view_offset_y + allocation.height / 2.0) * view_zoom);
		ibx = (int) ((bx + view_offset_x + allocation.width / 2.0) * view_zoom);
		iby = (int) ((-by + view_offset_y + allocation.height / 2.0) * view_zoom);

		show_zoom_area (iax, iay, ibx, iby); // set this later on button release
		set_zoom_from_area ();
		zoom_out (); // add some margin

		redraw_area (0, 0, allocation.width, allocation.height);
		update_zoom_bar ();
	}

	public override void store_current_view () {
		ZoomView n;

		if (zoom_list_index + 1 < zoom_list.size) {
			n = zoom_list.get (zoom_list_index);
			while (n != zoom_list.get (zoom_list.size - 1)) {
				zoom_list.remove_at (zoom_list.size - 1);
			}
		}

		zoom_list.add (new ZoomView (view_offset_x, view_offset_y, view_zoom, allocation));
		zoom_list_index = (int) zoom_list.size - 1;

		if (zoom_list.size > 50) {
			zoom_list.remove_at (0);
		}
	}

	public override void restore_last_view () {
		if (zoom_list.size == 0 || zoom_list_index - 1 < 0) {
			return;
		}

		zoom_list_index--;

		ZoomView z = zoom_list.get (zoom_list_index);

		view_offset_x = z.x;
		view_offset_y = z.y;
		view_zoom = z.zoom;
		allocation = z.allocation;

		update_zoom_bar ();
	}

	public override void next_view () {
		ZoomView z;

		if (zoom_list.size == 0 || zoom_list_index + 1 >= zoom_list.size) {
			return;
		}

		zoom_list_index++;

		z = zoom_list.get (zoom_list_index);

		view_offset_x = z.x;
		view_offset_y = z.y;
		view_zoom = z.zoom;
		allocation = z.allocation;

		update_zoom_bar ();
	}

	public override void reset_zoom () {
		view_offset_x = 0;
		view_offset_y = 0;

		set_zoom (1);

		store_current_view ();
		update_zoom_bar ();
	}

	/** Get x-height or top line. */
	public Line get_upper_line () {
		if (has_top_line () || xheight_lines_visible) {
			return get_line ("top");
		}

		return get_line ("x-height");
	}

	/** Get base line. */
	public Line get_lower_line () {
		return get_line ("baseline");
	}

	/** Set default zoom. See default_zoom. */
	public void set_default_zoom () {
		int bottom = 0;
		int top = 0;
		int left = 0;
		int right = 0;

		return_if_fail (vertical_help_lines.size != 0);
		return_if_fail (horizontal_help_lines.size != 0);

		reset_zoom ();

		bottom = get_lower_line ().get_position_pixel ();
		top = get_upper_line ().get_position_pixel ();

		left = vertical_help_lines.get (vertical_help_lines.size - 1).get_position_pixel ();
		right = vertical_help_lines.get (0).get_position_pixel ();

		set_zoom_area (left + 10, top - 10, right - 10, bottom + 10);
		set_zoom_from_area ();
	}

	/** Set default zoom and redraw canvas. */
	public void default_zoom () {
		set_default_zoom ();
		update_view ();
	}

	public bool is_empty () {
		foreach (Path p in get_visible_paths ()) {
			if (p.points.size > 0) {
				return false;
			}
		}

		return true;
	}

	public void set_zoom (double z)
		requires (z > 0)
	{
		view_zoom = z;
	}

	public void set_background_visible (bool visibility) {
		background_image_visible = visibility;
	}

	public bool get_background_visible () {
		return background_image_visible;
	}

	public void draw_coordinate (Context cr) {
		Theme.color (cr, "Table Border");
		cr.set_font_size (12);
		cr.move_to (0, 10);
		cr.show_text (@"($motion_x, $motion_y)");
		cr.stroke ();
	}

	/** Draw filled paths. */
	public void draw_paths (Context cr, Color? c = null) {
		PathList stroke;
		Color color;
		bool open;
		
		cr.save ();
		cr.new_path ();
		foreach (Path p in get_visible_paths ()) {
			if (c != null) {
				color = (!) c;
			} else if (p.color != null) {
				color = (!) p.color;
			} else {
				color = Color.black ();
			}

			if (p.stroke > 0) {
				stroke = p.get_stroke_fast ();
				draw_path_list (stroke, cr, color);
			} else {
				open = p.is_open ();
				
				if (open) {
					p.close ();
					p.recalculate_linear_handles ();
				}
				
				p.draw_path (cr, this, color);
				
				if (open) {
					p.reopen ();
				}
			}
		}
		cr.fill ();
		cr.restore ();
	}

	public void draw_path (Context cr) {
		PathList stroke;
		Color color;

		cr.save ();
		cr.new_path ();
		foreach (Path p in get_visible_paths ()) {
			if (p.stroke > 0) {
				stroke = p.get_stroke_fast ();

				if (p.is_editable ()) {
					color = Theme.get_color ("Filled Stroke");
					color.a = 0.8;
				} else {
					color = Color.black ();
				}

				draw_path_list (stroke, cr, color);
			}
		}
		cr.fill ();
		cr.restore ();

		if (!(MainWindow.get_toolbox ().get_current_tool () is PenTool)
			&& !(MainWindow.get_toolbox ().get_current_tool () is PointTool)
			&& !(MainWindow.get_toolbox ().get_current_tool () is TrackTool)
			&& !(MainWindow.get_toolbox ().get_current_tool () is BezierTool)) {
			cr.save ();
			cr.new_path ();
			foreach (Path p in active_paths) {
				if (p.stroke > 0) {
					stroke = p.get_stroke_fast ();
					color = Theme.get_color ("Selected Objects");
					draw_path_list (stroke, cr, color);
				}
			}
			cr.fill ();
			cr.restore ();
		}

		if (is_open () && Path.fill_open_path) {
			cr.save ();
			cr.new_path ();
			foreach (Path p in get_visible_paths ()) {
				if (p.stroke == 0) {
					color = p.color == null ? get_path_fill_color () : (!) p.color;
					p.draw_path (cr, this, color);
				}
			}
			cr.fill ();
			cr.restore ();
		}

		if (is_open ()) {
			cr.save ();
			cr.new_path ();
			foreach (Path p in get_visible_paths ()) {
				p.draw_outline (cr);
				p.draw_edit_points (cr);
			}
			cr.restore ();
		}

		if (!is_open ()) {
			// This was good for testing but it is way too slow:
			// Svg.draw_svg_path (cr, get_svg_data (), Glyph.xc () + left, Glyph.yc () - baseline);

			cr.save ();
			cr.new_path ();
			foreach (Path p in get_visible_paths ()) {
				if (p.stroke == 0) {
					color = p.color == null ? Color.black () : (!) p.color;
					p.draw_path (cr, this, color);
				}
			}
			cr.close_path ();
			cr.fill ();
			cr.restore ();

			foreach (Path p in active_paths) {
				cr.save ();
				cr.new_path ();
				if (p.stroke == 0) {
					p.draw_path (cr, this);
				}
				cr.close_path ();
				cr.fill ();
				cr.restore ();
			}
		}

		if (show_orientation_arrow) {
			foreach (Path p in get_visible_paths ()) {
				if (p.stroke > 0) {
					stroke = p.get_stroke_fast ();
					foreach (Path ps in stroke.paths) {
						ps.draw_orientation_arrow (cr, orientation_arrow_opacity);
					}
				} else {
					p.draw_orientation_arrow (cr, orientation_arrow_opacity);
				}
			}
		}
	}

	private Color get_path_fill_color () {
		return Theme.get_color ("Fill Color");
	}

	public void draw_path_list (PathList pl, Context cr, Color? c = null) {
		foreach (Path p in pl.paths) {
			p.draw_path (cr, this, c);
		}
	}

	public void draw_background_color (Context cr, double opacity) {
		if (opacity > 0) {
			cr.save ();
			cr.rectangle (0, 0, allocation.width, allocation.height);
			Theme.color (cr, "Canvas Background");
			cr.fill ();
			cr.restore ();
		}
	}

	public void draw_help_lines (Context cr) {
		foreach (Line line in get_all_help_lines ()) {
			cr.save ();
			line.draw (cr, allocation);
			cr.restore ();
		}
	}

	public void set_allocation (WidgetAllocation a) {
		allocation = a;
	}

	public override void draw (WidgetAllocation allocation, Context cmp) {
		Tool tool;

		this.allocation = allocation;

		cmp.save ();
		draw_background_color (cmp, 1);
		cmp.restore ();

		if (background_image != null && background_image_visible) {
			((!)background_image).draw (cmp, allocation, view_offset_x, view_offset_y, view_zoom);
		}

		if (unlikely (Preferences.draw_boundaries)) {
			foreach (Path p in get_visible_paths ()) {
				p.draw_boundaries (cmp);
			}
		}

		draw_background_glyph (allocation, cmp);
		juxtapose (allocation, cmp);

		if (BirdFont.show_coordinates) {
			draw_coordinate (cmp);
		}

		if (show_help_lines) {
			cmp.save ();
			draw_help_lines (cmp);
			cmp.restore ();
		}

		if (!is_empty ()) {
			cmp.save ();
			cmp.scale (view_zoom, view_zoom);
			cmp.translate (-view_offset_x, -view_offset_y);
			draw_path (cmp);
			cmp.restore ();
		}

		cmp.save ();
		tool = MainWindow.get_toolbox ().get_current_tool ();
		tool.draw_action (tool, cmp, this);
		cmp.restore ();
	}

	private void zoom_in_at_point (double x, double y, double amount = 15) {
		int n = (int) (-amount);
		zoom_at_point (x, y, n);
	}

	private void zoom_out_at_point (double x, double y, double amount = 15) {
		double a = -amount;
		int n = (int) (a * ((allocation.width - a) / allocation.width));
		zoom_at_point (x, y, n);
	}

	public void zoom_tap (double distance) {
		int w = (int) (distance);
		if (distance != 0) {
			show_zoom_area (-w , -w, allocation.width + w, allocation.height + w);
			set_zoom_from_area ();
		}
	}

	/** Zoom in @param n pixels. */
	private void zoom_at_point (double x, double y, int n) {
		double w = allocation.width;
		double h = allocation.height;

		double rx = Math.fabs (w / 2 - x) / (w / 2);
		double ry = Math.fabs (h / 2 - y) / (h / 2);

		int xd = (x < w / 2) ? (int) (n * rx) : (int) (-n * rx);
		int yd = (y < h / 2) ? (int) (n * ry) : (int) (-n * ry);

		show_zoom_area (-n + xd, -n + yd, allocation.width + n + xd, allocation.height + n + yd);
		set_zoom_from_area ();
	}

	private void move_view_offset (double x, double y) {
		view_offset_x = move_offset_x + (pointer_begin_x - x) * (1/view_zoom);
		view_offset_y = move_offset_y + (pointer_begin_y - y) * (1/view_zoom);
		GlyphCanvas.redraw ();
	}

	public void store_undo_state (bool clear_redo = false) {
		Glyph g = copy ();
		undo_list.add (g);

		if (clear_redo) {
			redo_list.clear ();
		}
	}

	public void store_redo_state () {
		Glyph g = copy ();
		redo_list.add (g);
	}

	public Glyph copy () {
		Glyph g = new Glyph.no_lines (name, unichar_code);

		g.current_layer = current_layer;

		g.left_limit = left_limit;
		g.right_limit = right_limit;

		g.remove_lines ();

		foreach (Line line in get_all_help_lines ()) {
			g.add_line (line.copy ());
		}

		g.layers = layers.copy ();

		foreach (Path p in active_paths) {
			g.active_paths.add (p);
		}

		if (background_image != null) {
			g.background_image = ((!) background_image).copy ();
		}

		g.empty = empty;
		g.open = open;
		g.version_id = version_id;

		return g;
	}

	public void reload () {
		Font f = BirdFont.get_current_font ();

		if (f.has_glyph (name)) {
			set_glyph_data ((!) f.get_glyph (name));
		}
	}

	public override void undo () {
		Glyph g;
		Tool tool;

		if (undo_list.size == 0) {
			return;
		}

		tool = MainWindow.get_toolbox ().get_current_tool ();
		tool.before_undo ();

		g = undo_list.get (undo_list.size - 1);

		store_redo_state ();
		set_glyph_data (g);

		undo_list.remove_at (undo_list.size - 1);

		DrawingTools.update_layers ();
		PenTool.update_selected_points ();

		clear_active_paths ();

		tool.after_undo ();
	}

	public override void redo () {
		Glyph g;

		if (redo_list.size == 0) {
			return;
		}

		g = redo_list.get (redo_list.size - 1);

		store_undo_state (false);
		set_glyph_data (g);

		redo_list.remove_at (redo_list.size - 1);

		DrawingTools.update_layers ();
		PenTool.update_selected_points ();

		clear_active_paths ();
	}

	void set_glyph_data (Glyph g) {
		current_layer = g.current_layer;
		layers = g.layers.copy ();

		left_limit = g.left_limit;
		right_limit = g.right_limit;

		remove_lines ();
		foreach (Line line in g.get_all_help_lines ()) {
			add_line (line.copy ());
		}

		add_help_lines ();

		if (g.background_image != null) {
			background_image = ((!) g.background_image).copy ();
		}

		clear_active_paths ();
		foreach (Path p in g.active_paths) {
			add_active_path (null, p);
		}

		redraw_area (0, 0, allocation.width, allocation.height);
	}

	/** Split curve in two parts and add a new point in between.
	 * @return the new point
	 */
	public void insert_new_point_on_path (double x, double y) {
		double min, distance;
		Path? p = null;
		Path path;
		EditPoint? np = null;
		EditPoint lep;

		double xt;
		double yt;

		xt = x * ivz () - xc () + view_offset_x;
		yt = yc () - y * ivz () - view_offset_y;

		min = double.MAX;

		foreach (Path pp in get_visible_paths ()) {
			lep = new EditPoint ();
			pp.get_closest_point_on_path (lep, xt, yt);
			distance = Math.sqrt (Math.pow (Math.fabs (xt - lep.x), 2) + Math.pow (Math.fabs (yt - lep.y), 2));

			if (distance < min) {
				min = distance;
				p = pp;
				np = lep;
			}
		}

		if (p == null) {
			return;
		}

		path = (!) p;

		lep = new EditPoint ();
		path.get_closest_point_on_path (lep, xt, yt);
		path.insert_new_point_on_path (lep);
	}

	static bool in_range (double offset_x, double coordinate_x1, double coordinate_x2) {
		return coordinate_x1 <= offset_x <= coordinate_x2;
	}

	public void juxtapose (WidgetAllocation allocation, Context cr) {
		string glyph_sequence = Preferences.get ("glyph_sequence");
		unichar c;
		Font font = BirdFont.get_current_font ();
		Glyph glyph = MainWindow.get_current_glyph ();
		Glyph juxtaposed;
		StringBuilder current = new StringBuilder ();
		int pos;
		string name;
		double x, kern;
		double left, baseline;
		string last_name;

		double box_x1, box_x2, box_y1, box_y2;
		double marker_x, marker_y;

		KerningClasses classes = font.get_kerning_classes ();

		x = 0;

		box_x1 = path_coordinate_x (0);
		box_y1 = path_coordinate_y (0);
		box_x2 = path_coordinate_x (allocation.width);
		box_y2 = path_coordinate_y (allocation.height);

		current.append_unichar (glyph.unichar_code);
		pos = glyph_sequence.index_of (current.str);

		baseline = font.base_line;;
		left = glyph.get_line ("left").pos;

		x = glyph.get_width ();
		last_name = glyph.name;
		for (int i = pos + 1; i < glyph_sequence.char_count (); i++) {
			c = glyph_sequence.get_char (i);
			name = (!) c.to_string ();
			juxtaposed = (font.has_glyph (name)) ? (!) font.get_glyph (name) : font.get_space ().get_current ();

			if (font.has_glyph (last_name) && font.has_glyph (name)) {
				kern = classes.get_kerning (last_name, name);
			} else {
				kern = 0;
			}

			if (!juxtaposed.is_empty ()
				&& (in_range (left + x + kern, box_x1, box_x2) // the letter is visible
				|| in_range (left + x + kern + juxtaposed.get_width (), box_x1, box_x2))) {

				marker_x = Glyph.xc () + left + x + kern - glyph.view_offset_x;
				marker_y = Glyph.yc () - baseline - glyph.view_offset_y;

				cr.save ();
				cr.scale (glyph.view_zoom, glyph.view_zoom);
				Theme.color (cr, "Foreground 1");

				Svg.draw_svg_path (cr, juxtaposed.get_svg_data (), marker_x, marker_y);
				cr.restore ();
			}

			x += juxtaposed.get_width () + kern;

			last_name = name;
		}

		x = 0;
		last_name = glyph.name;
		for (int i = pos - 1; i >= 0; i--) {
			c = glyph_sequence.get_char (i);
			name = (!) c.to_string ();
			juxtaposed = (font.has_glyph (name)) ? (!) font.get_glyph (name) : font.get_space ().get_current ();

			if (font.has_glyph (last_name) && font.has_glyph (name)) {
				kern = classes.get_kerning (name, last_name);
			} else {
				kern = 0;
			}

			x -= juxtaposed.get_width ();
			x -= kern;

			marker_x = Glyph.xc () + left + x;
			marker_y = Glyph.yc () - baseline;
			if (!juxtaposed.is_empty ()
				&&(in_range (left + x, box_x1, box_x2)
				|| in_range (left + x + juxtaposed.get_width (), box_x1, box_x2))) {
				cr.save ();
				cr.scale (glyph.view_zoom, glyph.view_zoom);
				cr.translate (-glyph.view_offset_x, -glyph.view_offset_y);
				Theme.color (cr, "Foreground 1");
				Svg.draw_svg_path (cr, juxtaposed.get_svg_data (), marker_x, marker_y);
				cr.restore ();
			}

			last_name = name;
		}
	}

	/** @return left side bearing */
	public double get_lsb () {
		return get_line ("left").pos;
	}

	/** @return bottom line */
	public double get_baseline () {
		Font font = BirdFont.get_current_font ();
		return font.base_line;
	}

	void draw_background_glyph (WidgetAllocation allocation, Context cr) {
		double left, baseline, current_left;
		Glyph g;
		Font font = BirdFont.get_current_font ();

		current_left = get_line ("left").pos;

		if (background_glyph != null) {
			g = (!) background_glyph;
			baseline = font.base_line;
			left = g.get_line ("left").pos;
			cr.save ();
			cr.scale (view_zoom, view_zoom);
			cr.translate (-view_offset_x, -view_offset_y);
			Theme.color (cr, "Background Glyph");

			Svg.draw_svg_path (cr, g.get_svg_data (),
				Glyph.xc () + left - (left - current_left) ,
				Glyph.yc () - baseline);
			cr.restore ();
		}

	}

	public string get_hex () {
		return Font.to_hex_code (unichar_code);
	}

	public override void move_view (double x, double y) {
		view_offset_x += x / view_zoom;
		view_offset_y += y / view_zoom;
		GlyphCanvas.redraw ();
	}

	/** Scroll or zoom from tap events. */
	public void change_view_event (int finger, int x, int y) {
		double dx, dy;
		double last_distance, new_distance;

		dx = 0;
		dy = 0;

		new_distance = 0;

		if (last_tap0_y == -1 || last_tap0_x == -1 || last_tap1_y == -1 || last_tap1_x == -1) {
			return;
		}

		if (finger == 0) {
			dx = last_tap0_x - x;
			dy = last_tap0_y - y;
			new_distance = Path.distance (last_tap1_x, x, last_tap1_y, y);
		}

		if (finger == 1) {
			dx = last_tap1_x - x;
			dy = last_tap1_y - y;
			new_distance = Path.distance (last_tap0_x, x, last_tap0_y, y);
		}

		last_distance = Path.distance (last_tap0_x, last_tap1_x, last_tap0_y, last_tap1_y);

		if (zoom_distance != 0) {
			zoom_tap (zoom_distance - new_distance);
		}

		if (finger == 1) {
			warning (@"dx $dx dy $dy last_tap1_x $last_tap1_x  last_tap1_y $last_tap1_y   x $x  y $y");
			move_view (dx, dy);
		}

		zoom_distance = new_distance;
	}

	public override void tap_down (int finger, int x, int y) {
		TimeoutSource delay;

		if (finger == 0) {
			delay = new TimeoutSource (400); // wait for second finger
			delay.set_callback(() => {
				if (!change_view && !ignore_input) {
					button_press (1, x, y);
				}
				return false;
			});
			delay.attach (null);

			last_tap0_x = x;
			last_tap0_y = y;
		}

		if (finger == 1) {
			change_view = true;
			last_tap1_x = x;
			last_tap1_y = y;
		}
	}

	public override void tap_up (int finger, int x, int y) {
		if (finger == 0) {
			button_release (1, x, y);

			last_tap0_x = -1;
			last_tap0_y = -1;
		}

		if (finger == 1) {
			last_tap1_x = -1;
			last_tap1_y = -1;

			change_view = false;
			zoom_distance = 0;
		}
	}

	public override void tap_move (int finger, int x, int y) {
		if (!change_view) {
			motion_notify (x, y);
		} else {
			change_view_event (finger, x, y);
		}

		if (finger == 0) {
			last_tap0_x = x;
			last_tap0_y = y;
		}

		if (finger == 1) {
			last_tap1_x = x;
			last_tap1_y = y;
		}
	}

	public void update_spacing_class () {
		Font font = BirdFont.get_current_font ();
		GlyphCollection? g;
		GlyphCollection gc;
		Glyph glyph;
		Gee.ArrayList<string> s;
		SpacingData sd;

		sd = font.get_spacing ();
		s = sd.get_all_connections ((!) unichar_code.to_string ());
		foreach (string l in s) {
			if (l != (!) unichar_code.to_string ()) {
				g = font.get_glyph_collection (l);
				if (g != null) {
					gc = (!) g;
					glyph = gc.get_current ();

					if (glyph.left_limit == glyph.right_limit) {
						warning ("Zero width glyph in kerning class.");
					}

					left_limit = glyph.left_limit;
					right_limit = glyph.right_limit;

					break;
				}
			}
		}

		add_help_lines ();
	}

	public void update_other_spacing_classes () {
		Font font = BirdFont.get_current_font ();
		GlyphCollection? g;
		GlyphCollection gc;
		Glyph glyph;
		Gee.ArrayList<string> s;
		SpacingData sd;

		sd = font.get_spacing ();
		s = sd.get_all_connections (get_name ());

		foreach (string l in s) {
			if (l != (!) unichar_code.to_string ()) {
				g = font.get_glyph_collection (l);
				if (g != null) {
					gc = (!) g;
					glyph = gc.get_current ();
					glyph.left_limit = left_limit;
					glyph.right_limit = right_limit;
				}
			}
		}
	}

	public void set_cache (string key, Surface cache) {
		glyph_cache.set (key, cache);
	}

	public bool has_cache (string key) {
		return glyph_cache.has_key (key);
	}

	public Surface get_cache (string key) {
		if (unlikely (!has_cache (key))) {
			warning ("No cache for glyph.");
			return new ImageSurface (Cairo.Format.ARGB32, 1, 1);
		}

		return glyph_cache.get (key);
	}

	public void add_custom_guide () {
		TextListener listener;

		listener = new TextListener (t_("Guide"), "", t_("Add"));

		listener.signal_text_input.connect ((text) => {
			new_guide_name = text;
		});

		listener.signal_submit.connect (() => {
			Line guide;
			double position;

			position = path_coordinate_y (allocation.height / 2.0);
			guide = new Line (new_guide_name, new_guide_name, position);
			horizontal_help_lines.add (guide);

			BirdFont.get_current_font ().custom_guides.add (guide);

			TabContent.hide_text_input ();
			GlyphCanvas.redraw ();
		});

		TabContent.show_text_input (listener);
	}

	public Path? get_last_path ()  {
		var paths = get_all_paths ();
		return_val_if_fail (paths.size > 0, null);
		return paths.get (paths.size - 1);
	}

	public void move_layer_up () {
		Layer layer = get_current_layer ();

		if (current_layer + 2 <= layers.subgroups.size) {
			return_if_fail (0 <= current_layer + 2 <= layers.subgroups.size);
			layers.subgroups.insert (current_layer + 2, layer);

			return_if_fail (0 <= current_layer + 1 < layers.subgroups.size);
			layers.subgroups.remove_at (current_layer);

			set_current_layer (layer);
		}
	}

	public void move_layer_down () {
		Layer layer = get_current_layer ();

		if (current_layer >= 1) {
			return_if_fail (0 <= current_layer - 1 < layers.subgroups.size);
			layers.subgroups.insert (current_layer - 1, layer);

			return_if_fail (0 <= current_layer + 1 < layers.subgroups.size);
			layers.subgroups.remove_at (current_layer + 1);

			set_current_layer (layer);
		}
	}

	public void fix_curve_orientation () {
		int inside_count;
		bool inside;
		Path outline;

		foreach (Path p1 in get_visible_paths ()) {
			inside_count = 0;

			foreach (Path p2 in get_visible_paths ()) {
				if (p1 != p2) {
					inside = true;
					outline = p2.flatten (100);

					foreach (EditPoint ep in p1.points) {
						if (!SvgParser.is_inside (ep, outline)) {
							inside = false;
						}
					}

					if (inside) {
						inside_count++;
					}
				}
			}

			if (inside_count % 2 == 0) {
				p1.force_direction (Direction.CLOCKWISE);
			} else {
				p1.force_direction (Direction.COUNTER_CLOCKWISE);
			}
		}
	}

	public override void magnify (double magnification) {
		double x_center = path_coordinate_x (xc ());
		double y_center = path_coordinate_y (yc ());

		view_zoom *= 1 + magnification;

		view_offset_x -= path_coordinate_x (xc ()) - x_center;
		view_offset_y += path_coordinate_y (yc ()) - y_center;

		GlyphCanvas.redraw ();
	}
	
	public Glyph self_interpolate (double weight) {
		Glyph g1 = copy ();
		Glyph g2 = copy ();
		
		g1.fix_curve_orientation ();
		g2.layers = new Layer (); // remove all paths
		
		foreach (Path p in g1.get_visible_paths ()) {
			bool counter = !p.is_clockwise ();

			g2.add_path (p.copy ());
			
			p.remove_points_on_points ();
			Path master = p.get_self_interpolated_master (counter, weight);
			p = p.interpolate_estimated_path (master, weight);
			p.recalculate_linear_handles ();
			
			g2.add_path (p);
			g2.add_path (master);
		}
		
		return g2;
	}
}

}

Contact

Email: johan.mattsson.m@gmail.com
Mastodon: typo.social/@birdfont
GitHub: johanmattssonm
LinkedIn: johan-mattsson-2ab324112