Clone in a terminal:
OverView.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;

namespace BirdFont {

/** A display with all glyphs present in this font. */
public class OverView : FontDisplay {
	public WidgetAllocation allocation = new WidgetAllocation ();
	
	public OverViewItem selected_item = new OverViewItem ();

	public Gee.ArrayList<GlyphCollection> copied_glyphs = new Gee.ArrayList<GlyphCollection> ();
	public Gee.ArrayList<GlyphCollection> selected_items = new Gee.ArrayList<GlyphCollection> ();	
	
	int selected = 0;
	int first_visible = 0;
	int rows = 0;
	int items_per_row = 0;
	
	double view_offset_y = 0;
	double view_offset_x = 0;
	
	public signal void open_new_glyph_signal (unichar c);
	public signal void open_glyph_signal (GlyphCollection c);
	
	public GlyphRange glyph_range {
		get {
			return _glyph_range;
		}
		
		set {
			_glyph_range = value;
		}
	}
	
	GlyphRange _glyph_range;
	
	string search_query = "";
	
	public Gee.ArrayList<OverViewItem> visible_items = new Gee.ArrayList<OverViewItem> ();
	
	/** List of undo commands. */
	public Gee.ArrayList<OverViewUndoItem> undo_items = new Gee.ArrayList<OverViewUndoItem> ();
	public Gee.ArrayList<OverViewUndoItem> redo_items = new Gee.ArrayList<OverViewUndoItem> ();
	
	/** Show all characters that has been drawn. */
	public bool all_available {
		set {
			_all_available = value;
			update_item_list ();
		}
		
		get {
			return _all_available;
		}
	}
	
	bool _all_available = false;
	
	/** Show unicode database info. */
	CharacterInfo? character_info = null;
	
	double scroll_size = 1;
	const double UCD_LINE_HEIGHT = 17 * 1.3;

	private bool update_scheduled = true;

	public OverView (GlyphRange? range = null,
		bool open_selected = true, bool default_character_set = true) {
			
		GlyphRange gr;
		
		if (range == null) {
			gr = new GlyphRange ();
			set_current_glyph_range (gr);
		}

		if (open_selected) {
			this.open_glyph_signal.connect ((glyph_collection) => {
				TabBar tabs = MainWindow.get_tab_bar ();
				string n = glyph_collection.get_current ().name;
				bool selected = tabs.select_char (n);
				GlyphTab glyph_tab;
				
				if (!selected) {
					glyph_tab = new GlyphTab (glyph_collection);
					tabs.add_tab (glyph_tab, true, glyph_collection);
					set_glyph_zoom (glyph_collection);
					PenTool.update_orientation ();
				}
			});

			this.open_new_glyph_signal.connect ((character) => {
				// ignore control characters
				if (character <= 0x1F || character == 0xFFFF) { 
					return;
				}
				
				create_new_glyph (character);
			});
		}

		if (default_character_set) {
			IdleSource idle = new IdleSource ();

			idle.set_callback (() => {
				use_default_character_set ();
				selected_canvas ();
				return false;
			});
			
			idle.attach (null);
		}
						
		update_item_list ();
		update_scrollbar ();
		reset_zoom ();
		
		string? zoom = Preferences.get ("overview_zoom");
		
		if (zoom != null) {
			string z = (!) zoom;
			
			if (z != "") {
				set_zoom (double.parse (z));
			}			
		}
	}

	public void reset_cache () {
		foreach (OverViewItem i in visible_items) {
			i.clear_cache ();
		}
	}

	public Glyph? get_selected_glyph () {
		if (selected_items.size == 0) {
			return null;
		}
		
		return selected_items.get (0).get_current ();
	}
	
	public void select_all_glyphs () {
		Font f;
		GlyphCollection? glyphs;
		
		f = BirdFont.get_current_font ();
		
		for (int index = 0; index < f.length (); index++) {
			glyphs = f.get_glyph_collection_index ((uint32) index);
			return_if_fail (glyphs != null);
			
			selected_items.add ((!) glyphs);
		}
		
		foreach (OverViewItem item in visible_items) {
			item.selected = item.glyphs != null;
		}
		
		GlyphCanvas.redraw ();
	}
	
	public void use_default_character_set () {
		GlyphRange gr = new GlyphRange ();
		all_available = false;
		DefaultCharacterSet.use_default_range (gr);
		set_current_glyph_range (gr);
		OverviewTools.update_overview_characterset ();
		FontDisplay.dirty_scrollbar = true;
	}
	
	public GlyphCollection create_new_glyph (unichar character) {
		StringBuilder name = new StringBuilder ();
		TabBar tabs = MainWindow.get_tab_bar ();
		bool selected;
		Glyph glyph;
		GlyphCollection glyph_collection;
		GlyphCanvas canvas;
		GlyphTab glyph_tab; 
		
		glyph_collection = MainWindow.get_current_glyph_collection ();
		name.append_unichar (character);
		selected = tabs.select_char (name.str);
				
		if (!selected) {
			glyph_collection = add_character_to_font (character);
			glyph_tab = new GlyphTab (glyph_collection);
			glyph = glyph_collection.get_current ();
			glyph.layers.add_layer (new Layer ());
			tabs.add_tab (glyph_tab, true, glyph_collection);
			
			selected_items.add (glyph_collection);
			
			canvas = MainWindow.get_glyph_canvas ();
			canvas.set_current_glyph_collection (glyph_collection);
			
			set_glyph_zoom (glyph_collection);
		} else {
			warning ("Glyph is already open");
		}
		
		OverviewTools.update_overview_characterset ();
		return glyph_collection;
	}
		
	public GlyphCollection add_empty_character_to_font (unichar character, bool unassigned, string name) {
		return add_character_to_font (character, true, unassigned);
	}
	
