[ease] Replaced the GtkIconView based slide sorter



commit 2855cea4c1201653965bc17b8f748918cce33c85
Author: Nate Stedman <natesm gmail com>
Date:   Fri Nov 26 17:58:31 2010 -0500

    Replaced the GtkIconView based slide sorter
    
    * EaseIconView, a (semi) reimplementation in Clutter
    * Still broken, doesn't scroll, doesn't drag, etc.

 ease-core/Makefile.am         |    1 +
 ease-core/ease-icon-view.vala |  557 +++++++++++++++++++++++++++++++++++++++++
 ease/ease-editor-window.vala  |    9 +-
 ease/ease-slide-sorter.vala   |   56 +++--
 4 files changed, 595 insertions(+), 28 deletions(-)
---
diff --git a/ease-core/Makefile.am b/ease-core/Makefile.am
index 3405d67..69a6ffa 100644
--- a/ease-core/Makefile.am
+++ b/ease-core/Makefile.am
@@ -17,6 +17,7 @@ libease_core_ EASE_CORE_VERSION@_la_SOURCES = \
 	ease-enums.vala \
 	ease-gradient.vala \
 	ease-html-exporter.vala \
+	ease-icon-view.vala \
 	ease-image-actor.vala \
 	ease-image-element.vala \
 	ease-image.vala \
