Tidy up some function and variables
[gtkpod:phantomjinx-gtkpod-plugin.git] / plugins / clarity / clarity_canvas.c
1 /*
2  |  Copyright (C) 2002-2011 Jorg Schuler <jcsjcs at users sourceforge net>
3  |                                             Paul Richardson <phantom_sf at users.sourceforge.net>
4  |  Part of the gtkpod project.
5  |
6  |  URL: http://www.gtkpod.org/
7  |  URL: http://gtkpod.sourceforge.net/
8  |
9  |  This program is free software; you can redistribute it and/or modify
10  |  it under the terms of the GNU General Public License as published by
11  |  the Free Software Foundation; either version 2 of the License, or
12  |  (at your option) any later version.
13  |
14  |  This program is distributed in the hope that it will be useful,
15  |  but WITHOUT ANY WARRANTY; without even the implied warranty of
16  |  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  |  GNU General Public License for more details.
18  |
19  |  You should have received a copy of the GNU General Public License
20  |  along with this program; if not, write to the Free Software
21  |  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22  |
23  |  iTunes and iPod are trademarks of Apple
24  |
25  |  This product is not supported/written/published by Apple!
26  |
27  */
28 #include <clutter-gtk/clutter-gtk.h>
29 #include "libgtkpod/gp_itdb.h"
30 #include "libgtkpod/fileselection.h"
31 #include "libgtkpod/misc.h"
32 #include "plugin.h"
33 #include "clarity_cover.h"
34 #include "clarity_canvas.h"
35 #include "clarity_preview.h"
36 #include "clarity_utils.h"
37 #include "clarity_context_menu.h"
38
39 G_DEFINE_TYPE( ClarityCanvas, clarity_canvas, GTK_TYPE_BOX);
40
41 #define CLARITY_CANVAS_GET_PRIVATE(obj) \
42   (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CLARITY_TYPE_CANVAS, ClarityCanvasPrivate))
43
44 #define MAX_ANGLE                       70
45 #define COVER_SPACE                    50
46 #define FRONT_COVER_SPACE     150
47 #define MAX_SCALE                          1.4
48 #define VISIBLE_ITEMS                     8
49 #define FLOOR                              110
50
51 struct _ClarityCanvasPrivate {
52
53     AlbumModel *model;
54
55     // clutter embed widget
56     GtkWidget *embed;
57
58     // clutter items
59     GList *covers;
60     ClutterActor *container;
61     ClutterTimeline *timeline;
62     ClutterAlpha *alpha;
63     ClutterActor *title_text;
64     ClutterActor *artist_text;
65
66     gint curr_index;
67
68     gulong preview_signal;
69
70     gboolean blocked;
71 };
72
73 enum DIRECTION {
74     MOVE_LEFT = -1,
75     MOVE_RIGHT = 1
76 };
77
78 static void clarity_canvas_finalize(GObject *gobject) {
79     ClarityCanvasPrivate *priv = CLARITY_CANVAS(gobject)->priv;
80
81     //FIXME
82 //    g_list_free_full(priv->covers, clarity_cover_destroy);
83
84     if (G_IS_OBJECT(priv->alpha))
85         g_object_unref(priv->alpha);
86
87     if (G_IS_OBJECT(priv->timeline))
88         g_object_unref(priv->timeline);
89
90     if (GTK_IS_WIDGET(priv->embed))
91         gtk_widget_destroy(priv->embed);
92
93     /* call the parent class' finalize() method */
94     G_OBJECT_CLASS(clarity_canvas_parent_class)->finalize(gobject);
95 }
96
97 static void clarity_canvas_class_init(ClarityCanvasClass *klass) {
98     GObjectClass *gobject_class;
99
100     gobject_class = G_OBJECT_CLASS (klass);
101     gobject_class->finalize = clarity_canvas_finalize;
102
103     g_type_class_add_private(klass, sizeof(ClarityCanvasPrivate));
104 }
105
106 static void _update_text(ClarityCanvasPrivate *priv) {
107     g_return_if_fail(priv);
108
109     if (g_list_length(priv->covers) == 0)
110             return;
111
112     ClarityCover *ccover = g_list_nth_data(priv->covers, priv->curr_index);
113
114     gchar *title = clarity_cover_get_title(ccover);
115     gchar *artist = clarity_cover_get_artist(ccover);
116
117     clutter_text_set_text(CLUTTER_TEXT(priv->title_text), title);
118     clutter_text_set_text(CLUTTER_TEXT(priv->artist_text), artist);
119
120     g_free(title);
121     g_free(artist);
122
123     clutter_actor_raise_top(priv->title_text);
124     clutter_actor_raise_top(priv->artist_text);
125
126     gfloat artistx = (clutter_actor_get_width(priv->artist_text) / 2) * -1;
127     gfloat artisty = FLOOR - (clarity_cover_get_artwork_height(ccover) * MAX_SCALE);
128     clutter_actor_set_position(priv->artist_text, artistx, artisty);
129
130     gfloat titlex = (clutter_actor_get_width(priv->title_text) / 2) * -1;
131     gfloat titley = artisty - clutter_actor_get_height(priv->artist_text) - 2;
132     clutter_actor_set_position(priv->title_text, titlex, titley);
133 }
134
135 void clarity_canvas_block_change(ClarityCanvas *self, gboolean value) {
136     g_return_if_fail(self);
137
138     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
139     priv->blocked = value;
140
141     if (!value) {
142         _update_text(priv);
143     }
144 }
145
146 gboolean clarity_canvas_is_blocked(ClarityCanvas *self) {
147     g_return_val_if_fail(self, TRUE);
148     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
149     return priv->blocked;
150 }
151
152 static void _preview_cover(ClarityCanvasPrivate *priv) {
153     if (!priv->model)
154         return;
155
156     AlbumItem *item = album_model_get_item_with_index(priv->model, priv->curr_index);
157
158     GtkWidget *dialog = clarity_preview_new(item);
159
160     /* Display the dialog */
161     gtk_widget_show_all(dialog);
162 }
163
164 /**
165  * on_main_cover_image_clicked_cb:
166  *
167  * Call handler used for displaying the tracks associated with
168  * the main displayed album cover.
169  *
170  * @ClarityCanvas
171  * @event: event object used to determine the event type
172  * @data: any data needed by the function (not required)
173  *
174  */
175 static gint _on_main_cover_image_clicked_cb(GtkWidget *widget, GdkEvent *event, gpointer data) {
176     ClarityCanvas *self = CLARITY_CANVAS(widget);
177     ClarityCanvasPrivate *priv = self->priv;
178     guint mbutton;
179
180     if (event->type != GDK_BUTTON_PRESS)
181             return FALSE;
182
183     mbutton = event->button.button;
184
185     if ((mbutton == 1) && (event->button.state & GDK_SHIFT_MASK)) {
186         clarity_canvas_block_change(self, TRUE);
187
188         AlbumItem *item = album_model_get_item_with_index(priv->model, priv->curr_index);
189         if (item) {
190             gtkpod_set_displayed_tracks(item->tracks);
191         }
192
193         clarity_canvas_block_change(self, FALSE);
194     }
195     else if (mbutton == 1) {
196         _preview_cover(priv);
197     }
198     else if ((mbutton == 3) && (event->button.state & GDK_SHIFT_MASK)) {
199         /* Right mouse button clicked and shift pressed.
200          * Go straight to edit details window
201          */
202         AlbumItem *item = album_model_get_item_with_index(priv->model, priv->curr_index);
203         GList *tracks = item->tracks;
204         gtkpod_edit_details(tracks);
205     }
206     else if (mbutton == 3) {
207         /* Right mouse button clicked on its own so display
208          * popup menu
209          */
210         clarity_context_menu_init(self);
211     }
212
213     return FALSE;
214 }
215
216 /**
217  * embed_widget_size_allocated_cb
218  *
219  * Ensures that when the embed gtk widget is resized or moved
220  * around the clutter animations are centred correctly.
221  *
222  * This finds the new dimensions of the stage each time and centres
223  * the group container accordingly.
224  *
225  */
226 void _embed_widget_size_allocated_cb(GtkWidget *widget,
227                       GtkAllocation *allocation,
228                       gpointer data) {
229     ClarityCanvasPrivate *priv = (ClarityCanvasPrivate *) data;
230     ClutterActor *stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(widget));
231
232     gint centreX = clutter_actor_get_width(stage) / 2;
233     gint centreY = clutter_actor_get_height(stage) / 2;
234     clutter_actor_set_position(priv->container, centreX, centreY);
235 }
236
237 static void clarity_canvas_init(ClarityCanvas *self) {
238     ClarityCanvasPrivate *priv;
239
240     self->priv = CLARITY_CANVAS_GET_PRIVATE (self);
241
242     priv = self->priv;
243
244     priv->title_text = clutter_text_new();
245     clutter_text_set_font_name(CLUTTER_TEXT(priv->title_text), "Sans");
246
247     priv->artist_text = clutter_text_new();
248     clutter_text_set_font_name(CLUTTER_TEXT(priv->title_text), "Sans");
249
250     priv->container = clutter_group_new();
251     clutter_actor_set_reactive(priv->container, TRUE);
252     priv->preview_signal = g_signal_connect (self,
253                                 "button-press-event",
254                                 G_CALLBACK (_on_main_cover_image_clicked_cb),
255                                 priv);
256     clutter_container_add(CLUTTER_CONTAINER(priv->container), priv->title_text, priv->artist_text, NULL);
257
258     priv->embed = gtk_clutter_embed_new();
259     /*
260      * Minimum size before the scrollbars of the parent window
261      * are displayed.
262      */
263     gtk_widget_set_size_request(GTK_WIDGET(priv->embed), DEFAULT_IMG_SIZE * 4, DEFAULT_IMG_SIZE * 2.5);
264     /*
265      * Ensure that things are always centred when the embed
266      * widget is resized.
267      */
268     g_signal_connect(priv->embed, "size-allocate",
269                   G_CALLBACK(_embed_widget_size_allocated_cb), priv);
270
271     ClutterActor *stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(priv->embed));
272     clutter_container_add_actor(CLUTTER_CONTAINER(stage), priv->container);
273
274     gtk_widget_show(priv->embed);
275
276     gtk_box_pack_start(GTK_BOX(self), priv->embed, TRUE, TRUE, 0);
277
278     priv->covers = NULL;
279     priv->timeline = clutter_timeline_new(1600);
280     priv->alpha = clutter_alpha_new_full(priv->timeline, CLUTTER_EASE_OUT_EXPO);
281     priv->curr_index = 0;
282     priv->blocked = FALSE;
283
284 }
285
286 GtkWidget *clarity_canvas_new() {
287     return g_object_new(CLARITY_TYPE_CANVAS, NULL);
288 }
289
290 /**
291  * clarity_canvas_get_background_display_color:
292  *
293  * Returns the background color of the clarity canvas.
294  *
295  * The return value is a GdkRGBA
296  *
297  */
298 GdkRGBA *clarity_canvas_get_background_color(ClarityCanvas *self) {
299     g_return_val_if_fail(CLARITY_IS_CANVAS(self), NULL);
300
301     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
302
303     ClutterActor *stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(priv->embed));
304
305     ClutterColor *ccolor;
306     ccolor = g_malloc(sizeof(ClutterColor));
307
308     clutter_stage_get_color(CLUTTER_STAGE(stage), ccolor);
309     g_return_val_if_fail(ccolor, NULL);
310
311     GdkRGBA *rgba;
312     rgba = g_malloc(sizeof(GdkRGBA));
313     rgba->red = ((gdouble) ccolor->red) / 255;
314     rgba->green = ((gdouble) ccolor->green) / 255;
315     rgba->blue = ((gdouble) ccolor->blue) / 255;
316     rgba->alpha = ((gdouble) ccolor->alpha) / 255;
317
318     return rgba;
319 }
320
321 /**
322  * clarity_canvas_get_text_color:
323  *
324  * Returns the text color of the clarity text.
325  *
326  * The return value is a GdkRGBA
327  *
328  */
329 GdkRGBA *clarity_canvas_get_text_color(ClarityCanvas *self) {
330     g_return_val_if_fail(CLARITY_IS_CANVAS(self), NULL);
331
332     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
333
334     ClutterColor *ccolor;
335     ccolor = g_malloc(sizeof(ClutterColor));
336
337     clutter_text_get_color(CLUTTER_TEXT(priv->title_text), ccolor);
338     g_return_val_if_fail(ccolor, NULL);
339
340     GdkRGBA *rgba;
341     rgba = g_malloc(sizeof(GdkRGBA));
342     rgba->red = ((gdouble) ccolor->red) / 255;
343     rgba->green = ((gdouble) ccolor->green) / 255;
344     rgba->blue = ((gdouble) ccolor->blue) / 255;
345     rgba->alpha = ((gdouble) ccolor->alpha) / 255;
346
347     return rgba;
348 }
349
350 void clarity_canvas_set_background_color(ClarityCanvas *self, const gchar *color_string) {
351     g_return_if_fail(self);
352     g_return_if_fail(color_string);
353
354     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
355
356     ClutterActor *stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(priv->embed));
357
358     ClutterColor *ccolor;
359     ccolor = g_malloc(sizeof(ClutterColor));
360
361     clutter_color_from_string(ccolor, color_string);
362     clutter_stage_set_color(CLUTTER_STAGE(stage), ccolor);
363 }
364
365 void clarity_canvas_set_text_color(ClarityCanvas *self, const gchar *color_string) {
366     g_return_if_fail(self);
367     g_return_if_fail(color_string);
368
369     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
370
371     ClutterColor *ccolor;
372     ccolor = g_malloc(sizeof(ClutterColor));
373
374     clutter_color_from_string(ccolor, color_string);
375
376     clutter_text_set_color(CLUTTER_TEXT(priv->title_text), ccolor);
377     clutter_text_set_color(CLUTTER_TEXT(priv->artist_text), ccolor);
378 }
379
380 void clarity_canvas_clear(ClarityCanvas *self) {
381     g_return_if_fail(self);
382     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
383
384     if (CLUTTER_IS_ACTOR(priv->container)) {
385         GList *iter = priv->covers;
386         while(iter) {
387             ClarityCover *ccover = iter->data;
388             // cover is not referenced anywhere else so it should be destroyed too
389             clutter_container_remove(CLUTTER_CONTAINER(priv->container), CLUTTER_ACTOR(ccover), NULL);
390             iter = iter->next;
391         }
392
393         if (CLUTTER_IS_ACTOR(priv->artist_text))
394             clutter_text_set_text(CLUTTER_TEXT(priv->artist_text), "");
395
396         if (CLUTTER_IS_ACTOR(priv->title_text))
397             clutter_text_set_text(CLUTTER_TEXT(priv->title_text), "");
398     }
399
400     priv->covers = NULL;
401     priv->model = NULL;
402     priv->curr_index = 0;
403 }
404
405 static void _calculate_index_angle_and_dir (gint dist_from_front, enum DIRECTION dir, gint *angle, ClutterRotateDirection *rotation_dir) {
406     /* The front item direction depends on the direction we came from */
407     if (dist_from_front == 0) {
408         *rotation_dir =  (dir == MOVE_RIGHT ? CLUTTER_ROTATE_CCW : CLUTTER_ROTATE_CW);
409         *angle = 0;
410     }
411
412     /* Item on the right */
413     else if (dist_from_front > 0) {
414         *rotation_dir = CLUTTER_ROTATE_CCW;
415         *angle = 360 - MAX_ANGLE;
416     }
417
418     /* Item on the left */
419     else if (dist_from_front < 0) {
420         *rotation_dir = CLUTTER_ROTATE_CW;
421         *angle = MAX_ANGLE;
422     }
423 }
424
425 static gint _calculate_index_distance (gint dist_from_front) {
426     gint dist = ((ABS(dist_from_front) - 1) * COVER_SPACE) + FRONT_COVER_SPACE;
427
428     if (dist_from_front == 0)
429         return 0;
430
431     return (dist_from_front > 0 ? dist : 0 - dist);
432 }
433
434 static float _calculate_index_scale(gint dist_from_front) {
435     if (dist_from_front == 0)
436         return MAX_SCALE;
437     else
438         return 1;
439 }
440
441 static gint _calculate_index_opacity (gint dist_from_front) {
442     return CLAMP ( 255 * (VISIBLE_ITEMS - ABS(dist_from_front)) / VISIBLE_ITEMS, 0, 255);
443 }
444
445 static void _display_clarity_cover(ClarityCover *ccover, gint index) {
446     ClutterTimeline  *timeline = clutter_timeline_new(1600 * 5);
447     ClutterAlpha *alpha = clutter_alpha_new_full (timeline, CLUTTER_EASE_OUT_EXPO);
448
449     gint opacity = _calculate_index_opacity(index);
450     clutter_actor_animate_with_alpha(CLUTTER_ACTOR(ccover), alpha, "opacity", opacity, NULL);
451     clutter_timeline_start (timeline);
452 }
453
454 static void _set_cover_position(ClarityCover *ccover, gint index) {
455     gint pos = _calculate_index_distance(index);
456     clutter_actor_set_position(
457                     CLUTTER_ACTOR(ccover),
458                     pos - clarity_cover_get_artwork_width(ccover) / 2,
459                     FLOOR - clarity_cover_get_artwork_height(ccover));
460 }
461
462 static gboolean _create_cover_actors(ClarityCanvasPrivate *priv, AlbumItem *album_item, gint index) {
463     g_return_val_if_fail(priv, FALSE);
464
465     ClarityCover *ccover = clarity_cover_new();
466     clutter_actor_set_opacity(CLUTTER_ACTOR(ccover), 0);
467     priv->covers = g_list_insert(priv->covers, ccover, index);
468
469     clutter_container_add_actor(
470                             CLUTTER_CONTAINER(priv->container),
471                             CLUTTER_ACTOR(ccover));
472
473     clarity_cover_set_album_item(ccover, album_item);
474
475     _set_cover_position(ccover, index);
476
477     if((priv->curr_index + VISIBLE_ITEMS < index) ||
478             (priv->curr_index - VISIBLE_ITEMS > index)) {
479         return FALSE;
480     }
481
482     float scale = _calculate_index_scale(index);
483
484     gint angle;
485     ClutterRotateDirection rotation_dir;
486     _calculate_index_angle_and_dir(index, MOVE_LEFT, &angle, &rotation_dir);
487
488     clutter_actor_set_rotation(
489             CLUTTER_ACTOR(ccover),
490             CLUTTER_Y_AXIS,
491             angle,
492             clutter_actor_get_width(CLUTTER_ACTOR(ccover)) / 2,
493             0, 0);
494
495     clutter_actor_set_scale_full(
496             CLUTTER_ACTOR(ccover),
497             scale,
498             scale,
499             clarity_cover_get_artwork_width(ccover) / 2,
500             clarity_cover_get_artwork_height(ccover) / 2);
501
502     clutter_actor_lower_bottom(CLUTTER_ACTOR(ccover));
503
504     _display_clarity_cover(ccover, index);
505
506     return FALSE;
507 }
508
509 void _init_album_item(gpointer value, gint index, gpointer user_data) {
510     AlbumItem *item = (AlbumItem *) value;
511     ClarityCanvas *self = CLARITY_CANVAS(user_data);
512     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
513
514     album_model_init_coverart(priv->model, item);
515
516     clarity_canvas_block_change(self, TRUE);
517     _create_cover_actors(priv, item, index);
518     clarity_canvas_block_change(self, FALSE);
519 }
520
521 void _destroy_cover(ClarityCanvas *cc, gint index) {
522     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(cc);
523
524     ClarityCover *ccover = (ClarityCover *) g_list_nth_data(priv->covers, index);
525     if (!ccover)
526         return;
527
528     priv->covers = g_list_remove(priv->covers, ccover);
529
530     clutter_container_remove_actor(
531                                CLUTTER_CONTAINER(priv->container),
532                                CLUTTER_ACTOR(ccover));
533
534     clarity_cover_destroy(CLUTTER_ACTOR(ccover));
535
536     return;
537 }
538
539 static gpointer _init_album_model(gpointer data) {
540     g_return_val_if_fail(CLARITY_IS_CANVAS(data), NULL);
541
542     ClarityCanvas *cc = CLARITY_CANVAS(data);
543     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(cc);
544     AlbumModel *model = priv->model;
545
546     album_model_foreach(model, _init_album_item, cc);
547
548     return NULL;
549 }
550
551 void clarity_canvas_init_album_model(ClarityCanvas *self, AlbumModel *model) {
552     g_return_if_fail(self);
553     g_return_if_fail(model);
554
555     if (album_model_get_size(model) == 0)
556         return;
557
558     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
559     priv->model = model;
560
561     _init_album_model(self);
562
563 }
564
565 static void _clear_rotation_behaviours(GList *covers) {
566     //Clear rotation behaviours
567     GList *iter = covers;
568     while (iter) {
569         ClarityCover *ccover = iter->data;
570         clarity_cover_clear_rotation_behaviour(ccover);
571         iter = iter->next;
572     }
573 }
574
575 static void _animate_indices(ClarityCanvasPrivate *priv, gint direction, gint increment) {
576
577     for (gint i = 0; i < g_list_length(priv->covers); ++i) {
578         ClarityCover *ccover = g_list_nth_data(priv->covers, i);
579
580         gint dist = i - priv->curr_index + (direction * increment);
581         gfloat scale = 1;
582         gint pos = 0;
583         gint opacity = 0;
584         gint angle = 0;
585         ClutterRotateDirection rotation_dir;
586
587         opacity = _calculate_index_opacity(dist);
588         scale = _calculate_index_scale(dist);
589         pos = _calculate_index_distance(dist);
590         _calculate_index_angle_and_dir(dist, direction, &angle, &rotation_dir);
591
592         /*Rotation*/
593         clarity_cover_set_rotation_behaviour(ccover, priv->alpha, angle, rotation_dir);
594
595         /* Opacity */
596         clutter_actor_animate_with_alpha (CLUTTER_ACTOR(ccover), priv->alpha,
597                         "opacity", opacity,
598                         NULL);
599
600         gfloat w = clarity_cover_get_artwork_width(ccover);
601         gfloat h = clarity_cover_get_artwork_height(ccover);
602
603         /* Position and scale */
604         clutter_actor_animate_with_alpha (CLUTTER_ACTOR(ccover), priv->alpha,
605                         "scale-x",          scale,
606                         "scale-y",          scale,
607                         "scale-center-x" ,  w / 2,
608                         "scale-center-y", h / 2,
609                         "x", pos - (w / 2),
610                         "y", FLOOR - h,
611                         NULL);
612      }
613 }
614
615 static void _restore_z_order(ClarityCanvasPrivate *priv) {
616     g_return_if_fail(priv);
617
618     if (g_list_length(priv->covers) == 0)
619         return;
620
621     GList *main_cover = g_list_nth(priv->covers, priv->curr_index);
622     g_return_if_fail(main_cover);
623
624     GList *iter = main_cover ->prev;
625     while(iter) {
626         ClarityCover *ccover = iter->data;
627         clutter_actor_lower_bottom(CLUTTER_ACTOR(ccover));
628         iter = iter->prev;
629     }
630
631     iter = main_cover->next;
632     while(iter) {
633         ClarityCover *ccover = iter->data;
634         clutter_actor_lower_bottom(CLUTTER_ACTOR(ccover));
635         iter = iter->next;
636     }
637 }
638
639 static void _move(ClarityCanvasPrivate *priv, enum DIRECTION direction, gint increment) {
640
641     /* Stop any animation */
642     clutter_timeline_stop(priv->timeline);
643
644     /* Clear all current rotation behaviours */
645     _clear_rotation_behaviours(priv->covers);
646
647     /* Animate to move left */
648     _animate_indices (priv, direction, increment);
649
650     /* Begin the animation */
651     clutter_timeline_start(priv->timeline);
652
653     priv->curr_index += ((direction * -1) * increment);
654
655     _restore_z_order(priv);
656 }
657
658 void clarity_canvas_move_left(ClarityCanvas *self, gint increment) {
659     g_return_if_fail(self);
660     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
661
662     if(priv->curr_index == g_list_length(priv->covers) - 1)
663         return;
664
665     clarity_canvas_block_change(self, TRUE);
666     _move(priv, MOVE_LEFT, increment);
667     clarity_canvas_block_change(self, FALSE);
668 }
669
670 void clarity_canvas_move_right(ClarityCanvas *self, gint increment) {
671     g_return_if_fail(self);
672     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
673
674     if(priv->curr_index == 0)
675         return;
676
677     clarity_canvas_block_change(self, TRUE);
678     _move(priv, MOVE_RIGHT, increment);
679     clarity_canvas_block_change(self, FALSE);
680 }
681
682 gint clarity_canvas_get_current_index(ClarityCanvas *self) {
683     g_return_val_if_fail(self, 0);
684     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
685
686     return priv->curr_index;
687 }
688
689 AlbumItem *clarity_canvas_get_current_album_item(ClarityCanvas *self) {
690     g_return_val_if_fail(self, NULL);
691     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
692
693     if (!priv->model)
694         return NULL;
695
696     return album_model_get_item_with_index(priv->model, priv->curr_index);
697 }
698
699 void clarity_canvas_add_album_item(ClarityCanvas *self, AlbumItem *item) {
700     g_return_if_fail(self);
701     g_return_if_fail(item);
702
703     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
704     gint index = album_model_get_index_with_album_item(priv->model, item);
705
706     clarity_canvas_block_change(self, TRUE);
707
708     _init_album_item(item, index, self);
709
710     _animate_indices(priv, 0, 0);
711
712     clarity_canvas_block_change(self, FALSE);
713 }
714
715 void clarity_canvas_remove_album_item(ClarityCanvas *self, AlbumItem *item) {
716     g_return_if_fail(self);
717     g_return_if_fail(item);
718
719     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
720     gint index = album_model_get_index_with_album_item(priv->model, item);
721
722     clarity_canvas_block_change(self, TRUE);
723
724     _destroy_cover(self, index);
725
726     _animate_indices(priv, 0, 0);
727
728     clarity_canvas_block_change(self, FALSE);
729 }
730
731 void clarity_canvas_update(ClarityCanvas *self, AlbumItem *item) {
732     g_return_if_fail(self);
733
734     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
735
736     gint index = album_model_get_index_with_album_item(priv->model, item);
737
738     clarity_canvas_block_change(self, TRUE);
739
740     album_model_init_coverart(priv->model, item);
741
742     ClarityCover *ccover = (ClarityCover *) g_list_nth_data(priv->covers, index);
743     if (!ccover)
744         return;
745
746     clarity_cover_set_album_item(ccover, item);
747
748     _set_cover_position(ccover, index);
749
750     _animate_indices(priv, 0, 0);
751
752     clarity_canvas_block_change(self, FALSE);
753 }
754
755 static void _set_cover_from_file(ClarityCanvas *self) {
756     g_return_if_fail(self);
757
758     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
759
760     gchar *filename = fileselection_get_cover_filename();
761
762     if (filename) {
763         AlbumItem *item = album_model_get_item_with_index(priv->model, priv->curr_index);
764         clarity_util_update_coverart(item->tracks, filename);
765     }
766
767     g_free(filename);
768 }
769
770 void on_clarity_set_cover_menuitem_activate(GtkMenuItem *mi, gpointer data) {
771     g_return_if_fail(CLARITY_IS_CANVAS(data));
772
773     _set_cover_from_file(CLARITY_CANVAS(data));
774 }