Clone in a terminal:
KerningDisplay.vala
/*
	Copyright (C) 2012 2014 2015 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;

namespace BirdFont {

/** Kerning context. */
public class KerningDisplay : FontDisplay {

	public bool suppress_input = false;
	
	Gee.ArrayList <GlyphSequence> first_row;
	Gee.ArrayList <GlyphSequence> rows;
	
	int active_handle = -1;
	int selected_handle = -1;
	bool moving = false;
	Glyph left_active_glyph = new Glyph ("null", '\0');
	Glyph right_active_glyph = new Glyph ("null", '\0');
	
	double begin_handle_x = 0;
	double begin_handle_y = 0;
	
	double last_handle_x = 0;
	
	public bool text_input = false;

	Gee.ArrayList<UndoItem> undo_items;
	Gee.ArrayList<UndoItem> redo_items;
	bool first_update = true;

	Text kerning_label = new Text ();
	
	public bool adjust_side_bearings = false;
	public bool right_side_bearing = true;

	public static bool right_to_left = false; 	
	
	WidgetAllocation allocation = new WidgetAllocation ();
	
	public KerningDisplay () {
		GlyphSequence w = new GlyphSequence ();
		rows = new Gee.ArrayList <GlyphSequence> ();
		first_row = new Gee.ArrayList <GlyphSequence> ();
		undo_items = new Gee.ArrayList <UndoItem> ();
		redo_items = new Gee.ArrayList <UndoItem> ();
		w.set_otf_tags (KerningTools.get_otf_tags ());
		first_row.add (w);
	}

	public GlyphSequence get_first_row () {
		GlyphSequence first = new GlyphSequence ();
		Font font = BirdFont.get_current_font ();
		
		foreach (GlyphSequence s in first_row) {
			first.append (s.process_ligatures (font));
		}
		
		return first;
	}

	public void new_segment () {
		GlyphSequence s = new GlyphSequence ();
		s.set_otf_tags (KerningTools.get_otf_tags ());
		first_row.add (s);
	}

	public GlyphSequence get_last_segment () {
		if (first_row.size == 0) {
			new_segment ();
		}
		
		return first_row.get (first_row.size - 1);
	}
	
	Gee.ArrayList <GlyphSequence> get_all_rows () {
		Gee.ArrayList <GlyphSequence> r;
		
		r = new Gee.ArrayList <GlyphSequence> ();
		r.add (get_first_row ());
		foreach (GlyphSequence s in rows) {
			r.add (s);
		}
		
		return r;
	}

	public override string get_label () {
		return t_("Kerning");
	}
	
	public override string get_name () {
		return "Kerning";
	}

	public void show_parse_error () {
		string line1 = t_("The current kerning class is malformed.");
		string line2 = t_("Add single characters separated by space and ranges on the form A-Z.");
		string line3 = t_("Type “space” to kern the space character and “divis” to kern -.");
				
		MainWindow.show_dialog (new MessageDialog (line1 + " " + line2 + " " + line3));
	}
	
	public override void draw (WidgetAllocation allocation, Context cr) {
		draw_kerning_pairs (allocation, cr);
	}
	
	public double get_row_height () {
		Font current_font = BirdFont.get_current_font ();
		return current_font.top_limit - current_font.bottom_limit;
	}