	public GlyphCollection add_character_to_font (unichar character, bool empty = false,
			bool unassigned = false, string glyph_name = "") {
		StringBuilder name = new StringBuilder ();
		Font font = BirdFont.get_current_font ();
		GlyphCollection? fg;
		Glyph glyph;
		GlyphCollection glyph_collection;

		if (glyph_name == "") {
			name.append_unichar (character);
		} else {
			name.append (glyph_name);
		}
		
		if (all_available) {
			fg = font.get_glyph_collection_by_name (name.str);
		} else {
			fg = font.get_glyph_collection (name.str);
		}

		if (fg != null) {
			glyph_collection = (!) fg;
		} else {
			glyph_collection = new GlyphCollection (character, name.str);
			
			if (!empty) {
				glyph = new Glyph (name.str, !unassigned ? character : '\0');
				glyph_collection.add_master (new GlyphMaster ());
				glyph_collection.insert_glyph (glyph, true);
			}
			
			font.add_glyph_collection (glyph_collection);
		}
		
		glyph_collection.set_unassigned (unassigned);
		
		return glyph_collection;
	}
	
	public static void search () {
		OverView ow = MainWindow.get_overview ();
		TextListener listener = new TextListener (t_("Search"), ow.search_query, t_("Filter"));
		
		listener.signal_text_input.connect ((text) => {
			OverView o = MainWindow.get_overview ();
			o.search_query = text;
		});
		
		listener.signal_submit.connect (() => {
			OverView o = MainWindow.get_overview ();
			string q = o.search_query;
			
			if (q == "*") {
 				q = "star";
 			}
						
			if (q == "?") {
				q = "question";
			}

			if (q.char_count () > 1) {
				q = q.down ();
			}
			
			GlyphRange r = CharDatabase.search (q);
			o.set_current_glyph_range (r);
			MainWindow.get_tab_bar ().select_tab_name ("Overview");
	
			TextListener tl = new TextListener (t_("Search"), o.search_query, t_("Filter"));
			TabContent.show_text_input (tl);
		});
		
		TabContent.show_text_input (listener);
	}
	
	public Glyph? get_current_glyph () {
		OverViewItem oi = selected_item;
		if (oi.glyphs != null) {
			return ((!) oi.glyphs).get_current ();
		}
		return null;
	}
	
	public void set_glyph_zoom (GlyphCollection glyphs) {
		GlyphCanvas canvas;
		canvas = MainWindow.get_glyph_canvas ();
		canvas.set_current_glyph_collection (glyphs);
		Toolbox tools = MainWindow.get_toolbox ();
		ZoomTool z = (ZoomTool) tools.get_tool ("zoom_tool");
		z.store_current_view ();
		glyphs.get_current ().default_zoom ();
		z.store_current_view ();
		OverViewItem.reset_label ();
	}
	
	public double get_height () {
		double l;
		Font f;
		
		if (rows == 0) {
			return 0;
		}
				
		if (all_available) {
			f = BirdFont.get_current_font ();
			l = f.length ();
		} else {
			l = glyph_range.length ();
		}
				
		return 2.0 * OverViewItem.height * (l / rows);
	}

	public bool selected_char_is_visible () {
		return first_visible <= selected <= first_visible + items_per_row * rows;
	}

	public override bool has_scrollbar () {
		return true;
	}

	public bool all_characters_in_view () {
		double length;
		Font f;
		
		if (all_available) {
			f = BirdFont.get_current_font ();
			length = f.length ();
		} else {
			length = glyph_range.length ();
		}
		
		return length < items_per_row * rows;
	}
	
	public void move_up () {
		first_visible -= items_per_row;
		selected += items_per_row;
		
		if (first_visible < 0) {
			selected -= items_per_row;
			first_visible = 0;
			view_offset_y = 0;
		}
	}

	public void move_down () {
		if (!at_bottom ()) {
			first_visible += items_per_row;
			selected -= items_per_row;
		}
	}
	
	public override void scroll_wheel (double x, double y, double dx, double dy) {
		double pixel_delta = 3 * dy;
		
		if (dy > 0) {
			view_offset_y += pixel_delta;
			
			while (view_offset_y > 0) {
				view_offset_y -= OverViewItem.height;
				move_up ();
			}						
		} else {
			if (at_bottom ()) {
				if (view_offset_y > -2 * OverViewItem.height && !all_characters_in_view ()) {
					view_offset_y += pixel_delta;
				}
			} else {
				view_offset_y += pixel_delta;
				while (view_offset_y < -OverViewItem.height) {
					view_offset_y += OverViewItem.height;
					move_down ();
				}
			}
		}
				
		if (view_offset_y < -2 * OverViewItem.height) {
			view_offset_y = -2 * OverViewItem.height;
		}
		
		update_item_list ();
		update_scrollbar ();
		hide_menu ();	
		GlyphCanvas.redraw ();
	}
	
	public override void selected_canvas () {
		OverviewTools.update_overview_characterset ();
		KeyBindings.set_require_modifier (true);
		update_scrollbar ();
		update_zoom_bar ();
		OverViewItem.glyph_scale = 1;
		update_item_list ();
		selected_item = get_selected_item ();
		GlyphCanvas.redraw ();
	}
	
	public void update_zoom_bar () {
		double z = OverViewItem.width / OverViewItem.DEFAULT_WIDTH - 0.5;
		Toolbox.overview_tools.zoom_bar.set_zoom (z);
		Toolbox.redraw_tool_box ();
		update_item_list ();
	}
	
