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

public enum MenuDirection {
	DROP_DOWN,
	POP_UP;
}

namespace BirdFont {

public class VersionList : GLib.Object {
	public int current_version_id = -1;
	GlyphCollection glyph_collection;
	
	public Gee.ArrayList<Glyph> glyphs;

	public delegate void Selected (MenuAction self);
	public signal void selected (VersionList self);
	
	double x = -1;
	double y = -1;
	double width = 0;
	
	double menu_x = -1;	
	public bool menu_visible { get; set; }
	Gee.ArrayList <MenuAction> actions = new Gee.ArrayList <MenuAction> ();
	const int item_height = 25;
	MenuDirection direction = MenuDirection.DROP_DOWN;

	// Glyphs gets added to and removed from the glyph collection in 
	// these signal.
	public signal void signal_delete_item  (int item_index);
	public signal void add_glyph_item  (Glyph item);

	public VersionList (GlyphCollection gc) {
		menu_visible = false;
		MenuAction ma = add_item (t_("New version"));
		ma.has_delete_button = false;
		ma.action.connect ((self) => {
			return_if_fail (glyphs.size > 0);
			
			BirdFont.get_current_font ().touch ();
			
			add_new_version ();
			current_version_id = glyphs.get (glyphs.size - 1).version_id;
		});
	
		// delete one version
		signal_delete_item.connect ((index) => {
			delete_item (index);
		});

		this.glyph_collection = gc;
		glyphs = new Gee.ArrayList<Glyph> ();
		set_direction (MenuDirection.POP_UP);
		
		glyphs = new Gee.ArrayList<Glyph> ();
		
		if (gc.has_masters ()) {
			foreach (Glyph g in gc.get_current_master ().glyphs) {
				add_glyph (g, false);
			}
		}
		
		if (gc.length () > 0) {
			set_selected_version (gc.get_current ().version_id, false);
		}
	}
	
	private void delete_item (int index) {
		int current_version;
		Font font = BirdFont.get_current_font ();
		Overview over_view = MainWindow.get_overview ();
		
		font.touch ();
		
		index--; // first item adds new glyphs to the list
		
		// delete the entire glyph if the last remaining version is removed
		if (glyphs.size == 1) {
			over_view.store_undo_state (glyph_collection.copy ());
			font.delete_glyph (glyph_collection);
			string name = glyph_collection.get_name ();
			MainWindow.get_tab_bar ().close_background_tab_by_name (name);
			return;
		}
		
		return_if_fail (0 <= index < glyphs.size);

		Glyph g = glyph_collection.get_current ();
		GlyphMaster m = glyph_collection.get_current_master ();
		font.add_deleted_glyph (g, m);
		
		over_view.store_undo_state (glyph_collection.copy ());
		
		current_version = get_current_version_index ();

		glyphs.remove_at (index);
		glyph_collection.remove (index);
		
		recreate_index ();
		
		if (index == current_version) {
			set_selected_item (get_action_no2 ()); // select the first glyph if the current glyph is deleted
		} else if (index < current_version) {
			return_if_fail (0 <= current_version - 1 < glyphs.size);
			current_version_id = glyphs.get (current_version - 1).version_id;
			int i = get_current_version_index ();
			set_selected_item (get_action_index (i));
		}
		
		select_last_glyph ();
	}

	private int get_current_version_index () {
		int i = 0;
		foreach (Glyph g in glyphs) {
			if (g.version_id == current_version_id) {
				return i;
			}
			i++;
		}
		
		warning ("No index for menu item.");
		return 0;
	}

	public void set_selected_version (int version_id, bool update_loaded_glyph) {
		current_version_id = version_id;
		update_selection (update_loaded_glyph);
	}
	