	public void draw_kerning_pairs (WidgetAllocation allocation, Context cr) {
		Glyph glyph;
		double x, y, w, kern, alpha;
		double x2;
		double caret_y;
		int i, wi;
		Glyph? prev;
		GlyphSequence word_with_ligatures;
		GlyphRange? gr_left, gr_right;
		bool first_row = true;
		double row_height;
		Font font = BirdFont.get_current_font ();
		double item_size = 1.0 / KerningTools.font_size;
		double item_size2 = 2.0 / KerningTools.font_size;

		this.allocation = allocation;

		i = 0;
		
		// bg color
		cr.save ();
		Theme.color (cr, "Background 1");
		cr.rectangle (0, 0, allocation.width, allocation.height);
		cr.fill ();
		cr.restore ();
		
		cr.save ();
		cr.scale (KerningTools.font_size, KerningTools.font_size);
		
		glyph = MainWindow.get_current_glyph ();
		
		row_height = get_row_height ();
	
		alpha = 1;
		y = get_row_height () - font.base_line + 20;
		x = 20;
		w = 0;
		prev = null;
		kern = 0;
		
		if (right_to_left) {
			x = (allocation.width - 20) / KerningTools.font_size;
		}
				
		foreach (GlyphSequence word in get_all_rows ()) {
			wi = 0;
			word_with_ligatures = word.process_ligatures (font);
			gr_left = null;
			gr_right = null;
			foreach (Glyph? g in word_with_ligatures.glyph) {
				if (g == null) {
					continue;
				}
				
				if (prev == null || wi == 0) {
					kern = 0;
				} else {
					return_if_fail (wi < word_with_ligatures.ranges.size);
					return_if_fail (wi - 1 >= 0);
					
					gr_left = word_with_ligatures.ranges.get (wi - 1);
					gr_right = word_with_ligatures.ranges.get (wi);

					kern = get_kerning_for_pair (((!)prev).get_name (), ((!)g).get_name (), gr_left, gr_right);
				}
						
				// draw glyph
				if (g == null) {
					w = 50;
					alpha = 1;
				} else {
					alpha = 0;
					glyph = (!) g;

					cr.save ();
					glyph.add_help_lines ();

					if (right_to_left) {
						cr.translate (-kern + x - glyph.get_lsb () - glyph.get_width () - Glyph.xc (), glyph.get_baseline () + y  - Glyph.yc ());
					} else {						
						cr.translate (kern + x - glyph.get_lsb () - Glyph.xc (), glyph.get_baseline () + y  - Glyph.yc ());
					}
					
					glyph.draw_paths (cr);
					cr.restore ();
					
					w = glyph.get_width ();
				}
				
				// handle
				if (first_row && (active_handle == i || selected_handle == i)) {
					if (right_to_left) {
						x2 = x - kern / 2.0;
					} else {
						x2 = x + kern / 2.0;
					}
					
					cr.save ();
					
					if (selected_handle == i) {
						Theme.color (cr, "Foreground 1");
					} else { 
						cr.set_source_rgba (123/255.0, 123/255.0, 123/255.0, 1);
					}
					
					if (!adjust_side_bearings) {
						cr.move_to (x2 - 5 * item_size, y + 20 * item_size);
						cr.line_to (x2, y + 20 * item_size - 5 * item_size);
						cr.line_to (x2 + 5 * item_size, y + 20* item_size);
						cr.fill ();
						
						if (gr_left != null || gr_right != null) {
							cr.move_to (x2 - 5 * item_size, y + 20  * item_size);
							cr.line_to (x2 + 5 * item_size, y + 20  * item_size);
							cr.line_to (x2 + 5 * item_size, y + 24  * item_size);
							cr.line_to (x2 - 5 * item_size, y + 24  * item_size);
							cr.fill ();
						}
					} else {
						if (right_side_bearing) {
							cr.move_to (x2 - 5 * item_size2, y + 20 * item_size2);
							cr.line_to (x2, y + 20 * item_size2 - 5 * item_size2);
							cr.line_to (x2, y + 20* item_size2);
							cr.fill ();
						} else {
							cr.move_to (x2, y + 20 * item_size2);
							cr.line_to (x2, y + 20 * item_size2 - 5 * item_size2);
							cr.line_to (x2 + 5 * item_size2, y + 20* item_size2);
							cr.fill ();
						}
					}
					
					if (active_handle == i && !adjust_side_bearings) {
						cr.save ();
						cr.scale (1 / KerningTools.font_size, 1 / KerningTools.font_size);
						kerning_label.widget_x = x2 * KerningTools.font_size;
						kerning_label.widget_y = y * KerningTools.font_size + 40;
						kerning_label.draw (cr);
						cr.fill ();
						cr.restore ();
					}
					
					cr.restore ();
				}	
				
				if (right_to_left) {
					x -= w + kern;
				} else {
					x += w + kern;
				}
	
				// caption
				if (g == null || ((!)g).is_empty ()) {
					cr.save ();
					cr.set_source_rgba (153/255.0, 153/255.0, 153/255.0, alpha);
					cr.move_to (x - w / 2.0 - 5, y + 20);
					cr.set_font_size (10 * item_size);
					cr.show_text ("?");
					cr.restore ();
				}
							
				prev = g;
				
				wi++;
				i++;
			}
			
			// draw caret
			if (first_row) {
				x2 = x;
				caret_y = get_row_height () - font.base_line + 20;
				cr.save ();
				cr.set_line_width (1.0 / KerningTools.font_size);
				Theme.color_opacity (cr, "Foreground 1", 0.5);
				cr.move_to (x2, caret_y + 20);
				cr.line_to (x2, 20);
				cr.stroke ();
				cr.restore ();
				
				y += (50 / KerningTools.font_size);
			}
						
			y += row_height + 20;
			
			if (right_to_left) {
				x = (allocation.width - 20) / KerningTools.font_size;
			} else {				
				x = 20;
			}

			first_row = false;
			
			if (y > allocation.height) {
				break;
			}
		}
		
		for (int j = rows.size - 1; j > 30; j--) {
			rows.remove_at (j);
		}
		
		cr.fill ();
		cr.restore ();
	}