	public void set_zoom (double zoom) {
		double z = zoom + 0.5;
		OverViewItem.glyph_scale = 1;
		OverViewItem.width = OverViewItem.DEFAULT_WIDTH * z;
		OverViewItem.height = OverViewItem.DEFAULT_HEIGHT * z;
		OverViewItem.margin = OverViewItem.DEFAULT_MARGIN * z;
		update_item_list ();
		OverViewItem.reset_label ();
		Preferences.set ("overview_zoom", @"$zoom");

		Font font = BirdFont.get_current_font ();
		for (int index = 0; index < font.length (); index++) {
			GlyphCollection? glyphs = font.get_glyph_collection_index ((uint32) index);
			return_if_fail (glyphs != null);
			GlyphCollection g = (!) glyphs;
			g.get_current ().overview_thumbnail = null;
		}
		
		GlyphCanvas.redraw ();
	}
	
	public override void zoom_min () {
		OverViewItem.width = OverViewItem.DEFAULT_WIDTH * 0.5;
		OverViewItem.height = OverViewItem.DEFAULT_HEIGHT * 0.5;
		OverViewItem.margin = OverViewItem.DEFAULT_MARGIN * 0.5;
		update_item_list ();
		OverViewItem.reset_label ();
		GlyphCanvas.redraw ();
		update_zoom_bar ();
	}
	
	public override void reset_zoom () {
		OverViewItem.width = OverViewItem.DEFAULT_WIDTH;
		OverViewItem.height = OverViewItem.DEFAULT_HEIGHT;
		OverViewItem.margin = OverViewItem.DEFAULT_MARGIN;
		update_item_list ();
		OverViewItem.reset_label ();
		GlyphCanvas.redraw ();
		update_zoom_bar ();
	}

	public override void zoom_max () {
		OverViewItem.width = allocation.width;
		OverViewItem.height = allocation.height;
		update_item_list ();
		OverViewItem.reset_label ();
		GlyphCanvas.redraw ();
	}
	
	public override void zoom_in () {
		OverViewItem.width *= 1.1;
		OverViewItem.height *= 1.1;
		OverViewItem.margin *= 1.1;
		update_item_list ();
		OverViewItem.reset_label ();
		GlyphCanvas.redraw ();
		update_zoom_bar ();
	}
	
	public override void zoom_out () {
		OverViewItem.width *= 0.9;
		OverViewItem.height *= 0.9;
		OverViewItem.margin *= 0.9;
		update_item_list ();
		OverViewItem.reset_label ();
		GlyphCanvas.redraw ();
		update_zoom_bar ();
	}

	public override void store_current_view () {
	}
	
	public override void restore_last_view () {
	}

	public override void next_view () {
	}

	public override string get_label () {
		return t_("Overview");
	}
	
	public override string get_name () {
		return "Overview";
	}
	
	public void display_all_available_glyphs () {
		all_available = true;

		view_offset_y = 0;
		first_visible = 0;
		selected = 0;
		
		update_item_list ();
		selected_item = get_selected_item ();
		GlyphCanvas.redraw ();
	}
	
	OverViewItem get_selected_item () {
		if (visible_items.size == 0) {
			return new OverViewItem ();
		}
		
		if (!(0 <= selected < visible_items.size)) { 
			return selected_item;
		}

		OverViewItem item = visible_items.get (selected);
		item.selected = true;
		return item;
	}
	
	int get_items_per_row () {
		int i = 1;
		double tab_with = allocation.width - 30; // 30 px for the scroll bar
		OverViewItem.margin = OverViewItem.width * 0.1;
		double l = OverViewItem.margin + OverViewItem.full_width ();
		
		while (l <= tab_with) {
			l += OverViewItem.full_width ();
			i++;
		}
		
		return i - 1;
	}
	
	public void update_item_list () {
		update_scheduled = true;
	}
	
	public void process_item_list_update () {
		string character_string;
		Font f = BirdFont.get_current_font ();
		GlyphCollection? glyphs = null;
		uint32 index;
		OverViewItem item;
		double x, y;
		unichar character;
		Glyph glyph;
		double tab_with;
		int item_list_length;
		int visible_size;
		
		tab_with = allocation.width - 30; // scrollbar
		
		items_per_row = get_items_per_row ();
		rows = (int) (allocation.height /  OverViewItem.full_height ()) + 2;
		
		item_list_length = items_per_row * rows;
		visible_items.clear ();
		
		index = (uint32) first_visible;
		x = OverViewItem.margin;
		y = OverViewItem.margin;
		
		if (all_available) {
			uint font_length = f.length ();
			
			for (int i = 0; i < item_list_length && index < font_length; i++) {
				glyphs = f.get_glyph_collection_index ((uint32) index);
				return_if_fail (glyphs != null);
				
				glyph = ((!) glyphs).get_current ();
				character_string = glyph.name;
				character = glyph.unichar_code;
				
				item = new OverViewItem ();
				item.set_character (character);
				item.set_glyphs (glyphs);
				item.x = x;
				item.y = y;
				visible_items.add (item);
				index++;
			}
		} else {
			uint32 glyph_range_size = glyph_range.get_length ();
			
			uint max_length = glyph_range.length () - first_visible;
			
			if (item_list_length > max_length) {
				item_list_length = (int) max_length;
			}
			
			for (int i = 0; i < item_list_length && index < glyph_range_size; i++) {			
				item = new OverViewItem ();
				visible_items.add (item);
			}
			
			index = (uint32) first_visible;
			visible_size = visible_items.size;
			for (int i = 0; i < visible_size; i++) {
				item = visible_items.get (i);
				character = glyph_range.get_character ((uint32) index);
				item.set_character (character);
				index++;
			}

			visible_size = visible_items.size;
			for (int i = 0; i < visible_size; i++) {
				item = visible_items.get (i);
				glyphs = f.get_glyph_collection_by_name ((!) item.character.to_string ());
				item.set_glyphs (glyphs);
			}
		}
		
		x = OverViewItem.margin;
		y = OverViewItem.margin;
		
		visible_size = visible_items.size;
		int selected_index;
		bool selected_item;
		double full_width = OverViewItem.full_width ();
		
		for (int i = 0; i < visible_size; i++) {
			item = visible_items.get (i);

			selected_item = false;
	
			if (all_available) {
				glyphs = f.get_glyph_collection_index ((uint32) i);
			} else {			
				glyphs = f.get_glyph_collection_by_name ((!) item.character.to_string ());
			}
			
			if (glyphs != null) {
				selected_index = selected_items.index_of ((!) glyphs);
				selected_item = (selected_index != -1);
			}
						
			selected_item |= (i == selected);
			item.selected = selected_item;
			
			item.x = x + view_offset_x;
			item.y = y + view_offset_y;
			
			x += full_width;
			
			if (x + full_width >= tab_with) {
				x = OverViewItem.margin;
				y += OverViewItem.full_height ();
			}
		}
		
		update_scheduled = false;
	}
	
