Clone in a terminal:
KerningTools.vala
/*
	Copyright (C) 2013 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;
using Math;

namespace BirdFont {

public class KerningTools : ToolCollection  {
	public static Gee.ArrayList<Expander> expanders;
	public static int next_class = 0;
	public static Expander classes;
	
	public static double font_size = 1;
	public static ZoomBar zoom_bar;
	
	public static Tool previous_kerning_string;
	public static Tool next_kerning_string;
	
	public static Expander otf_features;
	
	static OtfTags active_otf_features;
	
	public KerningTools () {
		selected ();
	}
	
	public override void selected () {
		init ();
	}
	
	public static OtfTags get_otf_tags () {
		if (is_null (active_otf_features)) {
			return new OtfTags ();
		}
		
		return active_otf_features;
	}

	public static void init () {
		Font font = BirdFont.get_current_font ();
		
		active_otf_features = new OtfTags ();
		
		Expander kerning_tools = new Expander (t_("Kerning Tools"));
		
		if (is_null (classes)) {
			classes = new Expander ();
			update_kerning_classes ();
		}
		
		expanders = new Gee.ArrayList<Expander> ();

		Expander font_name = new Expander ();
		font_name.add_tool (new FontName ());

		Expander zoom_expander = new Expander (t_("Font Size"));

		zoom_bar = new ZoomBar ();
		zoom_bar.new_zoom.connect ((z) => {
			Font f;
			
			font_size = 3 * z;
			
			if (font_size < 0.1) {
				font_size = 0.1;
			}
			
			f = BirdFont.get_current_font ();
			f.settings.set_setting ("kerning_zoom", @"$z");
			
			GlyphCanvas.redraw ();
		});
		zoom_expander.add_tool (zoom_bar);
		
		Tool new_kerning_class = new Tool ("kerning_class", t_("Create new kerning class."));
		new_kerning_class.select_action.connect ((self) => {
			Font f = BirdFont.get_current_font ();
			string label = t_("Kerning class");
			KerningRange kr = new KerningRange (f, @"$label $(++next_class)");
			classes.add_tool (kr);
			self.set_selected (false);
			classes.clear_cache ();
			classes.redraw ();
		});
		kerning_tools.add_tool (new_kerning_class);

		Tool text_kerning = new Tool ("kerning_text_input", t_("Use text input to enter kerning values."));
		text_kerning.select_action.connect ((self) => {
			KerningDisplay d = MainWindow.get_kerning_display ();
			d.set_kerning_by_text ();
			self.set_selected (false);
		});
		kerning_tools.add_tool (text_kerning);

		Tool insert_last = new Tool ("insert_glyph_from_overview", t_("Insert glyph from overview"));
		insert_last.select_action.connect ((self) => {
			KerningDisplay d = MainWindow.get_kerning_display ();
			GlyphSelection gs = new GlyphSelection ();
			
			gs.selected_glyph.connect ((gc) => {
				d.inser_glyph (gc.get_current ());
				MainWindow.get_tab_bar ().select_tab_name ("Kerning");
			});
			
			GlyphCanvas.set_display (gs);
			self.set_selected (false);
		});
		kerning_tools.add_tool (insert_last);

		Tool insert_unicode = new Tool ("insert_unichar", t_("Insert character by unicode value"));
		insert_unicode.select_action.connect ((self) => {
			KerningDisplay d = MainWindow.get_kerning_display ();
			d.insert_unichar ();
			self.set_selected (false);
		});
		kerning_tools.add_tool (insert_unicode);

		Tool right_to_left = new Tool ("right_to_left", t_("Right to left"));
		right_to_left.select_action.connect ((self) => {
			KerningDisplay.right_to_left = !KerningDisplay.right_to_left;
			right_to_left.set_selected (KerningDisplay.right_to_left);
			GlyphCanvas.redraw ();
		});
		kerning_tools.add_tool (right_to_left);
		
		string empty_kerning_text = t_("Open a text file with kerning strings first.");
		
		previous_kerning_string = new Tool ("previous_kerning_string", t_("Previous kerning string"));
		previous_kerning_string.select_action.connect ((self) => {
			FontDisplay fd = MainWindow.get_current_display ();
			KerningDisplay d = (KerningDisplay) fd;
			Font f = BirdFont.get_current_font ();
			string w = f.kerning_strings.previous ();
			
			if (f.kerning_strings.is_empty ()) {
				MainWindow.show_message (empty_kerning_text);
			} else if (w == "") {
				MainWindow.show_message (t_("You have reached the beginning of the list."));
			} else {
				d.new_line ();
				d.add_text (w);
			}
			self.set_selected (false);
		});
		kerning_tools.add_tool (previous_kerning_string);

		next_kerning_string = new Tool ("next_kerning_string", t_("Next kerning string"));
		next_kerning_string.select_action.connect ((self) => {
			FontDisplay fd = MainWindow.get_current_display ();
			KerningDisplay d = (KerningDisplay) fd;
			Font f = BirdFont.get_current_font ();
			string w = f.kerning_strings.next ();
			
			if (f.kerning_strings.is_empty ()) {
				MainWindow.show_message (empty_kerning_text);
			} else if (w == "") {
				MainWindow.show_message (t_("You have reached the end of the list."));
			} else {
				d.new_line ();
				d.add_text (w);
			}
			self.set_selected (false);
		});
		kerning_tools.add_tool (next_kerning_string);
				
		otf_features = new Expander (t_("Substitutions"));
		
		foreach (string tag in font.alternates.get_all_tags ()) {
			add_otf_label (tag);
		}

		kerning_tools.set_persistent (false);
		kerning_tools.set_unique (false);
		
		classes.set_persistent (true);
		classes.set_unique (true);
		
		expanders.add (font_name);
		expanders.add (zoom_expander);
		expanders.add (kerning_tools);
		expanders.add (otf_features);		
		expanders.add (classes);
	}
	
	public static void add_otf_label (string tag) {
		OtfLabel otf_label = new OtfLabel (tag);
		FontSettings fs = BirdFont.get_current_font ().settings;
		
		otf_features.add_tool (otf_label);
		otf_label.otf_feature_activity.connect ((enable, tag) => {
			OtfTags tags = active_otf_features.copy ();
			KerningDisplay kd = MainWindow.get_kerning_display ();
			kd.new_segment ();
			
			// create a new feature set in order to keep the features
			// for other parts of the text in kerning tab
			active_otf_features = tags;
			
			if (enable) {
				tags.add (tag);
				fs.set_setting (@"kerning_$(tag)", "true");
			} else {
				tags.remove (tag);
				fs.set_setting (@"kerning_$(tag)", "false");
			}
			
			kd.get_last_segment ().set_otf_tags (tags);
			
			GlyphCanvas.redraw ();
		});
		
		bool enable = fs.get_setting (@"kerning_$(tag)") == "true";
		otf_label.set_selected_tag (enable);
	}
	
	public static void add_unique_class (KerningRange kerning_class) {
		KerningRange k;
		
		if (is_null (classes)) { // FIXME: export without tools
			init ();
		}
		
		foreach (Tool t in classes.tool) {
			if (!(t is KerningRange)) {
				warning ("Tool is not kerning range");
				return;
			}

			k = (KerningRange) t;
			if (k.glyph_range.get_all_ranges () == kerning_class.glyph_range.get_all_ranges ()) {
				return;
			}
		}

		classes.add_tool (kerning_class);
	}
	
	public static GlyphRange get_kerning_class (int index) {
		if (likely (0 <= index < classes.tool.size)) {
			return ((KerningRange) classes.tool.get (index)).glyph_range;
		} else {
			warning ("Index out of bounds.");
			return new GlyphRange ();
		}
	}
	
	public static void update_kerning_classes () {
		Font font = BirdFont.get_current_font ();
		KerningClasses k = font.get_kerning_classes ();
		KerningRange kr;
		GlyphRange r;
		int i;
		
		remove_all_kerning_classes ();
		
		for (i = 0; i < k.classes_first.size; i++) {
			r = k.classes_first.get (i);
			if (r.is_class ()) {
				kr = new KerningRange (font);
				kr.set_ranges (r.get_all_ranges ());
				add_unique_class (kr);
			}
			
			r = k.classes_last.get (i);
			if (r.is_class ()) {
				kr = new KerningRange (font);
				kr.set_ranges (r.get_all_ranges ());
				add_unique_class (kr);
			}
		}
		
		classes.clear_cache ();
		classes.redraw ();
	}

	private static void remove_all_kerning_classes () {
		if (is_null (classes)) { // FIXME: export without tools
			init ();
		}
		
		classes.tool.clear ();
		
		if (!is_null (MainWindow.get_toolbox ())) {
			MainWindow.get_toolbox ().update_expanders ();
		}
	}
	
	public static void remove_empty_classes () {
		KerningRange kr;
		int i;
		
		if (classes.tool.size == 0) {
			return;
		}
		
		i = 0;
		foreach (Tool t in classes.tool) {
			return_if_fail (t is KerningRange);
			
			kr = (KerningRange) t;
			if (kr.glyph_range.is_empty ()) {
				classes.tool.remove_at (i);
				remove_empty_classes ();
				Toolbox.redraw_tool_box ();
				return;
			}
			
			i++;
		}
	}
	
	public override Gee.ArrayList<Expander> get_expanders () {
		return expanders;
	}
	
	public static void update_spacing_classes () {	
		KerningRange kr;
		
		if (classes.tool.size == 0) {
			return;
		}

		foreach (Tool t in classes.tool) {
			return_if_fail (t is KerningRange);
			
			kr = (KerningRange) t;
			kr.update_spacing_class ();
		}
	}

	public override Gee.ArrayList<string> get_displays () {
		Gee.ArrayList<string> d = new Gee.ArrayList<string> ();
		d.add ("Kerning");
		d.add ("Spacing");
		return d;
	}
		
}

}

Contact

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