	private void display_kerning_value (double k) {
		string kerning = round (k);
		kerning_label = new Text (@"$(kerning)", 17);
	}
	
	private void set_active_handle_index (int h) {
		double kern = get_kerning_for_handle (h);
		active_handle = h;
		
		if (1 <= active_handle < get_first_row ().glyph.size) {
			display_kerning_value (kern);
		}
	}
	
	private double get_kerning_for_handle (int handle) {
		string a, b;
		GlyphRange? gr_left, gr_right;
		bool got_pair;

		got_pair = get_kerning_pair (handle, out a, out b, out gr_left, out gr_right);
		
		if (got_pair) {
			return get_kerning_for_pair (a, b, gr_left, gr_right);
		}
		
		return 0;
	}

	private bool get_kerning_pair (int handle, out string left, out string right, 
		out GlyphRange? range_left, out GlyphRange? range_right) {
		string a, b;
		Font font;
		int wi = 0;
		GlyphSequence word_with_ligatures;
		int ranges_index = 0;
		GlyphRange? gr_left, gr_right;
		int row_index = 0;

		font = BirdFont.get_current_font ();
		font.touch ();

		a = "";
		b = "";
		
		left = "";
		right = "";
		range_left = null;
		range_right = null;
		
		if (handle <= 0) {
			return false;
		}
		
		word_with_ligatures = get_first_row ();
		ranges_index = 0;
		foreach (Glyph? g in word_with_ligatures.glyph) {
			
			if (g == null) {
				continue;
			}
			
			b = ((!) g).get_name ();
			
			if (handle == wi && row_index == 0) {
				if (wi >= word_with_ligatures.ranges.size) {
					return false;
				}
				return_val_if_fail (wi - 1 >= 0, false);
				
				if (word_with_ligatures.ranges.size != word_with_ligatures.glyph.size) {
					return false;
				}
				
				gr_left = word_with_ligatures.ranges.get (wi - 1);
				gr_right = word_with_ligatures.ranges.get (wi);
				
				left = a;
				right = b;
				range_left = gr_left;
				range_right = gr_right;
				
				return true;
			}
			
			wi++;
			
			a = b;
		}

		return false;	
	}

	public void set_absolute_kerning (int handle, double val) {
		double kern;
		
		if (MenuTab.has_suppress_event ()) {
			return;
		}
		
		if (!adjust_side_bearings) {
			kern = get_kerning_for_handle (handle);
			set_space (handle, val - kern);	
		}
	}