	public override void draw (WidgetAllocation allocation, Context cr) {
		if (update_scheduled
			|| this.allocation.width != allocation.width
			|| this.allocation.height != allocation.height
			|| this.allocation.width == 0) {
			this.allocation = allocation;
			process_item_list_update ();
		}
		
		this.allocation = allocation;
		
		// clear canvas
		cr.save ();
		Theme.color (cr, "Background 1");
		cr.rectangle (0, 0, allocation.width, allocation.height);
		cr.fill ();
		cr.restore ();
		
		foreach (OverViewItem i in visible_items) {
			i.draw (allocation, cr);
		}
		
		if (unlikely (visible_items.size == 0)) {
			draw_empty_canvas (allocation, cr);
		}
		
		if (unlikely (character_info != null)) {
			draw_character_info (cr);
		}
	}
		
	void draw_empty_canvas (WidgetAllocation allocation, Context cr) {
		Text t;
		
		cr.save ();
		t = new Text (t_("No glyphs in this view."), 24);
		Theme.text_color (t, "Text Foreground");
		t.widget_x = 40;
		t.widget_y = 30;
		t.draw (cr);
		cr.restore ();
	}
	
	public void scroll_rows (int row_adjustment) {
		for (int i = 0; i < row_adjustment; i++) {
			scroll (-OverViewItem.height);
		}
		
		for (int i = 0; i > row_adjustment; i--) {
			scroll (OverViewItem.height);
		}
	}
	
	public void scroll_adjustment (double pixel_adjustment) {
		double l;
		Font f;
				
		if (all_available) {
			f = BirdFont.get_current_font ();
			l = f.length ();
		} else {
			l = glyph_range.length ();
		}
		
		if (first_visible <= 0) {
			return;
		}

		if (first_visible + rows * items_per_row >= l) {
			return;
		}
		
		scroll ((int64) pixel_adjustment);
	}
	
	void scroll_to_position (int64 r) {
		if (r < 0) {
			scroll_top ();
			return;
		}
		
		selected -= (int) (r - first_visible);
		first_visible = (int) r;
		update_item_list ();
	}
	
	public override void scroll_to (double position) requires (items_per_row > 0) {
		double r;
		int nrows;
		Font f;
		
		if (all_available) {
			f = BirdFont.get_current_font ();
			nrows = (int) (f.length () / items_per_row);
		} else {
			nrows = (int) (glyph_range.length () / items_per_row);
		}
		
		view_offset_y = 0;
		r = (int64) (position * (nrows - rows + 3)); // 3 invisible rows
		r *= items_per_row;
		
		scroll_to_position ((int64) r);
		update_item_list ();
		GlyphCanvas.redraw ();
	}
		
	private void scroll (double pixel_adjustment) {
		if (first_visible < 0 && pixel_adjustment < 0) {
			scroll_top ();
			return;
		}
				
		view_offset_y += pixel_adjustment;
		
		if (view_offset_y >= 0) {
			while (view_offset_y > OverViewItem.height) {			
				view_offset_y -= OverViewItem.height;
				first_visible -= items_per_row;
			}

			first_visible -= items_per_row;
			view_offset_y -= OverViewItem.height;
		} else if (view_offset_y < -OverViewItem.height) {
			view_offset_y = 0;
			first_visible += items_per_row;
		}
		
		update_item_list ();
	}
	
	public void scroll_top () {
		first_visible = 0;
		view_offset_y = 0;
		
		update_item_list ();
		
		if (visible_items.size != 0) {
			selected_item = get_selected_item ();
		}
	}

	/** Returns true if the selected glyph is at the last row. */
	private bool last_row () {
		return visible_items.size - selected <= items_per_row;
	}

	public void key_down () {
		Font f = BirdFont.get_current_font ();
		int64 len = (all_available) ? f.length () : glyph_range.length ();
		
		if (at_bottom () && last_row ()) {
			return;
		}
		
		selected += items_per_row;
		
		if (selected >= items_per_row * rows) {
			first_visible += items_per_row;
			selected -= items_per_row;
		}
		
		if (first_visible + selected >= len) {
			selected = (int) (len - first_visible - 1);
			
			if (selected < items_per_row * (rows - 1)) {
				first_visible -= items_per_row;
				selected += items_per_row;
			}
		}
		
		if (selected >= visible_items.size) { 
			selected = (int) (visible_items.size - 1); 
		}

		selected_item = get_selected_item ();
		update_item_list ();
	}

	public void key_right () {
		Font f = BirdFont.get_current_font ();
		int64 len = (all_available) ? f.length () : glyph_range.length ();

		if (at_bottom () && first_visible + selected + 1 >= len) {
			selected = (int) (visible_items.size - 1);
			selected_item = get_selected_item ();
			return;
		}
		
		selected += 1;
		
		if (selected >= items_per_row * rows) {
			first_visible += items_per_row;
			selected -= items_per_row;
			selected -= 1;
		}		

		if (first_visible + selected > len) {
			first_visible -= items_per_row;
			selected = (int) (len - first_visible - 1);
			selected_item = get_selected_item ();
		}
		update_item_list ();
	}
	
