Implement the updating of a track in clarity
[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 loading_complete;
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 static void _set_loading_complete(ClarityCanvasPrivate *priv, gboolean value) {
136     priv->loading_complete = value;
137
138     if (value) {
139         _update_text(priv);
140     }
141 }
142
143 static void _preview_cover(ClarityCanvasPrivate *priv) {
144     if (!priv->model)
145         return;
146
147     AlbumItem *item = album_model_get_item_with_index(priv->model, priv->curr_index);
148
149     GtkWidget *dialog = clarity_preview_new(item);
150
151     /* Display the dialog */
152     gtk_widget_show_all(dialog);
153 }
154
155 /**
156  * on_main_cover_image_clicked_cb:
157  *
158  * Call handler used for displaying the tracks associated with
159  * the main displayed album cover.
160  *
161  * @ClarityCanvas
162  * @event: event object used to determine the event type
163  * @data: any data needed by the function (not required)
164  *
165  */
166 static gint _on_main_cover_image_clicked_cb(GtkWidget *widget, GdkEvent *event, gpointer data) {
167     ClarityCanvas *ccanvas = CLARITY_CANVAS(widget);
168     ClarityCanvasPrivate *priv = ccanvas->priv;
169     guint mbutton;
170
171     if (event->type != GDK_BUTTON_PRESS)
172             return FALSE;
173
174     mbutton = event->button.button;
175
176     if ((mbutton == 1) && (event->button.state & GDK_SHIFT_MASK)) {
177         _set_loading_complete(priv, FALSE);
178
179         AlbumItem *item = album_model_get_item_with_index(priv->model, priv->curr_index);
180         if (item) {
181             gtkpod_set_displayed_tracks(item->tracks);
182         }
183
184         _set_loading_complete(priv, TRUE);
185     }
186     else if (mbutton == 1) {
187         _preview_cover(priv);
188     }
189     else if ((mbutton == 3) && (event->button.state & GDK_SHIFT_MASK)) {
190         /* Right mouse button clicked and shift pressed.
191          * Go straight to edit details window
192          */
193         AlbumItem *item = album_model_get_item_with_index(priv->model, priv->curr_index);
194         GList *tracks = item->tracks;
195         gtkpod_edit_details(tracks);
196     }
197     else if (mbutton == 3) {
198         /* Right mouse button clicked on its own so display
199          * popup menu
200          */
201         clarity_context_menu_init(ccanvas);
202     }
203
204     return FALSE;
205 }
206
207 /**
208  * embed_widget_size_allocated_cb
209  *
210  * Ensures that when the embed gtk widget is resized or moved
211  * around the clutter animations are centred correctly.
212  *
213  * This finds the new dimensions of the stage each time and centres
214  * the group container accordingly.
215  *
216  */
217 void _embed_widget_size_allocated_cb(GtkWidget *widget,
218                       GtkAllocation *allocation,
219                       gpointer data) {
220     ClarityCanvasPrivate *priv = (ClarityCanvasPrivate *) data;
221     ClutterActor *stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(widget));
222
223     gint centreX = clutter_actor_get_width(stage) / 2;
224     gint centreY = clutter_actor_get_height(stage) / 2;
225     clutter_actor_set_position(priv->container, centreX, centreY);
226 }
227
228 static void clarity_canvas_init(ClarityCanvas *self) {
229     ClarityCanvasPrivate *priv;
230
231     self->priv = CLARITY_CANVAS_GET_PRIVATE (self);
232
233     priv = self->priv;
234
235     priv->title_text = clutter_text_new();
236     clutter_text_set_font_name(CLUTTER_TEXT(priv->title_text), "Sans");
237
238     priv->artist_text = clutter_text_new();
239     clutter_text_set_font_name(CLUTTER_TEXT(priv->title_text), "Sans");
240
241     priv->container = clutter_group_new();
242     clutter_actor_set_reactive(priv->container, TRUE);
243     priv->preview_signal = g_signal_connect (self,
244                                 "button-press-event",
245                                 G_CALLBACK (_on_main_cover_image_clicked_cb),
246                                 priv);
247     clutter_container_add(CLUTTER_CONTAINER(priv->container), priv->title_text, priv->artist_text, NULL);
248
249     priv->embed = gtk_clutter_embed_new();
250     /*
251      * Minimum size before the scrollbars of the parent window
252      * are displayed.
253      */
254     gtk_widget_set_size_request(GTK_WIDGET(priv->embed), DEFAULT_IMG_SIZE * 4, DEFAULT_IMG_SIZE * 2.5);
255     /*
256      * Ensure that things are always centred when the embed
257      * widget is resized.
258      */
259     g_signal_connect(priv->embed, "size-allocate",
260                   G_CALLBACK(_embed_widget_size_allocated_cb), priv);
261
262     ClutterActor *stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(priv->embed));
263     clutter_container_add_actor(CLUTTER_CONTAINER(stage), priv->container);
264
265     gtk_widget_show(priv->embed);
266
267     gtk_box_pack_start(GTK_BOX(self), priv->embed, TRUE, TRUE, 0);
268
269     priv->covers = NULL;
270     priv->timeline = clutter_timeline_new(1600);
271     priv->alpha = clutter_alpha_new_full(priv->timeline, CLUTTER_EASE_OUT_EXPO);
272     priv->curr_index = 0;
273     priv->loading_complete = FALSE;
274
275 }
276
277 GtkWidget *clarity_canvas_new() {
278     return g_object_new(CLARITY_TYPE_CANVAS, NULL);
279 }
280
281 /**
282  * clarity_canvas_get_background_display_color:
283  *
284  * Returns the background color of the clarity canvas.
285  *
286  * The return value is a GdkRGBA
287  *
288  */
289 GdkRGBA *clarity_canvas_get_background_color(ClarityCanvas *self) {
290     g_return_val_if_fail(CLARITY_IS_CANVAS(self), NULL);
291
292     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
293
294     ClutterActor *stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(priv->embed));
295
296     ClutterColor *ccolor;
297     ccolor = g_malloc(sizeof(ClutterColor));
298
299     clutter_stage_get_color(CLUTTER_STAGE(stage), ccolor);
300     g_return_val_if_fail(ccolor, NULL);
301
302     GdkRGBA *rgba;
303     rgba = g_malloc(sizeof(GdkRGBA));
304     rgba->red = ((gdouble) ccolor->red) / 255;
305     rgba->green = ((gdouble) ccolor->green) / 255;
306     rgba->blue = ((gdouble) ccolor->blue) / 255;
307     rgba->alpha = ((gdouble) ccolor->alpha) / 255;
308
309     return rgba;
310 }
311
312 /**
313  * clarity_canvas_get_text_color:
314  *
315  * Returns the text color of the clarity text.
316  *
317  * The return value is a GdkRGBA
318  *
319  */
320 GdkRGBA *clarity_canvas_get_text_color(ClarityCanvas *self) {
321     g_return_val_if_fail(CLARITY_IS_CANVAS(self), NULL);
322
323     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
324
325     ClutterColor *ccolor;
326     ccolor = g_malloc(sizeof(ClutterColor));
327
328     clutter_text_get_color(CLUTTER_TEXT(priv->title_text), ccolor);
329     g_return_val_if_fail(ccolor, NULL);
330
331     GdkRGBA *rgba;
332     rgba = g_malloc(sizeof(GdkRGBA));
333     rgba->red = ((gdouble) ccolor->red) / 255;
334     rgba->green = ((gdouble) ccolor->green) / 255;
335     rgba->blue = ((gdouble) ccolor->blue) / 255;
336     rgba->alpha = ((gdouble) ccolor->alpha) / 255;
337
338     return rgba;
339 }
340
341 void clarity_canvas_set_background_color(ClarityCanvas *self, const gchar *color_string) {
342     g_return_if_fail(self);
343     g_return_if_fail(color_string);
344
345     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
346
347     ClutterActor *stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(priv->embed));
348
349     ClutterColor *ccolor;
350     ccolor = g_malloc(sizeof(ClutterColor));
351
352     clutter_color_from_string(ccolor, color_string);
353     clutter_stage_set_color(CLUTTER_STAGE(stage), ccolor);
354 }
355
356 void clarity_canvas_set_text_color(ClarityCanvas *self, const gchar *color_string) {
357     g_return_if_fail(self);
358     g_return_if_fail(color_string);
359
360     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
361
362     ClutterColor *ccolor;
363     ccolor = g_malloc(sizeof(ClutterColor));
364
365     clutter_color_from_string(ccolor, color_string);
366
367     clutter_text_set_color(CLUTTER_TEXT(priv->title_text), ccolor);
368     clutter_text_set_color(CLUTTER_TEXT(priv->artist_text), ccolor);
369 }
370
371 void clarity_canvas_clear(ClarityCanvas *self) {
372     g_return_if_fail(self);
373     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
374
375     if (CLUTTER_IS_ACTOR(priv->container)) {
376         GList *iter = priv->covers;
377         while(iter) {
378             ClarityCover *ccover = iter->data;
379             // cover is not referenced anywhere else so it should be destroyed too
380             clutter_container_remove(CLUTTER_CONTAINER(priv->container), CLUTTER_ACTOR(ccover), NULL);
381             iter = iter->next;
382         }
383
384         if (CLUTTER_IS_ACTOR(priv->artist_text))
385             clutter_text_set_text(CLUTTER_TEXT(priv->artist_text), "");
386
387         if (CLUTTER_IS_ACTOR(priv->title_text))
388             clutter_text_set_text(CLUTTER_TEXT(priv->title_text), "");
389     }
390
391     priv->covers = NULL;
392     priv->model = NULL;
393     priv->curr_index = 0;
394 }
395
396 static void _calculate_index_angle_and_dir (gint dist_from_front, enum DIRECTION dir, gint *angle, ClutterRotateDirection *rotation_dir) {
397     /* The front item direction depends on the direction we came from */
398     if (dist_from_front == 0) {
399         *rotation_dir =  (dir == MOVE_RIGHT ? CLUTTER_ROTATE_CCW : CLUTTER_ROTATE_CW);
400         *angle = 0;
401     }
402
403     /* Item on the right */
404     else if (dist_from_front > 0) {
405         *rotation_dir = CLUTTER_ROTATE_CCW;
406         *angle = 360 - MAX_ANGLE;
407     }
408
409     /* Item on the left */
410     else if (dist_from_front < 0) {
411         *rotation_dir = CLUTTER_ROTATE_CW;
412         *angle = MAX_ANGLE;
413     }
414 }
415
416 static gint _calculate_index_distance (gint dist_from_front) {
417     gint dist = ((ABS(dist_from_front) - 1) * COVER_SPACE) + FRONT_COVER_SPACE;
418
419     if (dist_from_front == 0)
420         return 0;
421
422     return (dist_from_front > 0 ? dist : 0 - dist);
423 }
424
425 static float _calculate_index_scale(gint dist_from_front) {
426     if (dist_from_front == 0)
427         return MAX_SCALE;
428     else
429         return 1;
430 }
431
432 static gint _calculate_index_opacity (gint dist_from_front) {
433     return CLAMP ( 255 * (VISIBLE_ITEMS - ABS(dist_from_front)) / VISIBLE_ITEMS, 0, 255);
434 }
435
436 static void _display_clarity_cover(ClarityCover *ccover, gint index) {
437     ClutterTimeline  *timeline = clutter_timeline_new(1600 * 5);
438     ClutterAlpha *alpha = clutter_alpha_new_full (timeline, CLUTTER_EASE_OUT_EXPO);
439
440     gint opacity = _calculate_index_opacity(index);
441     clutter_actor_animate_with_alpha(CLUTTER_ACTOR(ccover), alpha, "opacity", opacity, NULL);
442     clutter_timeline_start (timeline);
443 }
444
445 static void _set_cover_position(ClarityCover *ccover, gint index) {
446     gint pos = _calculate_index_distance(index);
447     clutter_actor_set_position(
448                     CLUTTER_ACTOR(ccover),
449                     pos - clarity_cover_get_artwork_width(ccover) / 2,
450                     FLOOR - clarity_cover_get_artwork_height(ccover));
451 }
452
453 static gboolean _create_cover_actors(ClarityCanvasPrivate *priv, AlbumItem *album_item, gint index) {
454     g_return_val_if_fail(priv, FALSE);
455
456     _set_loading_complete(priv, FALSE);
457
458     ClarityCover *ccover = clarity_cover_new();
459     clutter_actor_set_opacity(CLUTTER_ACTOR(ccover), 0);
460     priv->covers = g_list_insert(priv->covers, ccover, index);
461
462     clutter_container_add_actor(
463                             CLUTTER_CONTAINER(priv->container),
464                             CLUTTER_ACTOR(ccover));
465
466     clarity_cover_set_album_item(ccover, album_item);
467
468     _set_cover_position(ccover, index);
469
470     if((priv->curr_index + VISIBLE_ITEMS < index) ||
471             (priv->curr_index - VISIBLE_ITEMS > index)) {
472         _set_loading_complete(priv, TRUE);
473         return FALSE;
474     }
475
476     float scale = _calculate_index_scale(index);
477
478     gint angle;
479     ClutterRotateDirection rotation_dir;
480     _calculate_index_angle_and_dir(index, MOVE_LEFT, &angle, &rotation_dir);
481
482     clutter_actor_set_rotation(
483             CLUTTER_ACTOR(ccover),
484             CLUTTER_Y_AXIS,
485             angle,
486             clutter_actor_get_width(CLUTTER_ACTOR(ccover)) / 2,
487             0, 0);
488
489     clutter_actor_set_scale_full(
490             CLUTTER_ACTOR(ccover),
491             scale,
492             scale,
493             clarity_cover_get_artwork_width(ccover) / 2,
494             clarity_cover_get_artwork_height(ccover) / 2);
495
496     clutter_actor_lower_bottom(CLUTTER_ACTOR(ccover));
497
498     _display_clarity_cover(ccover, index);
499
500     _set_loading_complete(priv, TRUE);
501
502     return FALSE;
503 }
504
505 void _init_album_item(gpointer value, gint index, gpointer user_data) {
506     AlbumItem *item = (AlbumItem *) value;
507     ClarityCanvas *cc = CLARITY_CANVAS(user_data);
508     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(cc);
509
510     album_model_init_coverart(priv->model, item);
511
512     _create_cover_actors(priv, item, index);
513 }
514
515 void _destroy_cover(ClarityCanvas *cc, gint index) {
516     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(cc);
517
518     ClarityCover *ccover = (ClarityCover *) g_list_nth_data(priv->covers, index);
519     if (!ccover)
520         return;
521
522     priv->covers = g_list_remove(priv->covers, ccover);
523
524     clutter_container_remove_actor(
525                                CLUTTER_CONTAINER(priv->container),
526                                CLUTTER_ACTOR(ccover));
527
528     clarity_cover_destroy(CLUTTER_ACTOR(ccover));
529
530     return;
531 }
532
533 static gpointer _init_album_model(gpointer data) {
534     g_return_val_if_fail(CLARITY_IS_CANVAS(data), NULL);
535
536     ClarityCanvas *cc = CLARITY_CANVAS(data);
537     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(cc);
538     AlbumModel *model = priv->model;
539
540     album_model_foreach(model, _init_album_item, cc);
541
542     return NULL;
543 }
544
545 void clarity_canvas_init_album_model(ClarityCanvas *self, AlbumModel *model) {
546     g_return_if_fail(self);
547     g_return_if_fail(model);
548
549     if (album_model_get_size(model) == 0)
550         return;
551
552     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
553     priv->model = model;
554
555     _init_album_model(self);
556
557 }
558
559 static void _clear_rotation_behaviours(GList *covers) {
560     //Clear rotation behaviours
561     GList *iter = covers;
562     while (iter) {
563         ClarityCover *ccover = iter->data;
564         clarity_cover_clear_rotation_behaviour(ccover);
565         iter = iter->next;
566     }
567 }
568
569 static void _animate_indices(ClarityCanvasPrivate *priv, gint direction, gint increment) {
570
571     for (gint i = 0; i < g_list_length(priv->covers); ++i) {
572         ClarityCover *ccover = g_list_nth_data(priv->covers, i);
573
574         gint dist = i - priv->curr_index + (direction * increment);
575         gfloat scale = 1;
576         gint pos = 0;
577         gint opacity = 0;
578         gint angle = 0;
579         ClutterRotateDirection rotation_dir;
580
581         opacity = _calculate_index_opacity(dist);
582         scale = _calculate_index_scale(dist);
583         pos = _calculate_index_distance(dist);
584         _calculate_index_angle_and_dir(dist, direction, &angle, &rotation_dir);
585
586         /*Rotation*/
587         clarity_cover_set_rotation_behaviour(ccover, priv->alpha, angle, rotation_dir);
588
589         /* Opacity */
590         clutter_actor_animate_with_alpha (CLUTTER_ACTOR(ccover), priv->alpha,
591                         "opacity", opacity,
592                         NULL);
593
594         gfloat w = clarity_cover_get_artwork_width(ccover);
595         gfloat h = clarity_cover_get_artwork_height(ccover);
596
597         /* Position and scale */
598         clutter_actor_animate_with_alpha (CLUTTER_ACTOR(ccover), priv->alpha,
599                         "scale-x",          scale,
600                         "scale-y",          scale,
601                         "scale-center-x" ,  w / 2,
602                         "scale-center-y", h / 2,
603                         "x", pos - (w / 2),
604                         "y", FLOOR - h,
605                         NULL);
606      }
607 }
608
609 static void _restore_z_order(ClarityCanvasPrivate *priv) {
610     g_return_if_fail(priv);
611
612     if (g_list_length(priv->covers) == 0)
613         return;
614
615     GList *main_cover = g_list_nth(priv->covers, priv->curr_index);
616     g_return_if_fail(main_cover);
617
618     GList *iter = main_cover ->prev;
619     while(iter) {
620         ClarityCover *ccover = iter->data;
621         clutter_actor_lower_bottom(CLUTTER_ACTOR(ccover));
622         iter = iter->prev;
623     }
624
625     iter = main_cover->next;
626     while(iter) {
627         ClarityCover *ccover = iter->data;
628         clutter_actor_lower_bottom(CLUTTER_ACTOR(ccover));
629         iter = iter->next;
630     }
631 }
632
633 static void _move(ClarityCanvasPrivate *priv, enum DIRECTION direction, gint increment) {
634
635     _set_loading_complete(priv, FALSE);
636
637     /* Stop any animation */
638     clutter_timeline_stop(priv->timeline);
639
640     /* Clear all current rotation behaviours */
641     _clear_rotation_behaviours(priv->covers);
642
643     /* Animate to move left */
644     _animate_indices (priv, direction, increment);
645
646     /* Begin the animation */
647     clutter_timeline_start(priv->timeline);
648
649     priv->curr_index += ((direction * -1) * increment);
650
651     _restore_z_order(priv);
652
653     _set_loading_complete(priv, TRUE);
654 }
655
656 void clarity_canvas_move_left(ClarityCanvas *self, gint increment) {
657     g_return_if_fail(self);
658     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
659
660     if(priv->curr_index == g_list_length(priv->covers) - 1)
661         return;
662
663     _move(priv, MOVE_LEFT, increment);
664 }
665
666 void clarity_canvas_move_right(ClarityCanvas *self, gint increment) {
667     g_return_if_fail(self);
668     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
669
670     if(priv->curr_index == 0)
671         return;
672
673     _move(priv, MOVE_RIGHT, increment);
674 }
675
676 gint clarity_canvas_get_current_index(ClarityCanvas *self) {
677     g_return_val_if_fail(self, 0);
678     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
679
680     return priv->curr_index;
681 }
682
683 gboolean clarity_canvas_is_loading(ClarityCanvas *self) {
684     g_return_val_if_fail(self, FALSE);
685     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
686     return !priv->loading_complete;
687 }
688
689 void clarity_canvas_add_album_item(ClarityCanvas *self, AlbumItem *item) {
690     g_return_if_fail(self);
691     g_return_if_fail(item);
692
693     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
694     gint index = album_model_get_index_with_album_item(priv->model, item);
695
696     _set_loading_complete(priv, FALSE);
697
698     _init_album_item(item, index, self);
699
700     _animate_indices(priv, 0, 0);
701
702     _set_loading_complete(priv, TRUE);
703 }
704
705 void clarity_canvas_remove_album_item(ClarityCanvas *self, AlbumItem *item) {
706     g_return_if_fail(self);
707     g_return_if_fail(item);
708
709     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
710     gint index = album_model_get_index_with_album_item(priv->model, item);
711
712     _set_loading_complete(priv, FALSE);
713
714     _destroy_cover(self, index);
715
716     _animate_indices(priv, 0, 0);
717
718     _set_loading_complete(priv, TRUE);
719 }
720
721 void clarity_canvas_update(ClarityCanvas *cc, AlbumItem *item) {
722     g_return_if_fail(cc);
723
724     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(cc);
725
726     gint index = album_model_get_index_with_album_item(priv->model, item);
727
728     _set_loading_complete(priv, FALSE);
729
730     album_model_init_coverart(priv->model, item);
731
732     ClarityCover *ccover = (ClarityCover *) g_list_nth_data(priv->covers, index);
733     if (!ccover)
734         return;
735
736     clarity_cover_set_album_item(ccover, item);
737
738     _set_cover_position(ccover, index);
739
740     _animate_indices(priv, 0, 0);
741
742     _set_loading_complete(priv, TRUE);
743 }
744
745 static void _set_cover_from_file(ClarityCanvas *self) {
746     g_return_if_fail(self);
747
748     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
749
750     gchar *filename;
751     Track *track;
752     GList *tracks;
753
754     filename = fileselection_get_cover_filename();
755
756     if (filename) {
757         AlbumItem *item = album_model_get_item_with_index(priv->model, priv->curr_index);
758         tracks = g_list_copy(item->tracks);
759
760         while (tracks) {
761             track = tracks->data;
762
763             if (gp_track_set_thumbnails(track, filename)) {
764                 ExtraTrackData *etd;
765                 etd = track->userdata;
766                 etd->tartwork_changed = TRUE;
767
768                 gtkpod_track_updated(track);
769                 data_changed(track->itdb);
770
771                 etd->tartwork_changed = FALSE;
772             }
773
774             tracks = tracks->next;
775         }
776     }
777
778     g_free(filename);
779 }
780
781 void on_clarity_set_cover_menuitem_activate(GtkMenuItem *mi, gpointer data) {
782     g_return_if_fail(CLARITY_IS_CANVAS(data));
783
784     _set_cover_from_file(CLARITY_CANVAS(data));
785 }