	/** Adjust kerning or right side bearing. */
	private void set_space (int handle, double val) {
		string a, b;
		Font font;
		GlyphRange? gr_left, gr_right;

		font = BirdFont.get_current_font ();
		font.touch ();

		if (!adjust_side_bearings) {
			get_kerning_pair (handle, out a, out b, out gr_left, out gr_right);
			set_kerning_pair (a, b, ref gr_left, ref gr_right, val);
		} else {
			if (right_side_bearing) {
				left_active_glyph.right_limit += val;
				left_active_glyph.remove_lines ();
				left_active_glyph.add_help_lines ();
				left_active_glyph.update_other_spacing_classes ();
			} else {
				right_active_glyph.left_limit -= val;
				right_active_glyph.remove_lines ();
				right_active_glyph.add_help_lines ();
				right_active_glyph.update_other_spacing_classes ();
			}
		}
	}

	/** Class based gpos kerning. */
	public void set_kerning_pair (string a, string b,
			ref GlyphRange? gr_left, ref GlyphRange? gr_right,
			double val) {
		double kern;
		GlyphRange grl, grr;
		KerningClasses classes;
		string n, f;
		bool has_kerning;
		Font font;
		
		font = BirdFont.get_current_font ();
		font.touch ();
		classes = font.get_kerning_classes ();
			
		kern = get_kerning_for_pair (a, b, gr_left, gr_right);
		
		try {
			if (gr_left == null) {
				grl = new GlyphRange ();
				grl.parse_ranges (a);
				gr_left = grl; // update the range list
			} else {
				grl = (!) gr_left;
			}

			if (gr_right == null) {
				grr = new GlyphRange ();
				grr.parse_ranges (b);
				gr_right = grr;
			} else {
				grr = (!) gr_right;
			}
			
			if (first_update) {
				f = grl.get_all_ranges ();
				n = grr.get_all_ranges ();
				has_kerning = classes.has_kerning (f, n);
				undo_items.add (new UndoItem (f, n, kern, has_kerning));
				redo_items.clear ();
				first_update = false;
			}
			
			classes.set_kerning (grl, grr, kern + val);
			display_kerning_value (kern + val);
		} catch (MarkupError e) {
			// FIXME: unassigned glyphs and ligatures
			warning (e.message);
		}
	}

	public static double get_kerning_for_pair (string a, string b, GlyphRange? gr_left, GlyphRange? gr_right) {
		KerningClasses k = BirdFont.get_current_font ().get_kerning_classes ();
		return k.get_kerning_for_pair (a, b, gr_left, gr_right);
	}
	
	public override void selected_canvas () {
		KeyBindings.set_require_modifier (true);
	}
	
	public void add_kerning_class (int index) {
		add_range (KerningTools.get_kerning_class (index));
	}
	
	public void add_range (GlyphRange range) {
		Font font = BirdFont.get_current_font ();
		Glyph? glyph;
		GlyphSequence s;
		
		glyph = font.get_glyph_by_name (range.get_char (0));
		
		if (glyph == null) {
			warning ("Kerning range is not represented by a valid glyph.");
			return;
		}
		
		if (first_row.size == 0) {
			s = new GlyphSequence ();
			first_row.add (s);
		} else {
			s = first_row.get (first_row.size - 1);
		}
		
		s.glyph.add ((!) glyph);
		s.ranges.add (range);
		
		GlyphCanvas.redraw ();
	}

