[ease] Replaced the GtkIconView based slide sorter
- From: Nate Stedman <natesm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [ease] Replaced the GtkIconView based slide sorter
- Date: Sat, 27 Nov 2010 10:31:55 +0000 (UTC)
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]