diff --git a/ease-core/ease-icon-view.vala b/ease-core/ease-icon-view.vala
new file mode 100644
index 0000000..3d2515a
--- /dev/null
+++ b/ease-core/ease-icon-view.vala
@@ -0,0 +1,557 @@
+/*  Ease, a GTK presentation application
+    Copyright (C) 2010 Nate Stedman
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/**
+ * A reimplentation, with Clutter, of a large part of the GtkIconView interface.
+ */
+public class Ease.IconView : Clutter.Box
+{
+	private const uint ICON_FADE_TIME = 350;
+	private const float ICON_FADE_SCALE = 0f;
+	private const int ICON_FADE_MODE = Clutter.AnimationMode.EASE_OUT_BACK;
+
+	/**
+	 * The amount of spacing, in pixels, between columns.
+	 */
+	public float column_spacing
+	{
+		get { return layout.column_spacing; }
+		set { layout.column_spacing = value; }
+	}
+	
+	public int columns { get; set; default = -1; }
+	
+	/**
+	 * The amount of padding around each item.
+	 */
+	public int item_padding { get; set; default = 6; }
+	
+	/**
+	 * The width of each item.
+	 */
+	public float item_width
+	{
+		get { return layout.min_column_width; }
+		set
+		{
+			layout.max_column_width = layout.min_column_width = value;
+			@foreach((actor) => (actor as Icon).contents_width = value);
+		}
+	}
+	
+	/**
+	 * The amount of space at the edges of the icon view.
+	 */
+	public int margin { get; set; default = 6; }
+	
+	/**
+	 * The column which contains markup. If this is set to anything except -1,
+	 * the icon view will use markup and will ignore { link text_column}.
+	 */
+	public int markup_column { get; set; default = -1; }
+	
+	/**
+	 * The TreeModel that this icon view is representing. Changes to the model
+	 * will be (relatively) immediately reflected in the icon view.
+	 */
+	public Gtk.TreeModel model
+	{
+		get { return _model; }
+		set
+		{
+			// don't do extra work please
+			if (_model == value) return;
+			
+			// disconnect old handlers
+			_model.row_changed.disconnect(on_model_row_changed);
+			_model.row_deleted.disconnect(on_model_row_deleted);
+			_model.row_inserted.disconnect(on_model_row_inserted);
+			_model.rows_reordered.disconnect(on_model_rows_reordered);
+			
+			// remove old actors
+			@foreach((actor) => {
+				remove_actor(actor);
+			});
+			
+			_model = value;
+			
+			// add new actors
+			_model.foreach((model, path, iter) => {
+				pack(create_icon(iter), null);
+				return false;
+			});
+			show_all();
+			
+			// add new handlers
+			_model.row_changed.connect(on_model_row_changed);
+			_model.row_deleted.connect(on_model_row_deleted);
+			_model.row_inserted.connect(on_model_row_inserted);
+			_model.rows_reordered.connect(on_model_rows_reordered);
+		}
+	}
+	private Gtk.TreeModel _model;
+	
+	/**
+	 * The column containing pixbufs to display.
+	 */
+	public int pixbuf_column { get; set; default = -1; }
+	
+	/**
+	 * Whether or not the icon view can be rearranged by dragging.
+	 */
+	public bool reorderable { get; set; default = false; }
+	
+	/**
+	 * The amount of spacing between rows of the icon view.
+	 */
+	public float row_spacing
+	{
+		get { return layout.row_spacing; }
+		set { layout.row_spacing = value; }
+	}
+	
+	/**
+	 * The selection mode of the icon view. Defaults to single selection.
+	 */
+	public Gtk.SelectionMode selection_mode
+		{ get; set; default = Gtk.SelectionMode.SINGLE; }
+	
+	/**
+	 * The amount of space that is placed between the icon and text of an item.
+	 */
+	public int spacing { get; set; default = 0; }
+	
+	/**
+	 * The column which contains text. If { link markup_column} is not set to
+	 * -1, this property is ignored.
+	 */
+	public int text_column { get; set; default = -1; }
+	
+	/**
+	 * The color of the text labels.
+	 */
+	public Clutter.Color text_color { get; set; }
+	
+	/**
+	 * The column that contains tooltips to be displayed.
+	 */
+	public int tooltip_column { get; set; default = -1; }
+	
+	/**
+	 * The layout manager which actually lays out the icon view and does
+	 * the hard work.
+	 */
+	private Clutter.FlowLayout layout;
+	
+	/**
+	 * The current selection "origin" (where shift-select originates from).
+	 */
+	private Gtk.TreeRowReference select_origin = null;
+	
+	public signal void item_activated(IconView iconview, Gtk.TreePath path);
+	
+	public signal void selection_changed(IconView iconview);
+
+	public IconView.with_model(Gtk.TreeModel model)
+	{
+		this.model = model;
+	}
+	
+	construct
+	{
+		// automagic layout makes this whole thing work
+		layout = new Clutter.FlowLayout(Clutter.FlowOrientation.HORIZONTAL);
+		layout_manager = layout;
+		
+		// defaults
+		layout.homogeneous = true;
+		row_spacing = 6;
+		column_spacing = 6;
+		text_color = { 0, 0, 0, 255 };
+		
+		// handle column changes
+		notify["text-column"].connect(() => {
+			if (markup_column != -1) return;
+			@foreach((actor) => {
+				(actor as Icon).update_text(text_column, false);
+			});
+		});
+		
+		notify["markup-column"].connect(() => {
+			if (markup_column == -1) return;
+			@foreach((actor) => {
+				(actor as Icon).update_text(markup_column, true);
+			});
+		});
+		
+		notify["pixbuf-column"].connect(() => {
+			@foreach((actor) => {
+				(actor as Icon).update_pixbuf(pixbuf_column);
+			});
+		});
+	}
+	
+	public void selected_foreach(ForeachFunc callback)
+	{
+		@foreach((actor) => {
+			if ((actor as Icon).selected)
+			{
+				callback(this, (actor as Icon).reference.get_path());
+			}
+		});
+	}
+	
+	public delegate void ForeachFunc(IconView view, Gtk.TreePath path); 
+	
+	private void on_model_row_changed(Gtk.TreeModel model,
+	                                  Gtk.TreePath path,
+	                                  Gtk.TreeIter iter)
+	{
+		@foreach((actor) => {
+			if (path.compare((actor as Icon).reference.get_path()) == 0)
+			{
+				(actor as Icon).update_pixbuf(pixbuf_column);
+				(actor as Icon).update_text(markup_column != -1 ?
+				                            markup_column :
+				                            text_column,
+				                            markup_column == -1);
+			}
+		});
+	}
+	
+	private void on_model_row_deleted(Gtk.TreeModel model, Gtk.TreePath path)
+	{
+		bool removed = false;
+		@foreach((actor) => {
+			if (removed) return;
+			if ((actor as Icon).reference.get_path().compare(path) == 0)
+			{
+				// disconnect signals
+				(actor as Icon).select.connect(on_icon_select);
+				(actor as Icon).activate.connect(on_icon_activate);
+				
+				// deselect the actor if it is selected, independently of others
+				on_icon_select(actor as Icon,
+				               Clutter.ModifierType.CONTROL_MASK);
+				
+				// fade out
+				(actor as Icon).fadeout();
+				
+				// don't remove any more
+				removed = true;
+			}
+		});
+	}
+	
+	private void on_model_row_inserted(Gtk.TreeModel model,
+	                                   Gtk.TreePath path,
+	                                   Gtk.TreeIter iter)
+	{
+		// how many icons should go before this one?
+		int count = 0;
+		@foreach((actor) => {
+			if ((actor as Icon).reference.get_path().compare(path) == -1)
+			{
+				count++;
+			}
+		});
+		
+		// create and add the icon
+		var icon = create_icon(iter);
+		icon.contents_width = item_width;
+		pack_at(icon, count, null);
+		
+		// fade the icon in
+		icon.scale_x = icon.scale_y = ICON_FADE_SCALE;
+		icon.scale_gravity = Clutter.Gravity.CENTER;
+		icon.animate(ICON_FADE_MODE, ICON_FADE_TIME,
+		             "scale-x", 1.0, "scale-y", 1.0, null);
+	}
+	
+	private void on_model_rows_reordered(Gtk.TreeModel model,
+	                                     Gtk.TreePath path,
+	                                     Gtk.TreeIter iter,
+	                                     void* new_order)
+	{
+		// vapi problem again
+		int[] order = (int[])new_order;
+	}
+	
+	// icon signal handlers
+	private void on_icon_activate(Icon icon)
+	{
+		item_activated(this, icon.reference.get_path());
+	}
+	
+	private void on_icon_select(Icon icon, Clutter.ModifierType modifiers)
+	{
+		if ((modifiers & Clutter.ModifierType.CONTROL_MASK) != 0)
+		{
+			// count the number of currently selected items
+			int count = 0;
+			selected_foreach(() => count++);
+			
+			// flip this item
+			icon.selected = !icon.selected;
+			
+			// if this item was the first to be selected
+			if (icon.selected && count == 0)
+			{
+				select_origin = icon.reference;
+			}
+			
+			// if this item was the origin and was deselected
+			else if (!icon.selected && select_origin == icon.reference)
+			{
+				// set the origin to null
+				select_origin = null;
+				
+				// see if there's another selected icon to take its place
+				@foreach((actor) => {
+					if (select_origin == null && (actor as Icon).selected)
+					{
+						select_origin = (actor as Icon).reference;
+					}
+				});
+			}
+		}
+		else if ((modifiers & Clutter.ModifierType.SHIFT_MASK) != 0)
+		{
+			// deselect everything if used on the current origin
+			if (select_origin == icon.reference)
+			{
+				// deselect all others and count how many were
+				int count = 0;
+				@foreach((actor) => {
+					if (actor == icon) return;
+					if ((actor as Icon).selected)
+					{
+						(actor as Icon).selected = false;
+						count++;
+					}
+				});
+				
+				if (count == 0)
+				{
+					// deselect the current icon
+					icon.selected = false;
+					select_origin = null;
+				}
+			}
+			
+			// if there's no current origin, ignore the shift
+			if (select_origin == null)
+			{
+				icon.selected = true;
+				select_origin = icon.reference;
+			}
+			
+			// select relative to the origin
+			else
+			{
+				var orig_path = select_origin.get_path();
+				var icon_path = icon.reference.get_path();
+				switch (orig_path.compare(icon_path))
+				{
+					// origin is before the clicked icon
+					case -1:
+						@foreach((actor) => {
+							var path = (actor as Icon).reference.get_path();
+							(actor as Icon).selected =
+								orig_path.compare(path) != 1 &&
+							    icon_path.compare(path) != -1;
+						});
+						break;
+					
+					// origin is after the clicked icon
+					case 1:
+						@foreach((actor) => {
+							var path = (actor as Icon).reference.get_path();
+							(actor as Icon).selected =
+								orig_path.compare(path) != -1 &&
+							    icon_path.compare(path) != 1;
+						});
+						break;
+				}
+			}
+		}
+		else
+		{
+			// deselect all others and count how many were selected
+			int count = 0;
+			@foreach((actor) => {
+				if (actor == icon) return;
+				if ((actor as Icon).selected)
+				{
+					(actor as Icon).selected = false;
+					count++;
+				}
+			});
+			
+			if (count > 0)
+			{
+				// keep the current icon selected or select it
+				icon.selected = true;
+				select_origin = icon.reference;
+			}
+			else
+			{
+				// select/deselect the current icon
+				icon.selected = !icon.selected;
+				select_origin = icon.selected ? icon.reference : null;
+			}
+		}
+	}
+	
+	private Icon create_icon(Gtk.TreeIter iter)
+	{
+		// get data from model
+		string text;
+		Gdk.Pixbuf pixbuf;
+		model.get(iter, text_column, out text, pixbuf_column, out pixbuf, -1);
+		
+		// create an icon
+		var p = new Gtk.TreePath.from_string(model.get_string_from_iter(iter));
+		var icon = new Icon(new Gtk.TreeRowReference(model, p),
+		                    markup_column != -1 ? markup_column : text_column,
+		                    markup_column == -1, pixbuf_column);
+		
+		// connect signals
+		icon.select.connect(on_icon_select);
+		icon.activate.connect(on_icon_activate);
+		
+		return icon;
+	}
+	
+	private class Icon : Clutter.Group
+	{
+		public Clutter.Texture texture;
+		public Clutter.Text text;
+		public Gtk.TreeRowReference reference;
+		public bool selected
+		{
+			get { return _selected; }
+			set
+			{
+				_selected = value;
+				texture.opacity = selected ? 100 : 255;
+			}
+		}
+		private bool _selected;
+		
+		public float contents_width
+		{
+			get { return _contents_width; }
+			set
+			{
+				texture.width = value;
+				text.width = value;
+				text.y = texture.height + 6;
+				_contents_width = value;
+			}
+		}
+		private float _contents_width;
+		
+		public signal void activate(Icon icon);
+		public signal void select(Icon icon, Clutter.ModifierType modifiers);
+		
+		public Icon(Gtk.TreeRowReference reference, int text_col,
+		            bool use_markup, int pixbuf_col)
+		{
+			this.reference = reference;
+			
+			// create the text actor
+			text = new Clutter.Text.with_text("Sans 8", " ");
+			text.line_alignment = Pango.Alignment.CENTER;
+			text.single_line_mode = false;
+			add_actor(text);
+			
+			// initial "update" of text and pixbuf
+			update_text(text_col, use_markup);
+			update_pixbuf(pixbuf_col);
+			
+			// select or activate when clicked
+			reactive = true;
+			texture.reactive = true;
+			button_press_event.connect((event) => {
+				if (event.click_count < 2)
+				{
+					select(this, event.modifier_state);
+				}
+				else
+				{
+					activate(this);
+				}
+				return false;
+			});
+		}
+		
+		public void update_text(int column, bool use_markup)
+		{
+			// are we using markup?
+			text.use_markup = use_markup;
+			
+			// get an iterator
+			var str = reference.get_path().to_string();
+			Gtk.TreeIter iter;
+			if (reference.get_model().get_iter_from_string(out iter, str))
+			{
+				// get and set the text
+				str = text.text;
+				reference.get_model().get(iter, column, out str);
+				text.text = str;
+			}
+		}
+		
+		public void update_pixbuf(int column)
+		{
+			// remove the current texture
+			if (texture != null)
+			{
+				remove_actor(texture);
+			}
+			
+			// get an iterator
+			var str = reference.get_path().to_string();
+			Gtk.TreeIter iter;
+			if (reference.get_model().get_iter_from_string(out iter, str))
+			{
+				// get and set the pixbuf
+				Gdk.Pixbuf pb;
+				reference.get_model().get(iter, column, out pb);
+				
+				if (pb == null) return;
+				texture =
+					GtkClutter.texture_new_from_pixbuf(pb) as Clutter.Texture;
+				
+				// if all was successful, add the texture	
+				if (texture != null)
+				{
+					texture.keep_aspect_ratio = true;
+					texture.width = contents_width;
+					text.y = texture.height + 6;
+					add_actor(texture);
+				}
+			}
+		}
+		
+		public void fadeout()
+		{
+			(get_parent() as Clutter.Container).remove_actor(this);
+		}
+	}
+}
diff --git a/ease/ease-editor-window.vala b/ease/ease-editor-window.vala
index b31ce2f..fa35b49 100644
--- a/ease/ease-editor-window.vala
+++ b/ease/ease-editor-window.vala
@@ -373,8 +373,8 @@ internal class Ease.EditorWindow : Gtk.Window
 		                                    slide.width,
 		                                    slide.height);
 		