	void set_selected_handle (int handle) {
		Glyph? g;
		selected_handle = handle;
		GlyphSequence sequence_with_ligatures;

		sequence_with_ligatures = get_first_row ();
		
		if (selected_handle <= 0) {
			selected_handle = 1;
		}
		
		if (selected_handle >= sequence_with_ligatures.glyph.size) {
			selected_handle = (int) sequence_with_ligatures.glyph.size - 1;
		}
		
		set_active_handle_index (handle);
		
		if (0 <= selected_handle - 1 < sequence_with_ligatures.glyph.size) {
			g = sequence_with_ligatures.glyph.get (selected_handle - 1);
			if (g != null) {
				left_active_glyph = (!) g;
			}
		}

		if (0 <= selected_handle < sequence_with_ligatures.glyph.size) {
			g = sequence_with_ligatures.glyph.get (selected_handle);
			if (g != null) {
				right_active_glyph = (!) g;
			}
		}
		
		GlyphCanvas.redraw ();
	}

	public static void previous_pair () {
		KerningDisplay kd;
		FontDisplay fd;
		SpacingTab st;
		
		fd = MainWindow.get_current_display ();
		
		if (fd is SpacingTab) {
			st = (SpacingTab) fd;
			if (!st.right_side_bearing) {
				st.right_side_bearing = true;
			} else {
				st.right_side_bearing = false;
				st.set_selected_handle (st.selected_handle - 1);
			}
		} else if (fd is KerningDisplay) {
			kd = (KerningDisplay) fd;
			kd.set_selected_handle (kd.selected_handle - 1);	
		}
		
		GlyphCanvas.redraw ();
	}
	
	public static void next_pair () {
		KerningDisplay kd;
		FontDisplay fd;
		SpacingTab st;
		
		fd = MainWindow.get_current_display ();
		
		if (fd is SpacingTab) {
			st = (SpacingTab) fd;
	
			if (st.right_side_bearing) {
				st.right_side_bearing = false;
			} else {
				st.right_side_bearing = true;
				st.set_selected_handle (st.selected_handle + 1);
			}
		} else if (fd is KerningDisplay) {
			kd = (KerningDisplay) fd;
			kd.set_selected_handle (kd.selected_handle + 1);	
		}
		
		GlyphCanvas.redraw ();
	}

	private static string round (double d) {
		char[] b = new char [22];
		unowned string s = d.format (b, "%.2f");
		string n = s.dup ();

		n = n.replace (",", ".");

		if (n == "-0.00") {
			n = "0.00";
		}
		
		return n;
	}
		
	public override void key_press (uint keyval) {
		unichar c;
		
		if (MenuTab.has_suppress_event ()) { // don't update kerning while saving font
			return;
		}
		
		c = (unichar) keyval;

		if (suppress_input) {
			return;
		}

		if ((keyval == 'u' || keyval == 'U') && KeyBindings.has_ctrl ()) {
			insert_unichar ();
		} else {
			if (keyval == Key.LEFT && KeyBindings.modifier == NONE) {
				first_update = true;
				set_space (selected_handle, -1 / KerningTools.font_size);
			}
			
			if (keyval == Key.RIGHT && KeyBindings.modifier == NONE) {
				first_update = true;
				set_space (selected_handle, 1 / KerningTools.font_size);
			}

			if (KeyBindings.modifier == NONE 
				|| KeyBindings.modifier == SHIFT 
				|| KeyBindings.modifier == ALT) {		
				
				if (keyval == Key.BACK_SPACE) {
					remove_last_character ();
				}
				
				if (c == Key.ENTER) {
					new_line ();
				}
				
				add_character (c);
			}
		}
		
		GlyphCanvas.redraw ();
	}
	
	void remove_last_character () {
		if (first_row.size > 0) {
			GlyphSequence gs = first_row.get (first_row.size - 1);
			
			if (gs.glyph.size > 0) {
				gs.glyph.remove_at (gs.glyph.size - 1);
				return_if_fail (gs.ranges.size > 0);
				gs.ranges.remove_at (gs.ranges.size - 1);
			} else {
				first_row.remove_at (first_row.size - 1);
				remove_last_character ();
			}
		}		
	}
	