	public Glyph get_current () {
		Glyph? gl = null;
		
		foreach (Glyph g in glyphs) {
			if (g.version_id == current_version_id) {
				return g;
			}
		}
		
		if (unlikely (glyphs.size > 0)) {
			warning (@"Can not find current glyph for id $current_version_id");
			gl = glyphs.get (glyphs.size - 1);
			set_selected_version (((!) gl).version_id, false);
			return (!) gl;
		}
		
		if (unlikely (glyphs.size == 0 && current_version_id == -1)) {
			warning (@"No glyphs added to collection");
			gl = new Glyph.no_lines ("", '\0');
		}
		
		return (!) gl;
	}

	public void add_new_version () {
		Glyph g = get_current ();
		Glyph new_version = g.copy ();
		new_version.version_id = get_last_id () + 1;
		
		// send signal back to the collection
		add_glyph_item (new_version);
		
		// add the item to the menu
		add_glyph (new_version);
	}
	
	public int get_last_id () {
		return_val_if_fail (glyphs.size > 0, 1);
		return glyphs.get (glyphs.size - 1).version_id;
	}
	
	private void set_selected_item (MenuAction ma, bool update_loaded_glyph = true) {
		int i = ma.index;
		Glyph current_glyph;
		Glyph g;
				
		return_if_fail (0 <= i < glyphs.size);
		g = glyphs.get (i);
		
		current_version_id = g.version_id;
		deselect_all ();
		ma.set_selected (true);
		
		glyph_collection.set_selected (g);
		
		reload_all_open_glyphs ();

		if (update_loaded_glyph && !is_null (BirdFont.current_glyph_collection)) {
			current_glyph = MainWindow.get_current_glyph ();
			g.set_allocation (current_glyph.allocation);
			g.close_path ();
			g.reset_zoom ();
		}		
	}
	
	/** Reload a glyph when a new version is selected. Updates the path
	 * in glyph view, not from disk but from the glyph table.
	 */
	void reload_all_open_glyphs () {
		TabBar b;
		Tab tab;
		Tab? tn;
		Glyph glyph;
		Glyph updated_glyph;
		Glyph? ug;
		Font font = BirdFont.get_current_font ();
		StringBuilder uni = new StringBuilder ();
		
		if (is_null (MainWindow.get_tab_bar ())) {
			return;
		}
		
		b = MainWindow.get_tab_bar ();
		
		for (int i = 0; i < b.get_length (); i++) {
			tn = b.get_nth (i);
			
			if (tn == null) {
				warning ("tab is null");
				return;
			}

			tab = (!) tn;

			if (! (tab.get_display () is GlyphTab)) {
				continue; 
			}
			
			GlyphTab glyph_tab = (GlyphTab) tab.get_display ();
			glyph = glyph_tab.glyphs.get_current ();
			uni.truncate (0);
			uni.append_unichar (glyph.unichar_code);
			ug = font.get_glyph (uni.str);
			
			if (ug == null) {
				return;
			}

			updated_glyph = (!) ug;
			tab.set_display (glyph_tab);
			updated_glyph.view_zoom = glyph.view_zoom;
			updated_glyph.view_offset_x = glyph.view_offset_x;
			updated_glyph.view_offset_y = glyph.view_offset_y;
		}
	}
	
	public void add_glyph (Glyph new_version, bool selected = true) {
		MenuAction ma;
		int v;
		
		v = new_version.version_id;
		glyphs.add (new_version);

		ma = add_item (t_("Version") + @" $(v + 1)");
		ma.index = (int) glyphs.size - 1;
		
		ma.action.connect ((self) => {
			Font font = BirdFont.get_current_font ();
			set_selected_item (self);
			font.touch ();
		});

		if (selected) {
			set_selected_item (ma);
		}
		
		if (selected) {
			update_selection ();
		}
	}
	
	bool has_version (int id) {
		foreach (Glyph g in glyphs) {
			if (g.version_id == id) {
				return true;
			}
		}
		return false;
	}
	
	void update_selection (bool update_loaded_glyph = true) {
		int index;
		
		if (has_version (current_version_id)) {
			index = get_current_version_index ();
			set_selected_item (get_action_index (index + 1), update_loaded_glyph); // the first item is the "new version"
		}
	}