	public void key_up () {
		selected -= items_per_row;
		
		if (selected < 0) {
			first_visible -= items_per_row;
			selected += items_per_row;			
		}
		
		if (first_visible < 0) {
			first_visible = 0;		
		}
		update_item_list ();
	}
	
	public void key_left () {
		selected -= 1;

		if (selected < 0) {
			first_visible -= items_per_row;
			selected += items_per_row;
			selected += 1;			
		}

		if (first_visible < 0) {
			scroll_top ();
		}
		update_item_list ();
	}
	
	public string get_selected_char () {
		Font f;
		Glyph? g;
		
		if (all_available) {
			f = BirdFont.get_current_font ();
			g = f.get_glyph_index (selected);
			return_val_if_fail (g != null, "".dup ());
			return ((!) g).get_name ();
		}
		
		return glyph_range.get_char (selected);
	}
	
	public override void key_press (uint keyval) {
		hide_menu ();
		update_item_list ();
		GlyphCanvas.redraw ();

		if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {
			return;
		}

		switch (keyval) {
			case Key.ENTER:
				open_current_glyph ();
				return;
			
			case Key.UP:
				get_selected_item ().selected = false;
			
				key_up ();
				selected_item = get_selected_item ();
				
				selected_items.clear ();
				if (selected_item.glyphs != null) {
					selected_items.add ((!) selected_item.glyphs);
				}
				
				update_scrollbar ();
				return;
				
			case Key.RIGHT:
				get_selected_item ().selected = false;
			
				key_right ();
				selected_item = get_selected_item ();
				
				selected_items.clear ();
				if (selected_item.glyphs != null) {
					selected_items.add ((!) selected_item.glyphs);
				}
				
				update_scrollbar ();
				return;
				
			case Key.LEFT:
				get_selected_item ().selected = false;
				
				key_left ();
				selected_item = get_selected_item ();
				
				selected_items.clear ();
				if (selected_item.glyphs != null) {
					selected_items.add ((!) selected_item.glyphs);
				}
				
				update_scrollbar();
				return;
				
			case Key.DOWN:
				get_selected_item ().selected = false;
				
				key_down ();
				selected_item = get_selected_item ();
				
				selected_items.clear ();
				if (selected_item.glyphs != null) {
					selected_items.add ((!) selected_item.glyphs);
				}
				
				update_scrollbar ();
				return;
				
			case Key.PG_UP:
				get_selected_item ().selected = false;
				
				for (int i = 0; i < rows; i++) {
					key_up ();
				}
				selected_item = get_selected_item ();
				
				selected_items.clear ();
				if (selected_item.glyphs != null) {
					selected_items.add ((!) selected_item.glyphs);
				}
				
				update_scrollbar ();
				return;
				
			case Key.PG_DOWN:
				get_selected_item ().selected = false;
				
				for (int i = 0; i < rows; i++) {
					key_down ();
				}
				selected_item = get_selected_item ();

				selected_items.clear ();
				if (selected_item.glyphs != null) {
					selected_items.add ((!) selected_item.glyphs);
				}
				
				update_scrollbar ();
				return;
				
			case Key.DEL:
				delete_selected_glyph ();
				selected_item = get_selected_item ();
				return;
				
			case Key.BACK_SPACE:
				delete_selected_glyph ();
				selected_item = get_selected_item ();
				return;
		}

		if (!KeyBindings.has_ctrl () && !KeyBindings.has_logo ()) {
			scroll_to_char (keyval);
		}
		
		selected_item = get_selected_item ();

		selected_items.clear ();
		if (selected_item.glyphs != null) {
			selected_items.add ((!) selected_item.glyphs);
		}
		
		update_item_list ();
		GlyphCanvas.redraw ();
	}
	
	public void delete_selected_glyph () {
		Font font = BirdFont.get_current_font ();
		OverViewUndoItem undo_item = new OverViewUndoItem ();
		
		undo_item.alternate_sets = font.alternates.copy ();
		
		foreach (GlyphCollection g in selected_items) {
			undo_item.glyphs.add (g.copy ());
		}
		store_undo_items (undo_item);

		foreach (GlyphCollection glyph_collection in selected_items) {
			font.delete_glyph (glyph_collection);
			string name = glyph_collection.get_name ();
			MainWindow.get_tab_bar ().close_background_tab_by_name (name);
		}

		update_item_list ();
		GlyphCanvas.redraw ();
	}
	
	public override void undo () {
		Font font = BirdFont.get_current_font ();
		OverViewUndoItem previous_collection;
		
		if (undo_items.size == 0) {
			return;
		}
		
		previous_collection = undo_items.get (undo_items.size - 1);
		redo_items.add (get_current_state (previous_collection));
		
		// remove the old glyph and add the new one
		foreach (GlyphCollection g in previous_collection.glyphs) {
			font.delete_glyph (g);
			
			if (g.length () > 0) {
				font.add_glyph_collection (g);
			}

			TabBar tabs = MainWindow.get_tab_bar ();
			Tab? tab = tabs.get_tab (g.get_name ());
			
			if (tab != null) {
				Tab t = (!) tab;
				set_glyph_zoom (g);
				t.set_glyph_collection (g);
				t.set_display (new GlyphTab (g));
			}
		}
		
		Font f = BirdFont.get_current_font ();
		f.alternates = previous_collection.alternate_sets.copy ();

		undo_items.remove_at (undo_items.size - 1);
		update_item_list ();
		GlyphCanvas.redraw ();
	}
	