	public void insert_unichar () {
		TextListener listener;
		string submitted_value = "";
		string unicodestart;
		
		unicodestart = (KeyBindings.has_shift ()) ? "" : "U+";
		listener = new TextListener (t_("Unicode"), unicodestart, t_("Insert"));
		
		listener.signal_text_input.connect ((text) => {
			submitted_value = text;
			
			if (MenuTab.has_suppress_event ()) {
				return;
			}
			
			GlyphCanvas.redraw ();
		});
		
		listener.signal_submit.connect (() => {
			unichar c;
			TabContent.hide_text_input ();
			
			text_input = false;
			suppress_input = false;
			
			if (submitted_value.has_prefix ("u+") || submitted_value.has_prefix ("U+")) {
				c = Font.to_unichar (submitted_value);
				add_character (c);
			} else {
				add_text (submitted_value);
			}
		});
		
		suppress_input = true;
		text_input = true;
		TabContent.show_text_input (listener);
	}
	
	public void new_line () {
		rows.insert (0, get_first_row ());
		first_row = new Gee.ArrayList<GlyphSequence> ();
		GlyphSequence gs = new GlyphSequence ();
		gs.set_otf_tags (KerningTools.get_otf_tags ());
		first_row.add (gs);
	}
	
	void add_character (unichar c) {
		Glyph? g;
		string name;

		if (MenuTab.has_suppress_event ()) {
			return;
		}
		
		if (!is_modifier_key (c) && c.validate ()) {
			name = (!) c.to_string ();
			g = BirdFont.get_current_font ().get_glyph_by_name (name);
			inser_glyph (g);
		}
	}
	
	public void inser_glyph (Glyph? g) {
		int handle;
		
		if (first_row.size == 0) {
			GlyphSequence gs = new GlyphSequence ();
			gs.set_otf_tags (KerningTools.get_otf_tags ());
			first_row.add (gs);
		}
		
		if (g != null) {
			first_row.get (first_row.size - 1).glyph.add (g);
			first_row.get (first_row.size - 1).ranges.add (null);
			
			handle = get_first_row ().glyph.size - 1;
			set_selected_handle (handle);
			set_active_handle_index (handle);
		}
	}
	
	public override void motion_notify (double ex, double ey) {
		double k, y;

		if (MenuTab.has_suppress_event ()) {
			return;
		}
				
		if (!moving) {
			set_active_handle (ex, ey);
		} else {	
			y = 1;
			
			if (Math.fabs (ey - begin_handle_y) > 20) {
				y = ((Math.fabs (ey - begin_handle_y) / 100) + 1);
			}
			
			k = (ex - last_handle_x) / y; // y-axis is for variable precision
			k /= KerningTools.font_size;

			if (right_to_left) {
				k *= -1;
			}			
			
			set_space (selected_handle, k);
			GlyphCanvas.redraw ();
		}
		
		last_handle_x = ex;
	}
	