	public MenuAction get_action_index (int index) {
		if (!(0 <= index < actions.size)) {
			warning (@"No action for index $index. (actions.size: $(actions.size))");
			return new MenuAction ("None");
		}
		return actions.get (index);
	}
	
	public void recreate_index () {
		int i = -1;
		foreach (MenuAction a in actions) {
			a.index = i;
			i++;
		}
	}
	
	public MenuAction get_action_no2 () {
		if (actions.size < 2) {
			warning ("No such action");
			return new MenuAction ("None");
		}
		
		return actions.get (1);
	}
	
	public void deselect_all () {
		foreach (MenuAction m in actions) {
			m.set_selected (false);
		}
	}
	
	public void set_direction (MenuDirection d) {
		direction = d;
	}
	
	public void close () {
		menu_visible = false;
	}
	
	public MenuAction add_item (string label) {
		MenuAction m = new MenuAction (label);
		add_menu_item (m);
		return m;
	}
	
	public void add_menu_item (MenuAction m) {
		actions.add (m);
	}
		
	public bool is_over_icon (double px, double py) {
		if (x == -1 || y == -1) {
			return false;
		}
		
		return x - 12 < px <= x && y - 5 < py < y + 12 + 5;
	}

	public bool menu_item_action (double px, double py) {
		MenuAction? action;
		MenuAction a;
		MenuAction ma;
		int index;
		
		if (menu_visible) {
			action = get_menu_action_at (px, py);
			
			if (action != null) {
				a = (!) action;
				
				// action for the delete button
				if (a.has_delete_button && menu_x + width - 13 < px <= menu_x + width) { 
					index = 0;
					ma = actions.get (0);
					while (true) {
						if (a == ma) {
							actions.remove_at (index);
							signal_delete_item (index);
							break;
						}
						
						if (ma == actions.get (actions.size - 1)) {
							break;
						} else {
							ma = actions.get (index + 1);
							index++;
						}

					}
					return false;
				} else {
					a.action (a);
					selected (this);
					menu_visible = false;
				}
				
				return true;
			}
		}
		
		return false;
	}

	void select_last_glyph () {
		MenuAction? ma = null;
		
		foreach (MenuAction m in actions) {
			ma = m;
		}
		
		if (ma != null) {
			set_selected_item ((!) ma);
		}
	}
	
	public bool menu_icon_action (double px, double py) {		
		menu_visible = is_over_icon (px, py);
		return menu_visible;
	}
	
	MenuAction? get_menu_action_at (double px, double py) {
		double n = 0;
		double ix, iy;
		
		foreach (MenuAction item in actions) {
			ix = menu_x - 6;
			
			if (direction == MenuDirection.DROP_DOWN) {
				iy = y + 12 + n * item_height;
			} else {
				iy = y - 24 - n * item_height;
			}
	
			if (ix <= px <= ix + width && iy <= py <= iy + item_height) {
				return item;
			}
			
			n++;			
		}

		return null;
	}
	
	public void set_position (double px, double py) {
		x = px;
		y = py;

		foreach (MenuAction item in actions) {
			item.text = new Text (item.label);
			if (item.text.get_sidebearing_extent () + 25 > width) {
				width = item.text.get_sidebearing_extent () + 25;
			}
		}
				
		if (x - width < 5) {
			menu_x = 5;
		} else {
			menu_x = x - width;
		}
	}
	
	public void draw_menu (Context cr) {
		double ix, iy;
		int n;
	
		if (likely (!menu_visible)) {
			return;
		}
		
		cr.save ();
		Theme.color (cr, "Default Background");
		cr.rectangle (menu_x, y - actions.size * item_height, width, actions.size * item_height);
		
		cr.fill_preserve ();
		cr.stroke ();
		cr.restore ();
		
		cr.save ();
		
		n = 0;
		foreach (MenuAction item in actions) {
			item.width = width;
			
			iy = y - 8 - n * item_height;
			ix = menu_x + 2;
			
			item.draw (ix, iy, cr);
			n++;
		}
		
		cr.restore ();
	}
}

}

Contact

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