1 /*
2 * Copyright (C) 2013 Bastien Nocera <hadess@hadess.net>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 *
18 * The Totem project hereby grant permission for non-gpl compatible GStreamer
19 * plugins to be used and distributed together with GStreamer and Totem. This
20 * permission are above and beyond the permissions granted by the GPL license
21 * Totem is covered by.
22 *
23 * Monday 7th February 2005: Christian Schaller: Add exception clause.
24 * See license_change file for details.
25 *
26 */
27
28 #include <icon-helpers.h>
29
30 #define GNOME_DESKTOP_USE_UNSTABLE_API 1
31 #include <libgnome-desktop/gnome-desktop-thumbnail.h>
32
33 #define DEFAULT_MAX_THREADS 1
34 #define THUMB_SEARCH_SIZE 256
35 #define THUMB_SEARCH_HEIGHT THUMB_SEARCH_SIZE
36 #define SOURCES_MAX_HEIGHT 64
37 #define VIDEO_ICON_SIZE 32
38
39 typedef enum {
40 ICON_BOX = 0,
41 ICON_CHANNEL,
42 ICON_VIDEO,
43 ICON_VIDEO_THUMBNAILING,
44 ICON_OPTICAL,
45 NUM_ICONS
46 } IconType;
47
48 static GnomeDesktopThumbnailFactory *factory;
49 static GThreadPool *thumbnail_pool;
50 static GdkPixbuf *icons[NUM_ICONS];
51 static GHashTable *cache_thumbnails; /* key=url, value=GdkPixbuf */
52
53 #define STROKE 0x3b3c38ff
54 #define FILL_DEFAULT 0x2d2d2dff
55 #define FILL_TRANSPARENT 0x00000000
56 #define FILL_MOVIE 0x000000ff
57
58 static GdkPixbuf *load_icon (GdkPixbuf *pixbuf,
59 gboolean resize,
60 guint32 fill);
61 static GdkPixbuf *load_named_icon (const char *name,
62 int size,
63 guint32 fill);
64
65 static gboolean
media_is_local(GrlMedia * media)66 media_is_local (GrlMedia *media)
67 {
68 const char *id;
69
70 id = grl_media_get_source (media);
71 if (g_strcmp0 (id, "grl-tracker-source") == 0 ||
72 g_strcmp0 (id, "grl-tracker3-source") == 0 ||
73 g_strcmp0 (id, "grl-filesystem") == 0 ||
74 g_strcmp0 (id, "grl-bookmarks") == 0)
75 return TRUE;
76 return FALSE;
77 }
78
79 GdkPixbuf *
totem_grilo_get_thumbnail_finish(GObject * source_object,GAsyncResult * res,GError ** error)80 totem_grilo_get_thumbnail_finish (GObject *source_object,
81 GAsyncResult *res,
82 GError **error)
83 {
84 g_return_val_if_fail (g_task_is_valid (res, source_object), NULL);
85
86 return g_task_propagate_pointer (G_TASK (res), error);
87 }
88
89 static void
load_thumbnail_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)90 load_thumbnail_cb (GObject *source_object,
91 GAsyncResult *res,
92 gpointer user_data)
93 {
94 GTask *task = user_data;
95 GdkPixbuf *pixbuf;
96 GError *error = NULL;
97 const GFile *file;
98
99 pixbuf = gdk_pixbuf_new_from_stream_finish (res, &error);
100 if (!pixbuf) {
101 g_task_return_error (task, error);
102 g_object_unref (task);
103 return;
104 }
105
106 /* Cache it */
107 file = g_task_get_task_data (task);
108 if (file) {
109 gboolean is_source;
110
111 is_source = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "is-source"));
112 if (is_source) {
113 GdkPixbuf *new_pixbuf;
114
115 new_pixbuf = load_icon (pixbuf, TRUE, FILL_DEFAULT);
116 g_object_unref (pixbuf);
117 pixbuf = new_pixbuf;
118 } else {
119 GdkPixbuf *new_pixbuf;
120
121 new_pixbuf = load_icon (pixbuf, FALSE, FILL_MOVIE);
122 g_object_unref (pixbuf);
123 pixbuf = new_pixbuf;
124 }
125 g_hash_table_insert (cache_thumbnails,
126 g_file_get_uri (G_FILE (file)),
127 g_object_ref (pixbuf));
128 }
129
130 g_task_return_pointer (task, pixbuf, g_object_unref);
131 g_object_unref (task);
132 }
133
134 static void
get_stream_thumbnail_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)135 get_stream_thumbnail_cb (GObject *source_object,
136 GAsyncResult *res,
137 gpointer user_data)
138 {
139 GTask *task = user_data;
140 GFileInputStream *stream;
141 GError *error = NULL;
142 gboolean is_source;
143
144 stream = g_file_read_finish (G_FILE (source_object), res, &error);
145 if (!stream) {
146 g_task_return_error (task, error);
147 g_object_unref (task);
148 return;
149 }
150
151 is_source = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "is-source"));
152 gdk_pixbuf_new_from_stream_at_scale_async (G_INPUT_STREAM (stream),
153 is_source ? -1 : THUMB_SEARCH_SIZE - 2,
154 is_source ? -1 : THUMB_SEARCH_HEIGHT -2 ,
155 TRUE,
156 g_task_get_cancellable (task),
157 load_thumbnail_cb,
158 task);
159 g_object_unref (G_OBJECT (stream));
160 }
161
162 static void
save_bookmark_thumbnail(GrlMedia * media,const char * uri)163 save_bookmark_thumbnail (GrlMedia *media,
164 const char *uri)
165 {
166 char *thumb_path, *thumb_url;
167 GrlRegistry *registry;
168 GrlSource *source;
169
170 if (g_strcmp0 (grl_media_get_source (media), "grl-bookmarks") != 0)
171 return;
172
173 thumb_path = gnome_desktop_thumbnail_path_for_uri (uri, GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE);
174 thumb_url = g_filename_to_uri (thumb_path, NULL, NULL);
175 g_free (thumb_path);
176 grl_media_set_thumbnail (media, thumb_url);
177 g_free (thumb_url);
178
179 registry = grl_registry_get_default ();
180 source = grl_registry_lookup_source (registry, "grl-bookmarks");
181 grl_source_store_sync (source,
182 NULL,
183 media,
184 GRL_WRITE_NORMAL,
185 NULL);
186 }
187
188 static void
thumbnail_media_async_thread(GTask * task,gpointer user_data)189 thumbnail_media_async_thread (GTask *task,
190 gpointer user_data)
191 {
192 GrlMedia *media;
193 GdkPixbuf *pixbuf, *tmp_pixbuf;
194 const char *uri;
195 GDateTime *mtime;
196 gint64 unix_date;
197
198 if (g_task_return_error_if_cancelled (task)) {
199 g_object_unref (task);
200 return;
201 }
202
203 media = GRL_MEDIA (g_task_get_source_object (task));
204 uri = grl_media_get_url (media);
205
206 mtime = grl_media_get_modification_date (media);
207 if (!mtime) {
208 GrlRegistry *registry;
209 GrlKeyID key_id;
210
211 registry = grl_registry_get_default ();
212 key_id = grl_registry_lookup_metadata_key (registry, "bookmark-date");
213 mtime = grl_data_get_boxed (GRL_DATA (media), key_id);
214 }
215 unix_date = mtime ? g_date_time_to_unix (mtime) : g_get_real_time () / 1000000;
216
217 if (!uri) {
218 g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "URI missing");
219 g_object_unref (task);
220 return;
221 }
222
223 tmp_pixbuf = gnome_desktop_thumbnail_factory_generate_thumbnail (factory, uri, "video/x-totem-stream");
224
225 if (!tmp_pixbuf) {
226 g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Thumbnailing failed");
227 g_object_unref (task);
228 return;
229 }
230
231 gnome_desktop_thumbnail_factory_save_thumbnail (factory, tmp_pixbuf, uri, unix_date);
232
233 /* Save the thumbnail URL for the bookmarks source */
234 save_bookmark_thumbnail (media, uri);
235
236 /* Add frame */
237 pixbuf = load_icon (tmp_pixbuf, FALSE, FILL_MOVIE);
238 g_object_unref (tmp_pixbuf);
239
240 g_task_return_pointer (task, pixbuf, g_object_unref);
241 g_object_unref (task);
242 }
243
244 static void
totem_grilo_thumbnail_media(GrlMedia * media,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)245 totem_grilo_thumbnail_media (GrlMedia *media,
246 GCancellable *cancellable,
247 GAsyncReadyCallback callback,
248 gpointer user_data)
249 {
250 GTask *task;
251
252 task = g_task_new (media, cancellable, callback, user_data);
253 g_task_set_priority (task, G_PRIORITY_LOW);
254 g_thread_pool_push (thumbnail_pool, task, NULL);
255 }
256
257 static GdkPixbuf *
totem_grilo_thumbnail_media_finish(GrlMedia * media,GAsyncResult * res,GError ** error)258 totem_grilo_thumbnail_media_finish (GrlMedia *media,
259 GAsyncResult *res,
260 GError **error)
261 {
262 g_return_val_if_fail (g_task_is_valid (res, media), NULL);
263
264 return g_task_propagate_pointer (G_TASK (res), error);
265 }
266
267 static void
thumbnail_media_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)268 thumbnail_media_cb (GObject *source_object,
269 GAsyncResult *res,
270 gpointer user_data)
271 {
272 GTask *task = user_data;
273 GdkPixbuf *pixbuf;
274 GError *error = NULL;
275
276 pixbuf = totem_grilo_thumbnail_media_finish (GRL_MEDIA (source_object), res, &error);
277 if (!pixbuf)
278 g_task_return_error (task, error);
279 else
280 g_task_return_pointer (task, pixbuf, g_object_unref);
281 g_object_unref (task);
282 }
283
284 void
totem_grilo_get_thumbnail(GObject * object,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)285 totem_grilo_get_thumbnail (GObject *object,
286 GCancellable *cancellable,
287 GAsyncReadyCallback callback,
288 gpointer user_data)
289 {
290 GTask *task;
291 const char *url_thumb = NULL;
292 const GdkPixbuf *thumbnail = NULL;
293 GFile *file;
294
295 task = g_task_new (G_OBJECT (object),
296 cancellable,
297 callback,
298 user_data);
299
300 if (GRL_IS_MEDIA (object)) {
301 url_thumb = grl_media_get_thumbnail (GRL_MEDIA (object));
302 if (!url_thumb && media_is_local (GRL_MEDIA (object))) {
303 totem_grilo_thumbnail_media (GRL_MEDIA (object),
304 cancellable,
305 thumbnail_media_cb,
306 task);
307 return;
308 }
309 } else if (GRL_IS_SOURCE (object)) {
310 GIcon *icon;
311
312 icon = grl_source_get_icon (GRL_SOURCE (object));
313 if (icon) {
314 GFile *file;
315
316 file = g_file_icon_get_file (G_FILE_ICON (icon));
317 url_thumb = g_file_get_uri (file);
318
319 g_object_set_data (G_OBJECT (task), "is-source", GUINT_TO_POINTER (TRUE));
320 }
321 }
322 if (url_thumb == NULL) {
323 g_task_return_pointer (task, NULL, NULL);
324 g_object_unref (task);
325 return;
326 }
327
328 /* Check cache */
329 thumbnail = g_hash_table_lookup (cache_thumbnails, url_thumb);
330 if (thumbnail) {
331 g_task_return_pointer (task,
332 g_object_ref (G_OBJECT (thumbnail)),
333 g_object_unref);
334 g_object_unref (task);
335 return;
336 }
337
338 file = g_file_new_for_uri (url_thumb);
339 g_task_set_task_data (task, file, g_object_unref);
340 g_file_read_async (file, G_PRIORITY_DEFAULT, cancellable,
341 get_stream_thumbnail_cb, task);
342 }
343
344 static void
put_pixel(guchar * p)345 put_pixel (guchar *p)
346 {
347 p[0] = (STROKE & 0xff000000) >> 24;
348 p[1] = (STROKE & 0x00ff0000) >> 16;
349 p[2] = (STROKE & 0x0000ff00) >> 8;
350 p[3] = (STROKE & 0x000000ff);
351 }
352
353 static GdkPixbuf *
load_icon(GdkPixbuf * pixbuf,gboolean resize,guint32 fill)354 load_icon (GdkPixbuf *pixbuf,
355 gboolean resize,
356 guint32 fill)
357 {
358 GdkPixbuf *ret;
359 guchar *pixels;
360 int rowstride;
361 int x, y;
362 int width, height;
363 gdouble offset_x, offset_y, scale;
364 int dest_x, dest_y;
365
366 ret = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
367 TRUE,
368 8, THUMB_SEARCH_SIZE, THUMB_SEARCH_HEIGHT);
369 pixels = gdk_pixbuf_get_pixels (ret);
370 rowstride = gdk_pixbuf_get_rowstride (ret);
371
372 /* Clean up and draw a border */
373 gdk_pixbuf_fill (ret, fill);
374
375 /* top */
376 for (x = 0; x < THUMB_SEARCH_SIZE; x++)
377 put_pixel (pixels + x * 4);
378 /* bottom */
379 for (x = 0; x < THUMB_SEARCH_SIZE; x++)
380 put_pixel (pixels + (THUMB_SEARCH_HEIGHT -1) * rowstride + x * 4);
381 /* left */
382 for (y = 1; y < THUMB_SEARCH_HEIGHT - 1; y++)
383 put_pixel (pixels + y * rowstride);
384 /* right */
385 for (y = 1; y < THUMB_SEARCH_HEIGHT - 1; y++)
386 put_pixel (pixels + y * rowstride + (THUMB_SEARCH_SIZE - 1) * 4);
387
388 width = gdk_pixbuf_get_width (pixbuf);
389 height = gdk_pixbuf_get_height (pixbuf);
390
391 if (resize &&
392 (width > (THUMB_SEARCH_SIZE / 4 * 3) ||
393 height > SOURCES_MAX_HEIGHT)) {
394 gdouble scale_x, scale_y;
395
396 scale_x = ((gdouble) THUMB_SEARCH_SIZE / 4.0 * 3.0) / (gdouble) width;
397 scale_y = (gdouble) SOURCES_MAX_HEIGHT / (gdouble) height;
398
399 scale = MIN(MIN(scale_x, scale_y), 1.0);
400 } else {
401 scale = 1.0;
402 }
403
404 /* Put the icon in the middle, with help from this post:
405 * http://permalink.gmane.org/gmane.comp.desktop.rox.devel/9065 */
406 offset_x = (THUMB_SEARCH_SIZE - width * scale) / 2;
407 offset_y = (THUMB_SEARCH_HEIGHT - height * scale) / 2;
408 dest_x = MAX(offset_x, 0);
409 dest_y = MAX(offset_y, 0);
410 gdk_pixbuf_composite (pixbuf, ret,
411 dest_x, dest_y,
412 MIN(THUMB_SEARCH_SIZE, width * scale),
413 MIN(THUMB_SEARCH_HEIGHT, height * scale),
414 offset_x,
415 offset_y,
416 scale, scale,
417 GDK_INTERP_BILINEAR,
418 255);
419
420 return ret;
421 }
422
423 static GdkPixbuf *
load_named_icon(const char * name,int size,guint32 fill)424 load_named_icon (const char *name,
425 int size,
426 guint32 fill)
427 {
428 GdkScreen *screen;
429 GIcon *icon;
430 GList *windows;
431 GtkIconInfo *info;
432 GtkIconTheme *theme;
433 GtkStyleContext *context;
434 GdkPixbuf *pixbuf, *ret;
435
436 windows = gtk_window_list_toplevels ();
437 if (windows == NULL)
438 return NULL;
439
440 icon = g_themed_icon_new (name);
441 screen = gdk_screen_get_default ();
442 theme = gtk_icon_theme_get_for_screen (screen);
443 info = gtk_icon_theme_lookup_by_gicon (theme, icon, size, GTK_ICON_LOOKUP_FORCE_SYMBOLIC);
444 context = gtk_widget_get_style_context (GTK_WIDGET (windows->data));
445 pixbuf = gtk_icon_info_load_symbolic_for_context (info, context, NULL, NULL);
446
447 ret = load_icon (pixbuf, FALSE, fill);
448
449 g_object_unref (pixbuf);
450 g_object_unref (info);
451 g_object_unref (icon);
452
453 return ret;
454 }
455
456 GdkPixbuf *
totem_grilo_get_icon(GrlMedia * media,gboolean * thumbnailing)457 totem_grilo_get_icon (GrlMedia *media,
458 gboolean *thumbnailing)
459 {
460 g_return_val_if_fail (thumbnailing != NULL, NULL);
461
462 *thumbnailing = FALSE;
463
464 if (grl_media_is_container (media)) {
465 return g_object_ref (icons[ICON_BOX]);
466 } else {
467 if (grl_media_get_thumbnail (media) ||
468 media_is_local (media)) {
469 *thumbnailing = TRUE;
470 return g_object_ref (icons[ICON_VIDEO_THUMBNAILING]);
471 } else {
472 if (g_str_equal (grl_media_get_source (media), "grl-optical-media"))
473 return g_object_ref (icons[ICON_OPTICAL]);
474 return g_object_ref (icons[ICON_VIDEO]);
475 }
476 }
477 return NULL;
478 }
479
480 const GdkPixbuf *
totem_grilo_get_video_icon(void)481 totem_grilo_get_video_icon (void)
482 {
483 return icons[ICON_VIDEO];
484 }
485
486 const GdkPixbuf *
totem_grilo_get_box_icon(void)487 totem_grilo_get_box_icon (void)
488 {
489 return icons[ICON_BOX];
490 }
491
492 const GdkPixbuf *
totem_grilo_get_channel_icon(void)493 totem_grilo_get_channel_icon (void)
494 {
495 return icons[ICON_CHANNEL];
496 }
497
498 const GdkPixbuf *
totem_grilo_get_optical_icon(void)499 totem_grilo_get_optical_icon (void)
500 {
501 return icons[ICON_OPTICAL];
502 }
503
504 void
totem_grilo_clear_icons(void)505 totem_grilo_clear_icons (void)
506 {
507 guint i;
508
509 for (i = 0; i < NUM_ICONS; i++)
510 g_clear_object (&icons[i]);
511
512 g_clear_pointer (&cache_thumbnails, g_hash_table_destroy);
513 g_clear_object (&factory);
514 g_thread_pool_free (thumbnail_pool, TRUE, FALSE);
515 thumbnail_pool = NULL;
516 }
517
518 void
totem_grilo_setup_icons(void)519 totem_grilo_setup_icons (void)
520 {
521 icons[ICON_BOX] = load_named_icon ("folder-symbolic", VIDEO_ICON_SIZE, FILL_DEFAULT);
522 icons[ICON_CHANNEL] = load_named_icon ("tv-symbolic", VIDEO_ICON_SIZE, FILL_DEFAULT);
523 icons[ICON_VIDEO] = load_named_icon ("folder-videos-symbolic", VIDEO_ICON_SIZE, FILL_DEFAULT);
524 icons[ICON_VIDEO_THUMBNAILING] = load_named_icon ("content-loading-symbolic", VIDEO_ICON_SIZE, FILL_TRANSPARENT);
525 icons[ICON_OPTICAL] = load_named_icon ("media-optical-dvd-symbolic", VIDEO_ICON_SIZE, FILL_DEFAULT);
526
527 cache_thumbnails = g_hash_table_new_full (g_str_hash,
528 g_str_equal,
529 g_free,
530 g_object_unref);
531
532 factory = gnome_desktop_thumbnail_factory_new (GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE);
533 thumbnail_pool = g_thread_pool_new ((GFunc) thumbnail_media_async_thread, NULL, DEFAULT_MAX_THREADS, TRUE, NULL);
534 }
535
536 void
totem_grilo_pause_icon_thumbnailing(void)537 totem_grilo_pause_icon_thumbnailing (void)
538 {
539 g_return_if_fail (thumbnail_pool != NULL);
540 g_thread_pool_set_max_threads (thumbnail_pool, 0, NULL);
541 }
542
543 void
totem_grilo_resume_icon_thumbnailing(void)544 totem_grilo_resume_icon_thumbnailing (void)
545 {
546 g_return_if_fail (thumbnail_pool != NULL);
547 g_thread_pool_set_max_threads (thumbnail_pool, DEFAULT_MAX_THREADS, NULL);
548 }
549