	public void set_active_handle (double ex, double ey) {
		double w = 0;
		double d, kern;
		double min = double.MAX;
		int i = 0;
		int row_index = 0;
		int col_index = 0;
		Glyph glyph = new Glyph.no_lines ("");
		double fs = KerningTools.font_size;
		double x = 20;

		if (right_to_left) {
			x = (allocation.width - 20) / KerningTools.font_size;
		}		
			
		GlyphRange? gr_left, gr_right;
		
		Glyph? prev = null;
		string gl_name = "";
		GlyphSequence word_with_ligatures;
		
		col_index = 0;
		
		word_with_ligatures = get_first_row ();
		foreach (Glyph? g in word_with_ligatures.glyph) {
			if (g == null) {
				w = 50;
				warning ("glyph does not exist");	
			} else {
				glyph = (!) g;
				w = glyph.get_width ();
			}
			
			gl_name = glyph.get_name ();
			
			if (prev == null && col_index != 0) {
				warning (@"previous glyph does not exist row: $row_index column: $col_index");
			}
			
			if (prev == null || col_index == 0) {
				kern = 0;
			} else {
				return_if_fail (col_index < word_with_ligatures.ranges.size);
				return_if_fail (col_index - 1 >= 0);
				
				gr_left = word_with_ligatures.ranges.get (col_index - 1);
				gr_right = word_with_ligatures.ranges.get (col_index);

				kern = get_kerning_for_pair (((!)prev).get_name (), ((!)g).get_name (), gr_left, gr_right);
			}
			
			if (right_to_left) {
				d = Math.pow (fs * (x - kern) - ex, 2);
			} else {				
				d = Math.pow (fs * (x + kern) - ex, 2);
			}
			
			if (d < min) {
				min = d;
				
				if (ex != fs * (x + kern)) {	// don't swap direction after button release
					right_side_bearing = ex < fs * (x + kern); // right or left side bearing handle
				}
				
				if (active_handle != i - row_index) {
					set_active_handle_index (i - row_index);
					GlyphCanvas.redraw ();
				}
				
				if (col_index == word_with_ligatures.glyph.size || col_index == 0) {
					set_active_handle_index (-1);
				} else {
					set_active_handle_index (active_handle + row_index);
				}
			}
			
			prev = g;
			
			if (right_to_left) {
				x -= w + kern;
			} else {
				x += w + kern;
			}
			
			i++;
			col_index++;
		}
		
		row_index++;
		x = 20;
	}
	
	public override void button_release (int button, double ex, double ey) {
		set_active_handle (ex, ey);
		moving = false;
		first_update = true;
		
		if (button == 3 || text_input) {
			set_kerning_by_text ();
		}
	}
	
	public void set_kerning_by_text () {
		TextListener listener;
		string kerning = @"$(get_kerning_for_handle (selected_handle))";

		if (MenuTab.has_suppress_event ()) {
			return;
		}
				
		if (selected_handle == -1) {
			set_selected_handle (0);
		}
		
		listener = new TextListener (t_("Kerning"), kerning, t_("Close"));
		
		listener.signal_text_input.connect ((text) => {
			string submitted_value;
			double parsed_value;
			
			if (MenuTab.has_suppress_event ()) {
				return;
			}
		
			submitted_value = text.replace (",", ".");
			parsed_value = double.parse (submitted_value);
			set_absolute_kerning (selected_handle, parsed_value);
			GlyphCanvas.redraw ();
		});
		
		listener.signal_submit.connect (() => {
			TabContent.hide_text_input ();
			text_input = false;
			suppress_input = false;
		});
		
		suppress_input = true;
		text_input = true;
		TabContent.show_text_input (listener);
		
		GlyphCanvas.redraw ();
	}
	
	public override void button_press (uint button, double ex, double ey) {
		set_active_handle (ex, ey);
		set_selected_handle (active_handle);
		begin_handle_x = ex;
		begin_handle_y = ey;
		last_handle_x = ex;
		moving = true;
	}
	
	/** Insert text form clipboard. */
	public void add_text (string t) {
		int c;
		
		if (MenuTab.has_suppress_event ()) {
			return;
		}
		
		c = t.char_count ();
		for (int i = 0; i <= c; i++) {
			add_character (t.get_char (t.index_of_nth_char (i)));
		}
		
		GlyphCanvas.redraw ();
	}

	public override void undo () {
		UndoItem ui;
		UndoItem redo_state;
		
		if (MenuTab.has_suppress_event ()) {
			return;
		}
		
		if (undo_items.size == 0) {
			return;
		}
		
		ui = undo_items.get (undo_items.size - 1);
		
		redo_state = apply_undo (ui);
		redo_items.add (redo_state);
		
		undo_items.remove_at (undo_items.size - 1);
	}

	public override void redo () {
		UndoItem ui;
		
		if (MenuTab.has_suppress_event ()) {
			return;
		}
		
		if (redo_items.size == 0) {
			return;
		}
		
		ui = redo_items.get (redo_items.size - 1);
		apply_undo (ui);
		redo_items.remove_at (redo_items.size - 1);
	}
	