	public override void redo () {
		Font font = BirdFont.get_current_font ();
		OverViewUndoItem previous_collection;

		if (redo_items.size == 0) {
			return;
		}
		
		previous_collection = redo_items.get (redo_items.size - 1);
		undo_items.add (get_current_state (previous_collection));

		// remove the old glyph and add the new one
		foreach (GlyphCollection g in previous_collection.glyphs) {
			font.delete_glyph (g);
			font.add_glyph_collection (g);

			TabBar tabs = MainWindow.get_tab_bar ();
			Tab? tab = tabs.get_tab (g.get_name ());
			
			if (tab != null) {
				Tab t = (!) tab;
				set_glyph_zoom (g);
				t.set_glyph_collection (g);
				t.set_display (new GlyphTab (g));
			}
		}
		font.alternates = previous_collection.alternate_sets.copy ();

		redo_items.remove_at (redo_items.size - 1);
		GlyphCanvas.redraw ();
	}	
	
	public OverViewUndoItem get_current_state (OverViewUndoItem previous_collection) {
		GlyphCollection? gc;
		OverViewUndoItem ui = new OverViewUndoItem ();
		Font font = BirdFont.get_current_font ();
		
		ui.alternate_sets = font.alternates.copy ();
		
		foreach (GlyphCollection g in previous_collection.glyphs) {
			gc = font.get_glyph_collection (g.get_name ());
			
			if (gc != null) {
				ui.glyphs.add (((!) gc).copy ());
			} else {
				ui.glyphs.add (new GlyphCollection (g.get_unicode_character (), g.get_name ()));
			}
		}
		
		return ui;		
	}
	
	public void store_undo_state (GlyphCollection gc) {
		OverViewUndoItem i = new OverViewUndoItem ();
		Font f = BirdFont.get_current_font ();
		i.alternate_sets = f.alternates.copy ();
		i.glyphs.add (gc);
		store_undo_items (i);
	}

	public void store_undo_items (OverViewUndoItem i) {
		undo_items.add (i);
		redo_items.clear ();
	}
	
	bool select_visible_glyph (string name) {
		int i = 0;
		
		foreach (OverViewItem o in visible_items) {
			if (o.get_name () == name) {
				selected = i;
				selected_item = get_selected_item ();

				if (selected_item.y > allocation.height - OverViewItem.height) {
					view_offset_y -= (selected_item.y + OverViewItem.height) - allocation.height;
				}

				if (selected_item.y < 0) {
					view_offset_y = 0;
				}		

				return true;
			}
			
			if (i > 1000) {
				warning ("selected character not found");
				return true;
			}
			
			i++;
		}
		
		return false;
	}
	
	public void scroll_to_char (unichar c) {
		StringBuilder s = new StringBuilder ();

		if (is_modifier_key (c)) {
			return;
		}
		
		s.append_unichar (c);
		scroll_to_glyph (s.str);
	}
		
	public void scroll_to_glyph (string name) {
		GlyphRange gr = glyph_range;
		int i, r, index;
		string ch;
		Font font = BirdFont.get_current_font ();
		GlyphCollection? glyphs = null;
		Glyph glyph;
		
		index = -1;
		
		if (items_per_row <= 0) {
			return;
		}

		ch = name;

		// selected char is visible
		if (select_visible_glyph (ch)) {
			return;
		}
		
		// scroll to char
		if (all_available) {
			
			// don't search for glyphs in huge CJK fonts 
			if (font.length () > 500) {
				r = 0;
			} else {
				// FIXME: too slow
				for (r = 0; r < font.length (); r += items_per_row) {
					for (i = 0; i < items_per_row && i < font.length (); i++) {
						glyphs = font.get_glyph_collection_index ((uint32) r + i);
						return_if_fail (glyphs != null);
						glyph = ((!) glyphs).get_current ();
						
						if (glyph.name == ch) {
							index = i;
						}
					}
					
					if (index > -1) {
						break;
					}
				}
			}
		} else {
			
			if (ch.char_count () > 1) {
				warning ("Can't scroll to ligature in this view");
				return;
			}
			
			for (r = 0; r < gr.length (); r += items_per_row) {
				for (i = 0; i < items_per_row; i++) {
					if (gr.get_char (r + i) == ch) {
						index = i;
					}
				}
				
				if (index > -1) {
					break;
				}
			}
		}
		
		if (index > -1) {
			first_visible = r;
			process_item_list_update ();
			update_item_list ();
			select_visible_glyph (ch);
		}
	}
	
	public override void double_click (uint button, double ex, double ey) 
		requires (!is_null (visible_items) && !is_null (allocation)) {
		
		return_if_fail (!is_null (this));
		
		foreach (OverViewItem i in visible_items) {
			if (i.double_click (button, ex, ey)) {
				open_overview_item (i);
			}
		}
	
		GlyphCanvas.redraw ();
	}

	public void open_overview_item (OverViewItem i) {
		return_if_fail (!is_null (i));
		
		if (i.glyphs != null) {
			open_glyph_signal ((!) i.glyphs);
			GlyphCollection gc = (!) i.glyphs;
			gc.get_current ().close_path ();
		} else {
			open_new_glyph_signal (i.character);
		}
	}
	
	public void set_character_info (CharacterInfo i) {
		character_info = i;
	}

	public int get_selected_index () {
		GlyphCollection gc;
		int index = 0;
		
		if (selected_items.size == 0) {
			return 0;
		}
		
		gc = selected_items.get (0);
		
		foreach (OverViewItem i in visible_items) {
			if (i.glyphs != null && gc == ((!) i.glyphs)) {
				break;
			}
			
			index++;
		}
		
		return index;
	}

	public void hide_menu () {
		foreach (OverViewItem i in visible_items) {
			i.hide_menu ();
		}	
	}