-		var index = document.index_of(slide) + 1;
-		
+		var index = sorter == null ?
+		                      document.index_of(slide) + 1 : document.length;
 		document.add_slide(index, s);
 	}
 	
@@ -655,8 +655,6 @@ internal class Ease.EditorWindow : Gtk.Window
 			// make the zoom slider work for the editor embed
 			zoom_slider.values = ZOOM_LEVELS;
 			zoom_slider.adjustment = zoom_adjustment;
-			zoom_slider.update_policy = Gtk.UpdateType.CONTINUOUS;
-			zoom_slider.animate = true;
 			
 			// wipe the document's dynamic pixbuf column
 			Slide s;
@@ -684,8 +682,7 @@ internal class Ease.EditorWindow : Gtk.Window
 			// make the zoom slider work for the sorter
 			zoom_slider.values = SORTER_ZOOM_LEVELS;
 			zoom_slider.adjustment = sorter_zoom_adjustment;
-			zoom_slider.update_policy = Gtk.UpdateType.DELAYED;
-			zoom_slider.animate = false;
+			sorter.set_zoom(zoom_slider.get_value() / 100f);
 			
 			// when a slide is clicked in the sorter, switch back here
 			sorter.display_slide.connect((s) => {
diff --git a/ease/ease-slide-sorter.vala b/ease/ease-slide-sorter.vala
index 3b49f40..3d3f407 100644
--- a/ease/ease-slide-sorter.vala
+++ b/ease/ease-slide-sorter.vala
@@ -20,11 +20,14 @@
  */
 internal class Ease.SlideSorter : Gtk.ScrolledWindow
 {
-	private Gtk.IconView view;
+	private Ease.IconView view;
+	private GtkClutter.Embed embed;
 	private Document document;
 	
 	private const int WIDTH = 100;
 	private const int WIDTH_ADDITIONAL = 300;
+	private const int LARGE_WIDTH = WIDTH + WIDTH_ADDITIONAL;
+	private const int PADDING = 10;
 	private int width;
 	
 	internal signal void display_slide(Slide s);
@@ -35,26 +38,47 @@ internal class Ease.SlideSorter : Gtk.ScrolledWindow
 		document.slide_added.connect(on_slide_added);
 		
 		// render dynamic-sized pixbufs
+		Slide slide;
+		foreach (var itr in document.slides)
+		{
+			// get the slide
+			document.slides.get(itr, Document.COL_SLIDE, out slide);
+			
+			// render a pixbuf at the appropriate size
+			document.slides.set(itr, Document.COL_PIXBUF_DYNAMIC,
+			                    SlideButtonPanel.pixbuf(slide, LARGE_WIDTH));
+		}
+		
 		set_zoom(zoom);
 		
 		// set up the icon view
-		view = new Gtk.IconView.with_model(document.slides);
+		view = new Ease.IconView();
+		view.x = view.y = PADDING;
 		view.pixbuf_column = Document.COL_PIXBUF_DYNAMIC;
-		view.markup_column = Document.COL_TITLE;
+		view.text_column = Document.COL_TITLE;
+		view.model = document.slides;
 		view.reorderable = true;
 		view.item_width = WIDTH;
 		
 		// add and show the iconview
-		add(view);
-		view.show();
+		embed = new GtkClutter.Embed();
+		(embed.get_stage() as Clutter.Stage).add_actor(view);
+		embed.show();
+		add(embed);
+		
+		// maintain the icon view's size when the stage is resized
+		embed.get_stage().allocation_changed.connect(() => {
+			view.width = embed.get_stage().width - 2 * PADDING;
+			view.height = embed.get_stage().height - 2 * PADDING;
+		});
 		
 		// when a slide is clicked, show it in the editor
 		view.item_activated.connect((v, path) => {
 			Gtk.TreeIter itr;
-			Slide slide;
+			Slide s;
 			view.model.get_iter(out itr, path);
-			view.model.get(itr, Document.COL_SLIDE, out slide);
-			display_slide(slide);
+			view.model.get(itr, Document.COL_SLIDE, out s);
+			display_slide(s);
 		});
 	}
 	
@@ -69,7 +93,6 @@ internal class Ease.SlideSorter : Gtk.ScrolledWindow
 			view.model.get(itr, Document.COL_SLIDE, out slide);
 			slides_to_remove.append(slide);
 		});
-		
 		slides_to_remove.foreach(() => {
 			if (document.length < 2) return;
 			ret_slide = document.remove_slide(slide);
@@ -80,24 +103,13 @@ internal class Ease.SlideSorter : Gtk.ScrolledWindow
 	
 	internal void set_zoom(double zoom)
 	{
-		width = (int)(WIDTH + zoom * WIDTH_ADDITIONAL);
-		
-		Slide slide;
-		foreach (var itr in document.slides)
-		{
-			// get the slide
-			document.slides.get(itr, Document.COL_SLIDE, out slide);
-			
-			// render a pixbuf at the appropriate size
-			document.slides.set(itr, Document.COL_PIXBUF_DYNAMIC,
-			                    SlideButtonPanel.pixbuf(slide, width));
-		}
+	 	view.item_width = (float)(WIDTH + zoom * WIDTH_ADDITIONAL);
 	}
 	
 	internal void on_slide_added(Slide slide, int index)
 	{
 		var itr = document.slides.index(index);
 		document.slides.set(itr, Document.COL_PIXBUF_DYNAMIC,
-		                    SlideButtonPanel.pixbuf(slide, width));
+		                    SlideButtonPanel.pixbuf(slide, LARGE_WIDTH));
 	}
 }



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]