	/** @return redo state. */
	public UndoItem apply_undo (UndoItem ui) {
		KerningClasses classes = BirdFont.get_current_font ().get_kerning_classes ();
		GlyphRange glyph_range_first, glyph_range_next;
		Font font = BirdFont.get_current_font ();
		string l, r;
		UndoItem redo_state = new UndoItem ("", "", 0, false);
		double? k;
		
		l = GlyphRange.unserialize (ui.first);
		r = GlyphRange.unserialize (ui.next);
				
		try {
			glyph_range_first = new GlyphRange ();
			glyph_range_next = new GlyphRange ();
			
			glyph_range_first.parse_ranges (ui.first);
			glyph_range_next.parse_ranges (ui.next);
				
			if (!ui.has_kerning) {
				if (glyph_range_first.is_class () || glyph_range_next.is_class ()) {
					redo_state.first = glyph_range_first.get_all_ranges ();
					redo_state.next = glyph_range_next.get_all_ranges ();
					redo_state.has_kerning = true;
					redo_state.kerning = classes.get_kerning_for_range (glyph_range_first, glyph_range_next);
					
					classes.delete_kerning_for_class (ui.first, ui.next);
				} else {
					
					redo_state.first = ui.first;
					redo_state.next = ui.next;
					redo_state.has_kerning = true;
					k = classes.get_kerning_for_single_glyphs (ui.first, ui.next);
					
					if (k != null) {
						redo_state.kerning = (!) k;
					} else {
						warning ("No kerning");
					}
					
					classes.delete_kerning_for_pair (ui.first, ui.next);
				}
			} else if (glyph_range_first.is_class () || glyph_range_next.is_class ()) {
				glyph_range_first = new GlyphRange ();
				glyph_range_next = new GlyphRange ();
				
				glyph_range_first.parse_ranges (ui.first);
				glyph_range_next.parse_ranges (ui.next);

				redo_state.first = glyph_range_first.get_all_ranges ();
				redo_state.next = glyph_range_next.get_all_ranges ();
				k = classes.get_kerning_for_range (glyph_range_first, glyph_range_next);
				
				if (k != null) {
					redo_state.kerning = (!) k;
					redo_state.has_kerning = true;
				} else {
					redo_state.has_kerning = false;
				}
									
				classes.set_kerning (glyph_range_first, glyph_range_next, ui.kerning);
			} else {
				redo_state.first = ui.first;
				redo_state.next = ui.next;
				redo_state.has_kerning = true;
				k = classes.get_kerning_for_single_glyphs (ui.first, ui.next);
				
				if (k != null) {
					redo_state.kerning = (!) k;
					redo_state.has_kerning = true;
				} else {
					redo_state.has_kerning = false;
				}
					
				classes.set_kerning_for_single_glyphs (ui.first, ui.next, ui.kerning);
			}
		} catch (MarkupError e) {
			warning (e.message);
		}
		
		font.touch ();
		GlyphCanvas.redraw ();
		
		return redo_state;
	}

	public override void zoom_in () {
		KerningTools.font_size += 0.1;
		
		if (KerningTools.font_size > 3) {
			KerningTools.font_size = 3;
		}

		KerningTools.zoom_bar.set_zoom (KerningTools.font_size / 3);
		GlyphCanvas.redraw ();
	}

	public override void zoom_out () {
		KerningTools.font_size -= 0.1;
		
		if (KerningTools.font_size < 0.3) {
			KerningTools.font_size = 0.3;
		}
		
		KerningTools.zoom_bar.set_zoom (KerningTools.font_size / 3);
		GlyphCanvas.redraw ();
	}

	public override bool needs_modifier () {
		return true;
	}

	public class UndoItem : GLib.Object {
		public string first;
		public string next;
		public double kerning;
		public bool has_kerning;
		
		public UndoItem (string first, string next, double kerning, bool has_kerning) {
			this.first = first;
			this.next = next;
			this.kerning = kerning;
			this.has_kerning = has_kerning;
		}
	}
}

}

Contact

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