	public override void button_press (uint button, double x, double y) {
		OverViewItem i;
		int index = 0;
		int selected_index = -1;
		bool update = false;
		
		if (character_info != null) {
			character_info = null;
			GlyphCanvas.redraw ();
			return;
		}
		
		if (!KeyBindings.has_shift ()) {
			selected_items.clear ();
		}
		
		for (int j = 0; j < visible_items.size; j++) {
			i = visible_items.get (j);
			if (i.click_menu (button, x, y)) {
				GlyphCanvas.redraw ();
				return;
			}
		}

		for (int j = 0; j < visible_items.size; j++) {
			i = visible_items.get (j);
			if (i.click_info (button, x, y)) {
				GlyphCanvas.redraw ();
				return;
			}
		}
				
		for (int j = 0; j < visible_items.size; j++) {
			i = visible_items.get (j);
			
			if (i.click (button, x, y)) {
				selected = index;
				selected_item = get_selected_item ();
				
				if (KeyBindings.has_shift ()) {
					if (selected_item.glyphs != null) {
						
						selected_index = selected_items.index_of ((!) selected_item.glyphs);
						if (selected_index == -1) {
							selected_items.add ((!) selected_item.glyphs);
						} else {
							return_if_fail (0 <= selected_index < selected_items.size);
							selected_items.remove_at (selected_index);
							selected = get_selected_index ();
							selected_item = get_selected_item ();
						}
					}
				} else {
					selected_items.clear ();
					if (selected_item.glyphs != null) {
						selected_items.add ((!) selected_item.glyphs);
					}
				}
				
				if (!is_null (i.version_menu)) {
					update = !((!)i).version_menu.menu_visible;
				} else {
					update = true;
				}
			}
			index++;
		}
	
		if (update) {
			update_item_list ();
		}
		
		GlyphCanvas.redraw ();
	}

	/** Returns true if overview shows the last character. */
	private bool at_bottom () {
		Font f;
		double t = rows * items_per_row + first_visible;

		if (all_available) {
			f = BirdFont.get_current_font ();
			return t >= f.length () + 2 * items_per_row;
		}
		
		return t >= glyph_range.length () + 2 * items_per_row;
	}
	
	public void set_current_glyph_range (GlyphRange range) {
		GlyphRange? current = glyph_range;
		string c;
		
		if (current != null) {
			c = glyph_range.get_char (selected);
		}
		
		all_available = false;
		
		glyph_range = range;
		scroll_top ();

		// TODO: scroll down to c
		update_item_list ();
		selected_item = get_selected_item ();

		GlyphCanvas.redraw ();
	}

	public void select_next_glyph () {
		key_right ();
	}
	
	public void open_current_glyph () {
		// keep this object even if open_glyph_signal closes the display
		this.ref ();
		 
		selected_item = get_selected_item ();
		if (selected_item.glyphs != null) {
			open_glyph_signal ((!) selected_item.glyphs);
			GlyphCollection gc = (!) selected_item.glyphs;
			gc.get_current ().close_path ();
		} else {
			open_new_glyph_signal (selected_item.character);
		}
		
		this.unref ();
	}

	public double update_scrollbar () {
		Font f;
		double nrows = 0;
		double pos = 0;
		double size;
		double visible_rows;
		
		if (rows == 0) {
			MainWindow.set_scrollbar_size (0);
			MainWindow.set_scrollbar_position (0);
		} else {
			if (all_available) {
				f = BirdFont.get_current_font ();
				nrows = Math.ceil ((f.length ()) / (double) rows);
				size = f.length ();
			} else {
				nrows = Math.ceil ((glyph_range.length ()) / (double) rows);
				size = glyph_range.length ();
			}
			
			if (nrows <= 0) {
				nrows = 1;
			}

			visible_rows = allocation.height / (OverViewItem.height + OverViewItem.margin);
			scroll_size = visible_rows / nrows;
			MainWindow.set_scrollbar_size (scroll_size);
			pos = first_visible / (size - visible_rows * items_per_row);
			
			if (pos > 1) {
				pos = 1;
			}
								
			MainWindow.set_scrollbar_position (pos);
			
			if (at_bottom () && first_visible == 0) {
				MainWindow.hide_scrollbar ();
			} else {
				MainWindow.show_scrollbar ();
			}
		}
		
		return pos;
	}

	/** Display one entry from the Unicode Character Database. */
	void draw_character_info (Context cr) 
	requires (character_info != null) {
		double x, y, w, h;
		int i;
		string unicode_value, unicode_description;
		string[] column;
		string entry;
		int len = 0;
		int length = 0;
		bool see_also = false;
		WidgetAllocation allocation = MainWindow.get_overview ().allocation;
		string name;
		string[] lines;
		double character_start;
		double character_height;
		
		entry = ((!)character_info).get_entry ();

		lines = entry.split ("\n");
		
		foreach (string line in entry.split ("\n")) {
			len = line.char_count ();
			if (len > length) {
				length = len;
			}
		}
		
		x = allocation.width * 0.1;
		y = allocation.height * 0.1;
		w = allocation.width * 0.9 - x; 
		h = allocation.height * 0.9 - y;
		
		if (w < 8 * length) {
			w = 8 * length;
			x = (allocation.width - w) / 2.0;
		}
		
		if (x < 0) {
			x = 2;
		}
		
		// background	
		cr.save ();
		Theme.color_opacity (cr, "Background 1", 0.98);
		cr.rectangle (x, y, w, h);
		cr.fill ();
		cr.restore ();

		cr.save ();
		Theme.color_opacity (cr, "Foreground 1", 0.98);
		cr.set_line_width (2);
		cr.rectangle (x, y, w, h);
		cr.stroke ();
		cr.restore ();

		// database entry
		
		if (((!)character_info).is_ligature ()) {
			name = ((!)character_info).get_name ();
			draw_info_line (name, cr, x, y, 0);
		} else {
			i = 0;
			foreach (string line in lines) {
				if (i == 0) {
					column = line.split ("\t");
					return_if_fail (column.length == 2);
					unicode_value = "U+" + column[0];
					unicode_description = column[1];

					draw_info_line (unicode_description, cr, x, y, i);
					i++;

					draw_info_line (unicode_value, cr, x, y, i);
					i++;			
				} else {
					
					if (line.has_prefix ("\t*")) {
						draw_info_line (line.replace ("\t*", "•"), cr, x, y, i);
						i++;					
					} else if (line.has_prefix ("\tx (")) {
						if (!see_also) {
							i++;
							draw_info_line (t_("See also:"), cr, x, y, i);
							i++;
							see_also = true;
						}
						
						draw_info_line (line.replace ("\tx (", "•").replace (")", ""), cr, x, y, i);
						i++;
					} else {
						i++;
					}
				}
			}
			
			character_start = y + 10 + i * UCD_LINE_HEIGHT;
			character_height = h - character_start;

			draw_fallback_character (cr, x, character_start, character_height);
		}
	}
	
	/** Fallback character in UCD info. */
	void draw_fallback_character (Context cr, double x, double y, double height)
	requires (character_info != null) {
		unichar c = ((!)character_info).unicode;

		if (height < 0) {
			return;
		}

		cr.save ();
		Text character = new Text ();
		character.set_use_cache (false);
		Theme.text_color (character, "Foreground 1");
		character.set_text ((!) c.to_string ());
		character.set_font_size (height);
		character.draw_at_top (cr, x + 10, y);
		cr.restore ();
	}

	void draw_info_line (string line, Context cr, double x, double y, int row) {
		Text ucd_entry = new Text (line);
		cr.save ();
		Theme.text_color (ucd_entry, "Foreground 1");
		ucd_entry.widget_x = 10 + x;
		ucd_entry.widget_y = 10 + y + row * UCD_LINE_HEIGHT;
		ucd_entry.draw (cr);
		cr.restore ();		
	}
	
	public void paste () {
		GlyphCollection gc;
		GlyphCollection? c;
		Glyph glyph;
		uint32 index;
		int i;
		int skip = 0;
		int s;
		string character_string;
		Gee.ArrayList<GlyphCollection> glyps;
		Font f;
		OverViewUndoItem undo_item;
		
		f = BirdFont.get_current_font ();
		gc = new GlyphCollection ('\0', "");
		glyps = new Gee.ArrayList<GlyphCollection> ();
		
		copied_glyphs.sort ((a, b) => {
			return (int) ((GlyphCollection) a).get_unicode_character () 
				- (int) ((GlyphCollection) b).get_unicode_character ();
		});

		index = (uint32) first_visible + selected;
		for (i = 0; i < copied_glyphs.size; i++) {
			if (all_available) {
				if (f.length () == 0) {
					c = add_empty_character_to_font (copied_glyphs.get (i).get_unicode_character (),
						copied_glyphs.get (i).is_unassigned (), 
						copied_glyphs.get (i).get_name ());
				} else if (index >= f.length ()) {
					// FIXME: duplicated unicodes?
					c = add_empty_character_to_font (copied_glyphs.get (i).get_unicode_character (),
						copied_glyphs.get (i).is_unassigned (), 
						copied_glyphs.get (i).get_name ());
				} else {
					c = f.get_glyph_collection_index ((uint32) index);
				}
				
				if (c == null) {
					c = add_empty_character_to_font (copied_glyphs.get (i).get_unicode_character (),
						copied_glyphs.get (i).is_unassigned (),
						copied_glyphs.get (i).get_name ());
				}
				
				return_if_fail (c != null);
				gc = (!) c; 
			} else {			
				if (i != 0) {
					s = (int) copied_glyphs.get (i).get_unicode_character ();
					s -= (int) copied_glyphs.get (i - 1).get_unicode_character ();
					s -= 1;
					skip += s;
				}

				character_string = glyph_range.get_char ((uint32) (index + skip));
				c = f.get_glyph_collection_by_name (character_string);
				
				if (c == null) {
					gc = add_empty_character_to_font (character_string.get_char (), 
						copied_glyphs.get (i).is_unassigned (),
						copied_glyphs.get (i).get_name ());
				} else {
					gc = (!) c;
				}
			}
			
			glyps.add (gc);
			index++;
		}

		undo_item = new OverViewUndoItem ();
		undo_item.alternate_sets = f.alternates.copy ();
		foreach (GlyphCollection g in glyps) {
			undo_item.glyphs.add (g.copy ());
		}
		store_undo_items (undo_item);

		if (glyps.size != copied_glyphs.size) {
			warning ("glyps.size != copied_glyphs.size");
			return;
		}

		if (copied_glyphs.size < i) {
			warning ("Array index out of bounds.");
			return;
		}
		
		i = 0;
		foreach (GlyphCollection g in glyps) {
			glyph = copied_glyphs.get (i).get_current ().copy ();
			glyph.version_id = (glyph.version_id == -1 || g.length () == 0) ? 1 : g.get_last_id () + 1;
			glyph.unichar_code = g.get_unicode_character ();

			if (!g.is_unassigned ()) {
				glyph.name = (!) glyph.unichar_code.to_string ();
			} else {
				glyph.name = g.get_name ();
			}
			
			g.insert_glyph (glyph, true);
			i++;
		}
		
		f.touch ();
		
		update_item_list ();
		GlyphCanvas.redraw ();
	}

	public override bool needs_modifier () {
		return true;
	}
	
	public class OverViewUndoItem {
		public AlternateSets alternate_sets = new AlternateSets ();
		public Gee.ArrayList<GlyphCollection> glyphs = new Gee.ArrayList<GlyphCollection> ();
	}
}

}

Contact

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