1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /* totem-playlist.c
3
4 Copyright (C) 2002, 2003, 2004, 2005 Bastien Nocera
5
6 The Gnome Library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Library General Public License as
8 published by the Free Software Foundation; either version 2 of the
9 License, or (at your option) any later version.
10
11 The Gnome Library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Library General Public License for more details.
15
16 You should have received a copy of the GNU Library General Public
17 License along with the Gnome Library; see the file COPYING.LIB. If not,
18 write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301 USA.
20
21 Author: Bastien Nocera <hadess@hadess.net>
22 */
23
24 #include "config.h"
25 #include "totem-playlist.h"
26
27 #include <stdlib.h>
28 #include <string.h>
29 #include <gtk/gtk.h>
30 #include <glib/gi18n.h>
31 #include <gdk/gdkkeysyms.h>
32 #include <gio/gio.h>
33
34 #include "totem-uri.h"
35 #include "totem-interface.h"
36
37 #define PL_LEN (gtk_tree_model_iter_n_children (playlist->priv->model, NULL))
38
39 static gboolean totem_playlist_add_one_mrl (TotemPlaylist *playlist,
40 const char *mrl,
41 const char *display_name,
42 const char *content_type,
43 const char *subtitle_uri,
44 gint64 starttime,
45 gboolean playing);
46
47 typedef gboolean (*ClearComparisonFunc) (TotemPlaylist *playlist, GtkTreeIter *iter, gconstpointer data);
48
49 static void totem_playlist_clear_with_compare (TotemPlaylist *playlist,
50 ClearComparisonFunc func,
51 gconstpointer data);
52
53 /* Callback function for GtkBuilder */
54 G_MODULE_EXPORT void totem_playlist_add_files (GtkWidget *widget, TotemPlaylist *playlist);
55 G_MODULE_EXPORT void playlist_remove_button_clicked (GtkWidget *button, TotemPlaylist *playlist);
56 G_MODULE_EXPORT void playlist_copy_location_action_callback (GtkWidget *button, TotemPlaylist *playlist);
57 G_MODULE_EXPORT void playlist_select_subtitle_action_callback (GtkWidget *button, TotemPlaylist *playlist);
58 G_MODULE_EXPORT void print_metadata_action_callback (GtkWidget *button, TotemPlaylist *playlist);
59
60
61 typedef struct {
62 TotemPlaylist *playlist;
63 TotemPlaylistForeachFunc callback;
64 gpointer user_data;
65 } PlaylistForeachContext;
66
67 struct TotemPlaylistPrivate
68 {
69 GtkWidget *treeview;
70 GtkTreeModel *model;
71 GtkTreePath *current;
72 GtkTreeSelection *selection;
73 TotemPlParser *parser;
74
75 /* Widgets */
76 GtkWidget *remove_button;
77
78 GSettings *settings;
79 GSettings *lockdown_settings;
80
81 /* This is a scratch list for when we're removing files */
82 GList *list;
83 guint current_to_be_removed : 1;
84
85 guint disable_save_to_disk : 1;
86
87 /* Repeat mode */
88 guint repeat : 1;
89 };
90
91 /* Signals */
92 enum {
93 CHANGED,
94 ITEM_ACTIVATED,
95 ACTIVE_NAME_CHANGED,
96 CURRENT_REMOVED,
97 SUBTITLE_CHANGED,
98 ITEM_ADDED,
99 ITEM_REMOVED,
100 LAST_SIGNAL
101 };
102
103 enum {
104 PLAYING_COL,
105 FILENAME_COL,
106 FILENAME_ESCAPED_COL,
107 URI_COL,
108 TITLE_CUSTOM_COL,
109 SUBTITLE_URI_COL,
110 FILE_MONITOR_COL,
111 MOUNT_COL,
112 MIME_TYPE_COL,
113 STARTTIME_COL,
114 NUM_COLS
115 };
116
117 enum {
118 PROP_0,
119 PROP_REPEAT
120 };
121
122 typedef struct {
123 const char *name;
124 const char *suffix;
125 TotemPlParserType type;
126 } PlaylistSaveType;
127
128 static int totem_playlist_table_signals[LAST_SIGNAL];
129
130 static void init_treeview (GtkWidget *treeview, TotemPlaylist *playlist);
131
132 #define totem_playlist_unset_playing(x) totem_playlist_set_playing(x, TOTEM_PLAYLIST_STATUS_NONE)
133
G_DEFINE_TYPE_WITH_PRIVATE(TotemPlaylist,totem_playlist,GTK_TYPE_BOX)134 G_DEFINE_TYPE_WITH_PRIVATE (TotemPlaylist, totem_playlist, GTK_TYPE_BOX)
135
136 /* Helper functions */
137 void
138 totem_playlist_select_subtitle_dialog(TotemPlaylist *playlist, TotemPlaylistSelectDialog mode)
139 {
140 char *subtitle, *current, *uri;
141 GFile *file, *dir;
142 TotemPlaylistStatus playing;
143 GtkTreeIter iter;
144
145 if (mode == TOTEM_PLAYLIST_DIALOG_PLAYING) {
146 /* Set subtitle file for the currently playing movie */
147 gtk_tree_model_get_iter (playlist->priv->model, &iter, playlist->priv->current);
148 } else if (mode == TOTEM_PLAYLIST_DIALOG_SELECTED) {
149 /* Set subtitle file in for the first selected playlist item */
150 GList *l;
151
152 l = gtk_tree_selection_get_selected_rows (playlist->priv->selection, NULL);
153 if (l == NULL)
154 return;
155 gtk_tree_model_get_iter (playlist->priv->model, &iter, l->data);
156 g_list_free_full (l, (GDestroyNotify) gtk_tree_path_free);
157 } else {
158 g_assert_not_reached ();
159 }
160
161 /* Look for the directory of the current movie */
162 gtk_tree_model_get (playlist->priv->model, &iter,
163 URI_COL, ¤t,
164 -1);
165
166 if (current == NULL)
167 return;
168
169 uri = NULL;
170 file = g_file_new_for_uri (current);
171 dir = g_file_get_parent (file);
172 g_object_unref (file);
173 if (dir != NULL) {
174 uri = g_file_get_uri (dir);
175 g_object_unref (dir);
176 }
177
178 subtitle = totem_add_subtitle (NULL, uri);
179 g_free (uri);
180
181 if (subtitle == NULL)
182 return;
183
184 gtk_tree_model_get (playlist->priv->model, &iter,
185 PLAYING_COL, &playing,
186 -1);
187
188 gtk_list_store_set (GTK_LIST_STORE(playlist->priv->model), &iter,
189 SUBTITLE_URI_COL, subtitle,
190 -1);
191
192 if (playing != TOTEM_PLAYLIST_STATUS_NONE) {
193 g_signal_emit (G_OBJECT (playlist),
194 totem_playlist_table_signals[SUBTITLE_CHANGED], 0,
195 NULL);
196 }
197
198 g_free(subtitle);
199 }
200
201 void
totem_playlist_set_current_subtitle(TotemPlaylist * playlist,const char * subtitle_uri)202 totem_playlist_set_current_subtitle (TotemPlaylist *playlist, const char *subtitle_uri)
203 {
204 GtkTreeIter iter;
205
206 if (playlist->priv->current == NULL)
207 return;
208
209 gtk_tree_model_get_iter (playlist->priv->model, &iter, playlist->priv->current);
210
211 gtk_list_store_set (GTK_LIST_STORE(playlist->priv->model), &iter,
212 SUBTITLE_URI_COL, subtitle_uri,
213 -1);
214
215 g_signal_emit (G_OBJECT (playlist),
216 totem_playlist_table_signals[SUBTITLE_CHANGED], 0,
217 NULL);
218 }
219
220 /* This one returns a new string, in UTF8 even if the MRL is encoded
221 * in the locale's encoding
222 */
223 static char *
totem_playlist_mrl_to_title(const gchar * mrl)224 totem_playlist_mrl_to_title (const gchar *mrl)
225 {
226 GFile *file;
227 char *filename_for_display, *unescaped;
228
229 if (g_str_has_prefix (mrl, "dvd://") != FALSE) {
230 /* This is "Title 3", where title is a DVD title
231 * Note: NOT a DVD chapter */
232 return g_strdup_printf (_("Title %d"), (int) g_strtod (mrl + 6, NULL));
233 } else if (g_str_has_prefix (mrl, "dvb://") != FALSE) {
234 /* This is "BBC ONE(BBC)" for "dvb://BBC ONE(BBC)" */
235 return g_strdup (mrl + 6);
236 }
237
238 file = g_file_new_for_uri (mrl);
239 unescaped = g_file_get_basename (file);
240 g_object_unref (file);
241
242 filename_for_display = g_filename_to_utf8 (unescaped,
243 -1, /* length */
244 NULL, /* bytes_read */
245 NULL, /* bytes_written */
246 NULL); /* error */
247
248 if (filename_for_display == NULL)
249 {
250 filename_for_display = g_locale_to_utf8 (unescaped,
251 -1, NULL, NULL, NULL);
252 if (filename_for_display == NULL) {
253 filename_for_display = g_filename_display_name
254 (unescaped);
255 }
256 g_free (unescaped);
257 return filename_for_display;
258 }
259
260 g_free (unescaped);
261
262 return filename_for_display;
263 }
264
265 static gboolean
totem_playlist_save_iter_foreach(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer user_data)266 totem_playlist_save_iter_foreach (GtkTreeModel *model,
267 GtkTreePath *path,
268 GtkTreeIter *iter,
269 gpointer user_data)
270 {
271 TotemPlPlaylist *playlist = user_data;
272 TotemPlPlaylistIter pl_iter;
273 gchar *uri, *title, *subtitle_uri, *mime_type, *starttime_str;
274 TotemPlaylistStatus status;
275 gboolean custom_title;
276 gint64 starttime;
277
278 gtk_tree_model_get (model, iter,
279 URI_COL, &uri,
280 FILENAME_COL, &title,
281 TITLE_CUSTOM_COL, &custom_title,
282 SUBTITLE_URI_COL, &subtitle_uri,
283 PLAYING_COL, &status,
284 MIME_TYPE_COL, &mime_type,
285 STARTTIME_COL, &starttime,
286 -1);
287
288 /* Prefer the current position for the starttime, if one is passed */
289 starttime_str = NULL;
290 if (status != TOTEM_PLAYLIST_STATUS_NONE) {
291 gint64 new_starttime;
292
293 new_starttime = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (playlist), "starttime"));
294 if (new_starttime != 0)
295 starttime = new_starttime;
296 }
297 if (starttime != 0)
298 starttime_str = g_strdup_printf ("%" G_GINT64_FORMAT, starttime);
299
300 totem_pl_playlist_append (playlist, &pl_iter);
301 totem_pl_playlist_set (playlist, &pl_iter,
302 TOTEM_PL_PARSER_FIELD_URI, uri,
303 TOTEM_PL_PARSER_FIELD_TITLE, (custom_title) ? title : NULL,
304 TOTEM_PL_PARSER_FIELD_SUBTITLE_URI, subtitle_uri,
305 TOTEM_PL_PARSER_FIELD_PLAYING, status != TOTEM_PLAYLIST_STATUS_NONE ? "true" : "",
306 TOTEM_PL_PARSER_FIELD_CONTENT_TYPE, mime_type,
307 TOTEM_PL_PARSER_FIELD_STARTTIME, starttime_str,
308 NULL);
309
310 g_free (uri);
311 g_free (title);
312 g_free (subtitle_uri);
313 g_free (mime_type);
314 g_free (starttime_str);
315
316 return FALSE;
317 }
318
319 static void
totem_playlist_save_session_playlist_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)320 totem_playlist_save_session_playlist_cb (GObject *source_object,
321 GAsyncResult *res,
322 gpointer user_data)
323 {
324 g_autoptr(GError) error = NULL;
325 gboolean ret;
326
327 ret = totem_pl_parser_save_finish (TOTEM_PL_PARSER (source_object),
328 res, &error);
329 if (!ret && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
330 g_warning ("Failed to save the session playlist: %s", error->message);
331 }
332
333 static void
totem_playlist_delete_session_playlist_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)334 totem_playlist_delete_session_playlist_cb (GObject *source_object,
335 GAsyncResult *res,
336 gpointer user_data)
337 {
338 g_autoptr(GError) error = NULL;
339 gboolean ret;
340
341 ret = g_file_delete_finish (G_FILE (source_object), res, &error);
342 if (!ret) {
343 if(!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
344 !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
345 g_warning ("Failed to delete session playlist: %s", error->message);
346 }
347 }
348
349 void
totem_playlist_save_session_playlist(TotemPlaylist * playlist,GFile * output,gint64 starttime)350 totem_playlist_save_session_playlist (TotemPlaylist *playlist,
351 GFile *output,
352 gint64 starttime)
353 {
354 g_autoptr(TotemPlPlaylist) pl_playlist = NULL;
355
356 if (playlist->priv->disable_save_to_disk) {
357 /* On lockdown, we do not touch the disk,
358 * even to remove the existing session */
359 return;
360 }
361 if (PL_LEN == 0) {
362 g_file_delete_async (output, 0, NULL, totem_playlist_delete_session_playlist_cb, NULL);
363 return;
364 }
365
366 pl_playlist = totem_pl_playlist_new ();
367
368 if (starttime > 0)
369 g_object_set_data (G_OBJECT (pl_playlist), "starttime", GINT_TO_POINTER (starttime));
370
371 gtk_tree_model_foreach (playlist->priv->model,
372 totem_playlist_save_iter_foreach,
373 pl_playlist);
374
375 totem_pl_parser_save_async (playlist->priv->parser,
376 pl_playlist,
377 output,
378 NULL,
379 TOTEM_PL_PARSER_XSPF,
380 NULL,
381 totem_playlist_save_session_playlist_cb,
382 NULL);
383 }
384
385 static void
gtk_tree_selection_has_selected_foreach(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer user_data)386 gtk_tree_selection_has_selected_foreach (GtkTreeModel *model,
387 GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
388 {
389 int *retval = (gboolean *)user_data;
390 *retval = TRUE;
391 }
392
393 static gboolean
gtk_tree_selection_has_selected(GtkTreeSelection * selection)394 gtk_tree_selection_has_selected (GtkTreeSelection *selection)
395 {
396 int retval, *boolean;
397
398 retval = FALSE;
399 boolean = &retval;
400 gtk_tree_selection_selected_foreach (selection,
401 gtk_tree_selection_has_selected_foreach,
402 (gpointer) (boolean));
403
404 return retval;
405 }
406
407 void
print_metadata_action_callback(GtkWidget * button,TotemPlaylist * playlist)408 print_metadata_action_callback (GtkWidget *button, TotemPlaylist *playlist)
409 {
410 GList *rows, *l;
411 guint i;
412
413 rows = gtk_tree_selection_get_selected_rows (playlist->priv->selection, NULL);
414 if (rows == NULL)
415 return;
416
417 i = 0;
418 for (l = rows; l != NULL; l = l->next) {
419 g_autofree char *url = NULL;
420 g_autofree char *sub_url = NULL;
421 gboolean playing;
422 GtkTreeIter iter;
423
424 gtk_tree_model_get_iter (playlist->priv->model, &iter, l->data);
425 gtk_tree_model_get (playlist->priv->model,
426 &iter,
427 PLAYING_COL, &playing,
428 URI_COL, &url,
429 SUBTITLE_URI_COL, &sub_url,
430 -1);
431
432 g_print ("Item #%d\n", i);
433 if (playing)
434 g_print ("\tPlaying\n");
435 g_print ("\tURI: %s\n", url);
436 if (sub_url)
437 g_print ("\tSubtitle URI: %s\n", sub_url);
438
439 gtk_tree_path_free (l->data);
440 i++;
441 }
442
443 g_list_free (rows);
444 }
445
446 void
playlist_select_subtitle_action_callback(GtkWidget * button,TotemPlaylist * playlist)447 playlist_select_subtitle_action_callback (GtkWidget *button, TotemPlaylist *playlist)
448 {
449 totem_playlist_select_subtitle_dialog (playlist, TOTEM_PLAYLIST_DIALOG_SELECTED);
450 }
451
452 void
playlist_copy_location_action_callback(GtkWidget * button,TotemPlaylist * playlist)453 playlist_copy_location_action_callback (GtkWidget *button, TotemPlaylist *playlist)
454 {
455 GList *l;
456 GtkClipboard *clip;
457 char *url;
458 GtkTreeIter iter;
459
460 l = gtk_tree_selection_get_selected_rows (playlist->priv->selection, NULL);
461 if (l == NULL)
462 return;
463
464 gtk_tree_model_get_iter (playlist->priv->model, &iter, l->data);
465 g_list_free_full (l, (GDestroyNotify) gtk_tree_path_free);
466
467 gtk_tree_model_get (playlist->priv->model,
468 &iter,
469 URI_COL, &url,
470 -1);
471
472 /* Set both the middle-click and the super-paste buffers */
473 clip = gtk_clipboard_get_for_display
474 (gdk_display_get_default(), GDK_SELECTION_CLIPBOARD);
475 gtk_clipboard_set_text (clip, url, -1);
476 clip = gtk_clipboard_get_for_display
477 (gdk_display_get_default(), GDK_SELECTION_PRIMARY);
478 gtk_clipboard_set_text (clip, url, -1);
479 g_free (url);
480
481 }
482
483 static void
selection_changed(GtkTreeSelection * treeselection,TotemPlaylist * playlist)484 selection_changed (GtkTreeSelection *treeselection, TotemPlaylist *playlist)
485 {
486 gboolean sensitivity;
487
488 if (gtk_tree_selection_has_selected (treeselection))
489 sensitivity = TRUE;
490 else
491 sensitivity = FALSE;
492
493 gtk_widget_set_sensitive (playlist->priv->remove_button, sensitivity);
494 }
495
496 /* This function checks if the current item is NULL, and try to update it
497 * as the first item of the playlist if so. It returns TRUE if there is a
498 * current item */
499 static gboolean
update_current_from_playlist(TotemPlaylist * playlist)500 update_current_from_playlist (TotemPlaylist *playlist)
501 {
502 int indice;
503
504 if (playlist->priv->current != NULL)
505 return TRUE;
506
507 if (PL_LEN != 0)
508 {
509 indice = 0;
510 playlist->priv->current = gtk_tree_path_new_from_indices (indice, -1);
511 } else {
512 return FALSE;
513 }
514
515 return TRUE;
516 }
517
518 void
totem_playlist_add_files(GtkWidget * widget,TotemPlaylist * playlist)519 totem_playlist_add_files (GtkWidget *widget, TotemPlaylist *playlist)
520 {
521 GSList *filenames, *l;
522 GList *mrl_list = NULL;
523
524 filenames = totem_add_files (NULL, NULL);
525 if (filenames == NULL)
526 return;
527
528 for (l = filenames; l != NULL; l = l->next) {
529 char *mrl = l->data;
530 mrl_list = g_list_prepend (mrl_list, totem_playlist_mrl_data_new (mrl, NULL));
531 g_free (mrl);
532 }
533
534 g_slist_free (filenames);
535
536 if (mrl_list != NULL)
537 totem_playlist_add_mrls (playlist, g_list_reverse (mrl_list), TRUE, NULL, NULL, NULL);
538 }
539
540 static void
totem_playlist_foreach_selected(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)541 totem_playlist_foreach_selected (GtkTreeModel *model, GtkTreePath *path,
542 GtkTreeIter *iter, gpointer data)
543 {
544 TotemPlaylist *playlist = (TotemPlaylist *)data;
545 GtkTreeRowReference *ref;
546
547 /* We can't use gtk_list_store_remove() here
548 * So we build a list a RowReferences */
549 ref = gtk_tree_row_reference_new (playlist->priv->model, path);
550 playlist->priv->list = g_list_prepend
551 (playlist->priv->list, (gpointer) ref);
552 if (playlist->priv->current_to_be_removed == FALSE
553 && playlist->priv->current != NULL
554 && gtk_tree_path_compare (path, playlist->priv->current) == 0)
555 playlist->priv->current_to_be_removed = TRUE;
556 }
557
558 static void
totem_playlist_emit_item_removed(TotemPlaylist * playlist,GtkTreeIter * iter)559 totem_playlist_emit_item_removed (TotemPlaylist *playlist,
560 GtkTreeIter *iter)
561 {
562 gchar *filename = NULL;
563 gchar *uri = NULL;
564
565 gtk_tree_model_get (playlist->priv->model, iter,
566 URI_COL, &uri, FILENAME_COL, &filename, -1);
567
568 g_signal_emit (playlist,
569 totem_playlist_table_signals[ITEM_REMOVED],
570 0, filename, uri);
571
572 g_free (filename);
573 g_free (uri);
574 }
575
576 static void
playlist_remove_files(TotemPlaylist * playlist)577 playlist_remove_files (TotemPlaylist *playlist)
578 {
579 totem_playlist_clear_with_compare (playlist, NULL, NULL);
580 }
581
582 void
playlist_remove_button_clicked(GtkWidget * button,TotemPlaylist * playlist)583 playlist_remove_button_clicked (GtkWidget *button, TotemPlaylist *playlist)
584 {
585 playlist_remove_files (playlist);
586 }
587
588 static int
totem_playlist_key_press(GtkWidget * win,GdkEventKey * event,TotemPlaylist * playlist)589 totem_playlist_key_press (GtkWidget *win, GdkEventKey *event, TotemPlaylist *playlist)
590 {
591 /* Special case some shortcuts */
592 if (event->state != 0) {
593 if ((event->state & GDK_CONTROL_MASK)
594 && event->keyval == GDK_KEY_a) {
595 gtk_tree_selection_select_all
596 (playlist->priv->selection);
597 return TRUE;
598 }
599 }
600
601 /* If we have modifiers, and either Ctrl, Mod1 (Alt), or any
602 * of Mod3 to Mod5 (Mod2 is num-lock...) are pressed, we
603 * let Gtk+ handle the key */
604 if (event->state != 0
605 && ((event->state & GDK_CONTROL_MASK)
606 || (event->state & GDK_MOD1_MASK)
607 || (event->state & GDK_MOD3_MASK)
608 || (event->state & GDK_MOD4_MASK)
609 || (event->state & GDK_MOD5_MASK)))
610 return FALSE;
611
612 if (event->keyval == GDK_KEY_Delete)
613 {
614 playlist_remove_files (playlist);
615 return TRUE;
616 }
617
618 return FALSE;
619 }
620
621 static void
set_playing_icon(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * model,GtkTreeIter * iter,TotemPlaylist * playlist)622 set_playing_icon (GtkTreeViewColumn *column, GtkCellRenderer *renderer,
623 GtkTreeModel *model, GtkTreeIter *iter, TotemPlaylist *playlist)
624 {
625 TotemPlaylistStatus playing;
626 const char *icon_name;
627
628 gtk_tree_model_get (model, iter, PLAYING_COL, &playing, -1);
629
630 switch (playing) {
631 case TOTEM_PLAYLIST_STATUS_PLAYING:
632 icon_name = "media-playback-start-symbolic";
633 break;
634 case TOTEM_PLAYLIST_STATUS_PAUSED:
635 icon_name = "media-playback-pause-symbolic";
636 break;
637 case TOTEM_PLAYLIST_STATUS_NONE:
638 default:
639 icon_name = NULL;
640 }
641
642 g_object_set (renderer, "icon-name", icon_name, NULL);
643 }
644
645 static void
init_columns(GtkTreeView * treeview,TotemPlaylist * playlist)646 init_columns (GtkTreeView *treeview, TotemPlaylist *playlist)
647 {
648 GtkCellRenderer *renderer;
649 GtkTreeViewColumn *column;
650
651 /* Playing pix */
652 renderer = gtk_cell_renderer_pixbuf_new ();
653 column = gtk_tree_view_column_new ();
654 g_object_set (G_OBJECT (column), "title", "Playlist", NULL);
655 gtk_tree_view_column_pack_start (column, renderer, FALSE);
656 gtk_tree_view_column_set_cell_data_func (column, renderer,
657 (GtkTreeCellDataFunc) set_playing_icon, playlist, NULL);
658 g_object_set (renderer, "stock-size", GTK_ICON_SIZE_MENU, NULL);
659 gtk_tree_view_append_column (treeview, column);
660
661 /* Labels */
662 renderer = gtk_cell_renderer_text_new ();
663 gtk_tree_view_column_pack_start (column, renderer, TRUE);
664 gtk_tree_view_column_set_attributes (column, renderer,
665 "text", FILENAME_COL, NULL);
666 }
667
668 static void
treeview_row_changed(GtkTreeView * treeview,GtkTreePath * arg1,GtkTreeViewColumn * arg2,TotemPlaylist * playlist)669 treeview_row_changed (GtkTreeView *treeview, GtkTreePath *arg1,
670 GtkTreeViewColumn *arg2, TotemPlaylist *playlist)
671 {
672 if (gtk_tree_path_compare (arg1, playlist->priv->current) == 0) {
673 g_signal_emit (G_OBJECT (playlist),
674 totem_playlist_table_signals[ITEM_ACTIVATED], 0,
675 NULL);
676 return;
677 }
678
679 if (playlist->priv->current != NULL) {
680 totem_playlist_unset_playing (playlist);
681 gtk_tree_path_free (playlist->priv->current);
682 }
683
684 playlist->priv->current = gtk_tree_path_copy (arg1);
685
686 g_signal_emit (G_OBJECT (playlist),
687 totem_playlist_table_signals[CHANGED], 0,
688 NULL);
689 }
690
691 static gboolean
search_equal_is_match(const gchar * s,const gchar * lc_key)692 search_equal_is_match (const gchar * s, const gchar * lc_key)
693 {
694 gboolean match = FALSE;
695
696 if (s != NULL) {
697 gchar *lc_s;
698
699 /* maybe also normalize both strings? */
700 lc_s = g_utf8_strdown (s, -1);
701 match = (lc_s != NULL && strstr (lc_s, lc_key) != NULL);
702 g_free (lc_s);
703 }
704
705 return match;
706 }
707
708 static gboolean
search_equal_func(GtkTreeModel * model,gint col,const gchar * key,GtkTreeIter * iter,gpointer userdata)709 search_equal_func (GtkTreeModel *model, gint col, const gchar *key,
710 GtkTreeIter *iter, gpointer userdata)
711 {
712 gboolean match;
713 gchar *lc_key, *fn = NULL;
714
715 lc_key = g_utf8_strdown (key, -1);
716
717 /* type-ahead search: first check display filename / title, then URI */
718 gtk_tree_model_get (model, iter, FILENAME_COL, &fn, -1);
719 match = search_equal_is_match (fn, lc_key);
720 g_free (fn);
721
722 if (!match) {
723 gchar *uri = NULL;
724
725 gtk_tree_model_get (model, iter, URI_COL, &uri, -1);
726 fn = g_filename_from_uri (uri, NULL, NULL);
727 match = search_equal_is_match (fn, lc_key);
728 g_free (fn);
729 g_free (uri);
730 }
731
732 g_free (lc_key);
733 return !match; /* needs to return FALSE if row matches */
734 }
735
736 static void
init_treeview(GtkWidget * treeview,TotemPlaylist * playlist)737 init_treeview (GtkWidget *treeview, TotemPlaylist *playlist)
738 {
739 GtkTreeSelection *selection;
740
741 init_columns (GTK_TREE_VIEW (treeview), playlist);
742
743 selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));
744 gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
745 g_signal_connect (G_OBJECT (selection), "changed",
746 G_CALLBACK (selection_changed), playlist);
747 g_signal_connect (G_OBJECT (treeview), "row-activated",
748 G_CALLBACK (treeview_row_changed), playlist);
749
750 playlist->priv->selection = selection;
751
752 /* make type-ahead search work in the playlist */
753 gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW (treeview),
754 search_equal_func, NULL, NULL);
755
756 gtk_widget_show (treeview);
757 }
758
759 static void
update_repeat_cb(GSettings * settings,const gchar * key,TotemPlaylist * playlist)760 update_repeat_cb (GSettings *settings, const gchar *key, TotemPlaylist *playlist)
761 {
762 playlist->priv->repeat = g_settings_get_boolean (settings, "repeat");
763
764 g_signal_emit (G_OBJECT (playlist),
765 totem_playlist_table_signals[CHANGED], 0,
766 NULL);
767 g_object_notify (G_OBJECT (playlist), "repeat");
768 }
769
770 static void
update_lockdown_cb(GSettings * settings,const gchar * key,TotemPlaylist * playlist)771 update_lockdown_cb (GSettings *settings, const gchar *key, TotemPlaylist *playlist)
772 {
773 playlist->priv->disable_save_to_disk = g_settings_get_boolean (settings, "disable-save-to-disk");
774 }
775
776 static void
init_config(TotemPlaylist * playlist)777 init_config (TotemPlaylist *playlist)
778 {
779 playlist->priv->settings = g_settings_new (TOTEM_GSETTINGS_SCHEMA);
780 playlist->priv->lockdown_settings = g_settings_new ("org.gnome.desktop.lockdown");
781
782 playlist->priv->disable_save_to_disk = g_settings_get_boolean (playlist->priv->lockdown_settings, "disable-save-to-disk");
783
784 g_signal_connect (playlist->priv->lockdown_settings, "changed::disable-save-to-disk",
785 G_CALLBACK (update_lockdown_cb), playlist);
786
787 playlist->priv->repeat = g_settings_get_boolean (playlist->priv->settings, "repeat");
788
789 g_signal_connect (playlist->priv->settings, "changed::repeat", (GCallback) update_repeat_cb, playlist);
790 }
791
792 static gboolean
parse_bool_str(const char * str)793 parse_bool_str (const char *str)
794 {
795 if (str == NULL)
796 return FALSE;
797 if (g_ascii_strcasecmp (str, "true") == 0)
798 return TRUE;
799 if (g_ascii_strcasecmp (str, "false") == 0)
800 return FALSE;
801 return atoi (str);
802 }
803
804 static void
totem_playlist_entry_parsed(TotemPlParser * parser,const char * uri,GHashTable * metadata,TotemPlaylist * playlist)805 totem_playlist_entry_parsed (TotemPlParser *parser,
806 const char *uri,
807 GHashTable *metadata,
808 TotemPlaylist *playlist)
809 {
810 const char *title, *content_type, *subtitle_uri, *starttime_str;
811 gint64 duration, starttime;
812 gboolean playing;
813
814 /* We ignore 0-length items in playlists, they're usually just banners */
815 duration = totem_pl_parser_parse_duration
816 (g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_DURATION), FALSE);
817 if (duration == 0)
818 return;
819 title = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_TITLE);
820 content_type = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_CONTENT_TYPE);
821 playing = parse_bool_str (g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_PLAYING));
822 subtitle_uri = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_SUBTITLE_URI);
823 starttime_str = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_STARTTIME);
824 starttime = totem_pl_parser_parse_duration (starttime_str, FALSE);
825 starttime = MAX (starttime, 0);
826 totem_playlist_add_one_mrl (playlist, uri, title, content_type, subtitle_uri, starttime, playing);
827 }
828
829 static gboolean
totem_playlist_compare_with_monitor(TotemPlaylist * playlist,GtkTreeIter * iter,gconstpointer data)830 totem_playlist_compare_with_monitor (TotemPlaylist *playlist, GtkTreeIter *iter, gconstpointer data)
831 {
832 GFileMonitor *monitor = (GFileMonitor *) data;
833 GFileMonitor *_monitor;
834 gboolean retval = FALSE;
835
836 gtk_tree_model_get (playlist->priv->model, iter,
837 FILE_MONITOR_COL, &_monitor, -1);
838
839 if (_monitor == monitor)
840 retval = TRUE;
841
842 if (_monitor != NULL)
843 g_object_unref (_monitor);
844
845 return retval;
846 }
847
848 static void
totem_playlist_file_changed(GFileMonitor * monitor,GFile * file,GFile * other_file,GFileMonitorEvent event_type,TotemPlaylist * playlist)849 totem_playlist_file_changed (GFileMonitor *monitor,
850 GFile *file,
851 GFile *other_file,
852 GFileMonitorEvent event_type,
853 TotemPlaylist *playlist)
854 {
855 if (event_type == G_FILE_MONITOR_EVENT_PRE_UNMOUNT ||
856 event_type == G_FILE_MONITOR_EVENT_UNMOUNTED) {
857 totem_playlist_clear_with_compare (playlist,
858 (ClearComparisonFunc) totem_playlist_compare_with_monitor,
859 monitor);
860 }
861 }
862
863 static void
totem_playlist_dispose(GObject * object)864 totem_playlist_dispose (GObject *object)
865 {
866 TotemPlaylist *playlist = TOTEM_PLAYLIST (object);
867
868 g_clear_object (&playlist->priv->parser);
869 g_clear_object (&playlist->priv->settings);
870 g_clear_object (&playlist->priv->lockdown_settings);
871 g_clear_pointer (&playlist->priv->current, gtk_tree_path_free);
872
873 G_OBJECT_CLASS (totem_playlist_parent_class)->dispose (object);
874 }
875
876 static void
totem_playlist_init(TotemPlaylist * playlist)877 totem_playlist_init (TotemPlaylist *playlist)
878 {
879 GtkWidget *container;
880 GtkBuilder *xml;
881 GtkWidget *widget;
882 GtkStyleContext *context;
883
884 gtk_orientable_set_orientation (GTK_ORIENTABLE (playlist),
885 GTK_ORIENTATION_VERTICAL);
886
887 playlist->priv = totem_playlist_get_instance_private (playlist);
888 playlist->priv->parser = totem_pl_parser_new ();
889
890 totem_pl_parser_add_ignored_scheme (playlist->priv->parser, "dvd:");
891 totem_pl_parser_add_ignored_scheme (playlist->priv->parser, "vcd:");
892 totem_pl_parser_add_ignored_scheme (playlist->priv->parser, "cd:");
893 totem_pl_parser_add_ignored_scheme (playlist->priv->parser, "dvb:");
894 totem_pl_parser_add_ignored_mimetype (playlist->priv->parser, "application/x-trash");
895 totem_pl_parser_add_ignored_mimetype (playlist->priv->parser, "text/html");
896 totem_pl_parser_add_ignored_mimetype (playlist->priv->parser, "application/x-ms-dos-executable");
897 totem_pl_parser_add_ignored_glob (playlist->priv->parser, "*.htm");
898 totem_pl_parser_add_ignored_glob (playlist->priv->parser, "*.html");
899 totem_pl_parser_add_ignored_glob (playlist->priv->parser, "*.nfo");
900 totem_pl_parser_add_ignored_glob (playlist->priv->parser, "*.txt");
901 totem_pl_parser_add_ignored_glob (playlist->priv->parser, "*.exe");
902
903 g_signal_connect (G_OBJECT (playlist->priv->parser),
904 "entry-parsed",
905 G_CALLBACK (totem_playlist_entry_parsed),
906 playlist);
907
908 xml = totem_interface_load ("playlist.ui", TRUE, NULL, playlist);
909
910 if (xml == NULL)
911 return;
912
913 gtk_widget_add_events (GTK_WIDGET (playlist), GDK_KEY_PRESS_MASK);
914 g_signal_connect (G_OBJECT (playlist), "key_press_event",
915 G_CALLBACK (totem_playlist_key_press), playlist);
916
917 /* Buttons */
918 playlist->priv->remove_button = GTK_WIDGET (gtk_builder_get_object (xml, "remove_button"));
919
920 /* Join treeview and buttons */
921 widget = GTK_WIDGET (gtk_builder_get_object (xml, ("scrolledwindow1")));
922 context = gtk_widget_get_style_context (widget);
923 gtk_style_context_set_junction_sides (context, GTK_JUNCTION_BOTTOM);
924 widget = GTK_WIDGET (gtk_builder_get_object (xml, ("toolbar1")));
925 context = gtk_widget_get_style_context (widget);
926 gtk_style_context_set_junction_sides (context, GTK_JUNCTION_TOP);
927
928 /* Reparent the vbox */
929 container = GTK_WIDGET (gtk_builder_get_object (xml, "vbox4"));
930 g_object_ref (container);
931 gtk_box_pack_start (GTK_BOX (playlist),
932 container,
933 TRUE, /* expand */
934 TRUE, /* fill */
935 0); /* padding */
936 g_object_unref (container);
937
938 playlist->priv->treeview = GTK_WIDGET (gtk_builder_get_object (xml, "treeview1"));
939 init_treeview (playlist->priv->treeview, playlist);
940 playlist->priv->model = gtk_tree_view_get_model
941 (GTK_TREE_VIEW (playlist->priv->treeview));
942
943 /* tooltips */
944 gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(playlist->priv->treeview),
945 FILENAME_ESCAPED_COL);
946
947 /* The configuration */
948 init_config (playlist);
949
950 gtk_widget_show_all (GTK_WIDGET (playlist));
951
952 g_object_unref (xml);
953 }
954
955 GtkWidget*
totem_playlist_new(void)956 totem_playlist_new (void)
957 {
958 return GTK_WIDGET (g_object_new (TOTEM_TYPE_PLAYLIST, NULL));
959 }
960
961 static gboolean
totem_playlist_add_one_mrl(TotemPlaylist * playlist,const char * mrl,const char * display_name,const char * content_type,const char * subtitle_uri,gint64 starttime,gboolean playing)962 totem_playlist_add_one_mrl (TotemPlaylist *playlist,
963 const char *mrl,
964 const char *display_name,
965 const char *content_type,
966 const char *subtitle_uri,
967 gint64 starttime,
968 gboolean playing)
969 {
970 GtkListStore *store;
971 GtkTreeIter iter;
972 char *filename_for_display, *uri, *escaped_filename;
973 GFileMonitor *monitor;
974 GMount *mount;
975 GFile *file;
976
977 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
978 g_return_val_if_fail (mrl != NULL, FALSE);
979
980 if (display_name == NULL || *display_name == '\0')
981 filename_for_display = totem_playlist_mrl_to_title (mrl);
982 else
983 filename_for_display = g_strdup (display_name);
984
985 uri = totem_create_full_path (mrl);
986
987 g_debug ("totem_playlist_add_one_mrl (): %s %s %s %s %"G_GINT64_FORMAT " %s\n", filename_for_display, uri, display_name, subtitle_uri, starttime, playing ? "true" : "false");
988
989 store = GTK_LIST_STORE (playlist->priv->model);
990
991 /* Get the file monitor */
992 file = g_file_new_for_uri (uri ? uri : mrl);
993 if (g_file_is_native (file) != FALSE) {
994 monitor = g_file_monitor_file (file,
995 G_FILE_MONITOR_NONE,
996 NULL,
997 NULL);
998 g_signal_connect (G_OBJECT (monitor),
999 "changed",
1000 G_CALLBACK (totem_playlist_file_changed),
1001 playlist);
1002 mount = NULL;
1003 } else {
1004 mount = totem_get_mount_for_media (uri ? uri : mrl);
1005 monitor = NULL;
1006 }
1007
1008 escaped_filename = g_markup_escape_text (filename_for_display, -1);
1009 gtk_list_store_insert_with_values (store, &iter, -1,
1010 PLAYING_COL, playing ? TOTEM_PLAYLIST_STATUS_PAUSED : TOTEM_PLAYLIST_STATUS_NONE,
1011 FILENAME_COL, filename_for_display,
1012 FILENAME_ESCAPED_COL, escaped_filename,
1013 URI_COL, uri ? uri : mrl,
1014 SUBTITLE_URI_COL, subtitle_uri,
1015 TITLE_CUSTOM_COL, display_name ? TRUE : FALSE,
1016 FILE_MONITOR_COL, monitor,
1017 MOUNT_COL, mount,
1018 MIME_TYPE_COL, content_type,
1019 STARTTIME_COL, starttime,
1020 -1);
1021 g_free (escaped_filename);
1022
1023 g_signal_emit (playlist,
1024 totem_playlist_table_signals[ITEM_ADDED],
1025 0, filename_for_display, uri ? uri : mrl);
1026
1027 g_free (filename_for_display);
1028 g_free (uri);
1029
1030 if (playlist->priv->current == NULL)
1031 playlist->priv->current = gtk_tree_model_get_path (playlist->priv->model, &iter);
1032
1033 g_signal_emit (G_OBJECT (playlist),
1034 totem_playlist_table_signals[CHANGED], 0,
1035 NULL);
1036
1037 return TRUE;
1038 }
1039
1040 typedef struct {
1041 GAsyncReadyCallback callback;
1042 gpointer user_data;
1043 gboolean cursor;
1044 TotemPlaylist *playlist;
1045 gchar *mrl;
1046 gchar *display_name;
1047 } AddMrlData;
1048
1049 static void
add_mrl_data_free(AddMrlData * data)1050 add_mrl_data_free (AddMrlData *data)
1051 {
1052 g_object_unref (data->playlist);
1053 g_free (data->mrl);
1054 g_free (data->display_name);
1055 g_slice_free (AddMrlData, data);
1056 }
1057
1058 static gboolean
handle_parse_result(TotemPlParserResult res,TotemPlaylist * playlist,const gchar * mrl,const gchar * display_name,GError ** error)1059 handle_parse_result (TotemPlParserResult res, TotemPlaylist *playlist, const gchar *mrl, const gchar *display_name, GError **error)
1060 {
1061 if (res == TOTEM_PL_PARSER_RESULT_UNHANDLED)
1062 return totem_playlist_add_one_mrl (playlist, mrl, display_name, NULL, NULL, 0, FALSE);
1063 if (res == TOTEM_PL_PARSER_RESULT_ERROR) {
1064 g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
1065 _("The playlist “%s” could not be parsed. It might be damaged."), display_name ? display_name : mrl);
1066
1067 return FALSE;
1068 }
1069 if (res == TOTEM_PL_PARSER_RESULT_IGNORED)
1070 return FALSE;
1071
1072 return TRUE;
1073 }
1074
1075 static void
add_mrl_cb(TotemPlParser * parser,GAsyncResult * result,AddMrlData * data)1076 add_mrl_cb (TotemPlParser *parser, GAsyncResult *result, AddMrlData *data)
1077 {
1078 TotemPlParserResult res;
1079 GSimpleAsyncResult *async_result;
1080 GError *error = NULL;
1081 gboolean ret;
1082
1083 g_assert (data != NULL);
1084
1085 /* Finish parsing the playlist */
1086 res = totem_pl_parser_parse_finish (parser, result, NULL);
1087
1088 /* Remove the cursor, if one was set */
1089 if (data->cursor)
1090 g_application_unmark_busy (g_application_get_default ());
1091
1092 /* Create an async result which will return the result to the code which called totem_playlist_add_mrl() */
1093 ret = handle_parse_result (res, data->playlist, data->mrl, data->display_name, &error);
1094 async_result = g_simple_async_result_new (G_OBJECT (data->playlist), data->callback, data->user_data, totem_playlist_add_mrl);
1095 if (error != NULL)
1096 g_simple_async_result_take_error (async_result, error);
1097
1098 /* Handle the various return cases from the playlist parser */
1099 g_simple_async_result_set_op_res_gboolean (async_result, ret);
1100
1101 /* Free the closure's data, now that we're finished with it */
1102 add_mrl_data_free (data);
1103
1104 /* Synchronously call the calling code's callback function (i.e. what was passed to totem_playlist_add_mrl()'s @callback parameter)
1105 * in the main thread to return the result */
1106 g_simple_async_result_complete (async_result);
1107 }
1108
1109 void
totem_playlist_add_mrl(TotemPlaylist * playlist,const char * mrl,const char * display_name,gboolean cursor,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)1110 totem_playlist_add_mrl (TotemPlaylist *playlist, const char *mrl, const char *display_name, gboolean cursor,
1111 GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data)
1112 {
1113 AddMrlData *data;
1114
1115 g_return_if_fail (mrl != NULL);
1116
1117 /* Display a waiting cursor if required */
1118 if (cursor)
1119 g_application_mark_busy (g_application_get_default ());
1120
1121 /* Build the data struct to pass to the callback function */
1122 data = g_slice_new (AddMrlData);
1123 data->callback = callback;
1124 data->user_data = user_data;
1125 data->cursor = cursor;
1126 data->playlist = g_object_ref (playlist);
1127 data->mrl = g_strdup (mrl);
1128 data->display_name = g_strdup (display_name);
1129
1130 /* Start parsing the playlist. Once this is complete, add_mrl_cb() is called, which will interpret the results and call @callback to
1131 * finish the process. */
1132 totem_pl_parser_parse_async (playlist->priv->parser, mrl, FALSE, cancellable, (GAsyncReadyCallback) add_mrl_cb, data);
1133 }
1134
1135 gboolean
totem_playlist_add_mrl_finish(TotemPlaylist * playlist,GAsyncResult * result,GError ** error)1136 totem_playlist_add_mrl_finish (TotemPlaylist *playlist, GAsyncResult *result, GError **error)
1137 {
1138 g_assert (g_simple_async_result_get_source_tag (G_SIMPLE_ASYNC_RESULT (result)) == totem_playlist_add_mrl);
1139
1140 if (g_simple_async_result_get_op_res_gboolean (G_SIMPLE_ASYNC_RESULT (result)))
1141 return TRUE;
1142 g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error);
1143 return FALSE;
1144 }
1145
1146 gboolean
totem_playlist_add_mrl_sync(TotemPlaylist * playlist,const char * mrl)1147 totem_playlist_add_mrl_sync (TotemPlaylist *playlist,
1148 const char *mrl)
1149 {
1150 GtkTreeIter iter;
1151 gboolean ret;
1152
1153 g_return_val_if_fail (mrl != NULL, FALSE);
1154
1155 ret = handle_parse_result (totem_pl_parser_parse (playlist->priv->parser, mrl, FALSE), playlist, mrl, NULL, NULL);
1156 if (!ret)
1157 return ret;
1158
1159 /* Find the currently playing track, and set ->current */
1160 ret = gtk_tree_model_get_iter_first (playlist->priv->model, &iter);
1161 while (ret) {
1162 TotemPlaylistStatus status;
1163
1164 gtk_tree_model_get (playlist->priv->model, &iter,
1165 PLAYING_COL, &status,
1166 -1);
1167 if (status == TOTEM_PLAYLIST_STATUS_PAUSED) {
1168 gtk_tree_path_free (playlist->priv->current);
1169 playlist->priv->current = gtk_tree_model_get_path (playlist->priv->model, &iter);
1170 break;
1171 }
1172 ret = gtk_tree_model_iter_next (playlist->priv->model, &iter);
1173 }
1174
1175 return TRUE;
1176 }
1177
1178 typedef struct {
1179 TotemPlaylist *playlist;
1180 GList *mrls; /* list of TotemPlaylistMrlDatas */
1181 gboolean cursor;
1182 GAsyncReadyCallback callback;
1183 gpointer user_data;
1184
1185 guint next_index_to_add;
1186 GList *unadded_entries; /* list of TotemPlaylistMrlDatas */
1187 volatile gint entries_remaining;
1188 } AddMrlsOperationData;
1189
1190 static void
add_mrls_operation_data_free(AddMrlsOperationData * data)1191 add_mrls_operation_data_free (AddMrlsOperationData *data)
1192 {
1193 /* Remove the cursor, if one was set */
1194 if (data->cursor)
1195 g_application_unmark_busy (g_application_get_default ());
1196
1197 g_list_free_full (data->mrls, (GDestroyNotify) totem_playlist_mrl_data_free);
1198 g_object_unref (data->playlist);
1199
1200 g_slice_free (AddMrlsOperationData, data);
1201 }
1202
1203 struct TotemPlaylistMrlData {
1204 gchar *mrl;
1205 gchar *display_name;
1206 TotemPlParserResult res;
1207
1208 /* Implementation details */
1209 AddMrlsOperationData *operation_data;
1210 guint index;
1211 };
1212
1213 /**
1214 * totem_playlist_mrl_data_new:
1215 * @mrl: a MRL
1216 * @display_name: (allow-none): a human-readable display name for the MRL, or %NULL
1217 *
1218 * Create a new #TotemPlaylistMrlData struct storing the given @mrl and @display_name.
1219 *
1220 * This will typically be immediately appended to a #GList to be passed to totem_playlist_add_mrls().
1221 *
1222 * Return value: (transfer full): a new #TotemPlaylistMrlData; free with totem_playlist_mrl_data_free()
1223 *
1224 * Since: 3.0
1225 */
1226 TotemPlaylistMrlData *
totem_playlist_mrl_data_new(const gchar * mrl,const gchar * display_name)1227 totem_playlist_mrl_data_new (const gchar *mrl,
1228 const gchar *display_name)
1229 {
1230 TotemPlaylistMrlData *data;
1231
1232 g_return_val_if_fail (mrl != NULL && *mrl != '\0', NULL);
1233
1234 data = g_slice_new (TotemPlaylistMrlData);
1235 data->mrl = g_strdup (mrl);
1236 data->display_name = g_strdup (display_name);
1237
1238 return data;
1239 }
1240
1241 /**
1242 * totem_playlist_mrl_data_free:
1243 * @data: (transfer full): a #TotemPlaylistMrlData
1244 *
1245 * Free the given #TotemPlaylistMrlData struct. This should not generally be called by code outside #TotemPlaylist.
1246 *
1247 * Since: 3.0
1248 */
1249 void
totem_playlist_mrl_data_free(TotemPlaylistMrlData * data)1250 totem_playlist_mrl_data_free (TotemPlaylistMrlData *data)
1251 {
1252 g_return_if_fail (data != NULL);
1253
1254 /* NOTE: This doesn't call add_mrls_operation_data_free() on @data->operation_data, since it's shared with other instances of
1255 * TotemPlaylistMrlData, and not truly reference counted. */
1256 g_free (data->display_name);
1257 g_free (data->mrl);
1258
1259 g_slice_free (TotemPlaylistMrlData, data);
1260 }
1261
1262 static void
add_mrls_finish_operation(AddMrlsOperationData * operation_data)1263 add_mrls_finish_operation (AddMrlsOperationData *operation_data)
1264 {
1265 /* Check whether this is the final callback invocation; iff it is, we can call the user's callback for the entire operation and free the
1266 * operation data */
1267 if (g_atomic_int_dec_and_test (&(operation_data->entries_remaining)) == TRUE) {
1268 GSimpleAsyncResult *async_result;
1269
1270 async_result = g_simple_async_result_new (G_OBJECT (operation_data->playlist), operation_data->callback, operation_data->user_data,
1271 totem_playlist_add_mrls);
1272 g_simple_async_result_complete (async_result);
1273 g_object_unref (async_result);
1274
1275 add_mrls_operation_data_free (operation_data);
1276 }
1277 }
1278
1279 /* Called exactly once for each MRL in a totem_playlist_add_mrls() operation. Called in the thread running the main loop. If the MRL which has just
1280 * been parsed is the next one in the sequence (of entries in @mrls as passed to totem_playlist_add_mrls()), it's added to the playlist proper.
1281 * Otherwise, it's added to a sorted queue of MRLs which have had their callbacks called out of order.
1282 * When a MRL is added to the playlist proper, any successor MRLs which are in the sorted queue are also added to the playlist proper.
1283 * When add_mrls_cb() is called for the last time for a given call to totem_playlist_add_mrls(), it calls the user's callback for the operation
1284 * (passed as @callback to totem_playlist_add_mrls()) and frees the #AddMrlsOperationData struct. This is handled by add_mrls_finish_operation().
1285 * The #TotemPlaylistMrlData for each MRL is freed by add_mrls_operation_data_free() at the end of the entire operation. */
1286 static void
add_mrls_cb(TotemPlParser * parser,GAsyncResult * result,TotemPlaylistMrlData * mrl_data)1287 add_mrls_cb (TotemPlParser *parser, GAsyncResult *result, TotemPlaylistMrlData *mrl_data)
1288 {
1289 AddMrlsOperationData *operation_data = mrl_data->operation_data;
1290
1291 /* Finish parsing the playlist */
1292 mrl_data->res = totem_pl_parser_parse_finish (parser, result, NULL);
1293
1294 g_assert (mrl_data->index >= operation_data->next_index_to_add);
1295
1296 if (mrl_data->index == operation_data->next_index_to_add) {
1297 GList *i;
1298
1299 /* The entry is the next one in the order, so doesn't need to be added to the unadded list, and can be added to playlist proper */
1300 operation_data->next_index_to_add++;
1301 handle_parse_result (mrl_data->res, operation_data->playlist, mrl_data->mrl, mrl_data->display_name, NULL);
1302
1303 /* See if we can now add any other entries which have already been processed */
1304 for (i = operation_data->unadded_entries;
1305 i != NULL && ((TotemPlaylistMrlData*) i->data)->index == operation_data->next_index_to_add;
1306 i = g_list_delete_link (i, i)) {
1307 TotemPlaylistMrlData *_mrl_data = (TotemPlaylistMrlData*) i->data;
1308
1309 operation_data->next_index_to_add++;
1310 handle_parse_result (_mrl_data->res, operation_data->playlist, _mrl_data->mrl, _mrl_data->display_name, NULL);
1311 }
1312
1313 operation_data->unadded_entries = i;
1314 } else {
1315 GList *i;
1316
1317 /* The entry has been parsed out of order, so needs to be added (in the correct position) to the unadded list for latter addition to
1318 * the playlist proper */
1319 for (i = operation_data->unadded_entries; i != NULL && mrl_data->index > ((TotemPlaylistMrlData*) i->data)->index; i = i->next);
1320 operation_data->unadded_entries = g_list_insert_before (operation_data->unadded_entries, i, mrl_data);
1321 }
1322
1323 /* Check whether this is the last callback; call the user's callback for the entire operation and free the operation data if appropriate */
1324 add_mrls_finish_operation (operation_data);
1325 }
1326
1327 /**
1328 * totem_playlist_add_mrls:
1329 * @self: a #TotemPlaylist
1330 * @mrls: (element-type TotemPlaylistMrlData) (transfer full): a list of #TotemPlaylistMrlData structs
1331 * @cursor: %TRUE to set a waiting cursor on the playlist for the duration of the operation, %FALSE otherwise
1332 * @cancellable: (allow-none): a #Cancellable, or %NULL
1333 * @callback: (scope async) (allow-none): callback to call once all the MRLs have been added to the playlist, or %NULL
1334 * @user_data: (closure) (allow-none): user data to pass to @callback, or %NULL
1335 *
1336 * Add the MRLs listed in @mrls to the playlist asynchronously, and ensuring that they're added to the playlist in the order they appear in the
1337 * input #GList.
1338 *
1339 * @mrls should be a #GList of #TotemPlaylistMrlData structs, each created with totem_playlist_mrl_data_new(). This function takes ownership of both
1340 * the list and its elements when called, so don't free either after calling totem_playlist_add_mrls().
1341 *
1342 * @callback will be called after all the MRLs in @mrls have been parsed and (if they were parsed successfully) added to the playlist. In the
1343 * callback function, totem_playlist_add_mrls_finish() should be called to check for errors.
1344 *
1345 * Since: 3.0
1346 */
1347 void
totem_playlist_add_mrls(TotemPlaylist * self,GList * mrls,gboolean cursor,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)1348 totem_playlist_add_mrls (TotemPlaylist *self,
1349 GList *mrls,
1350 gboolean cursor,
1351 GCancellable *cancellable,
1352 GAsyncReadyCallback callback,
1353 gpointer user_data)
1354 {
1355 AddMrlsOperationData *operation_data;
1356 GList *i;
1357 guint mrl_index = 0;
1358
1359 g_return_if_fail (TOTEM_IS_PLAYLIST (self));
1360 g_return_if_fail (mrls != NULL);
1361 g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
1362
1363 /* Build the data struct to pass to the callback function */
1364 operation_data = g_slice_new (AddMrlsOperationData);
1365 operation_data->playlist = g_object_ref (self);
1366 operation_data->mrls = mrls;
1367 operation_data->cursor = cursor;
1368 operation_data->callback = callback;
1369 operation_data->user_data = user_data;
1370 operation_data->next_index_to_add = mrl_index;
1371 operation_data->unadded_entries = NULL;
1372 g_atomic_int_set (&(operation_data->entries_remaining), 1);
1373
1374 /* Display a waiting cursor if required */
1375 if (cursor)
1376 g_application_mark_busy (g_application_get_default ());
1377
1378 for (i = mrls; i != NULL; i = i->next) {
1379 TotemPlaylistMrlData *mrl_data = (TotemPlaylistMrlData*) i->data;
1380
1381 if (mrl_data == NULL)
1382 continue;
1383
1384 /* Set the item's parsing index, so that it's inserted into the playlist in the position it appeared in @mrls */
1385 mrl_data->operation_data = operation_data;
1386 mrl_data->index = mrl_index++;
1387
1388 g_atomic_int_inc (&(operation_data->entries_remaining));
1389
1390 /* Start parsing the playlist. Once this is complete, add_mrls_cb() is called (i.e. it's called exactly once for each entry in
1391 * @mrls).
1392 * TODO: Cancellation is currently not supoprted, since no consumers of this API make use of it, and it needs careful thought when
1393 * being implemented, as a separate #GCancellable instance will have to be created for each parallel computation. */
1394 totem_pl_parser_parse_async (self->priv->parser, mrl_data->mrl, FALSE, NULL, (GAsyncReadyCallback) add_mrls_cb, mrl_data);
1395 }
1396
1397 /* Deal with the case that all async operations completed before we got to this point (since we've held a reference to the operation data so
1398 * that it doesn't get freed prematurely if all the scheduled async parse operations complete before we've finished scheduling the rest. */
1399 add_mrls_finish_operation (operation_data);
1400 }
1401
1402 /**
1403 * totem_playlist_add_mrls_finish:
1404 * @self: a #TotemPlaylist
1405 * @result: the #GAsyncResult that was provided to the callback
1406 * @error: (allow-none): a #GError for error reporting, or %NULL
1407 *
1408 * Finish an asynchronous batch MRL addition operation started by totem_playlist_add_mrls().
1409 *
1410 * Return value: %TRUE on success, %FALSE otherwise
1411 *
1412 * Since: 3.0
1413 */
1414 gboolean
totem_playlist_add_mrls_finish(TotemPlaylist * self,GAsyncResult * result,GError ** error)1415 totem_playlist_add_mrls_finish (TotemPlaylist *self,
1416 GAsyncResult *result,
1417 GError **error)
1418 {
1419 g_return_val_if_fail (TOTEM_IS_PLAYLIST (self), FALSE);
1420 g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
1421 g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
1422 g_return_val_if_fail (g_simple_async_result_is_valid (result, G_OBJECT (self), totem_playlist_add_mrls), FALSE);
1423
1424 /* We don't have anything to return at the moment. */
1425 return TRUE;
1426 }
1427
1428 static gboolean
totem_playlist_clear_cb(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)1429 totem_playlist_clear_cb (GtkTreeModel *model,
1430 GtkTreePath *path,
1431 GtkTreeIter *iter,
1432 gpointer data)
1433 {
1434 totem_playlist_emit_item_removed (data, iter);
1435 return FALSE;
1436 }
1437
1438 gboolean
totem_playlist_clear(TotemPlaylist * playlist)1439 totem_playlist_clear (TotemPlaylist *playlist)
1440 {
1441 GtkListStore *store;
1442
1443 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
1444
1445 if (PL_LEN == 0)
1446 return FALSE;
1447
1448 gtk_tree_model_foreach (playlist->priv->model,
1449 totem_playlist_clear_cb,
1450 playlist);
1451
1452 store = GTK_LIST_STORE (playlist->priv->model);
1453 gtk_list_store_clear (store);
1454
1455 g_clear_pointer (&playlist->priv->current, gtk_tree_path_free);
1456
1457 g_signal_emit (G_OBJECT (playlist),
1458 totem_playlist_table_signals[CURRENT_REMOVED],
1459 0, NULL);
1460
1461 return TRUE;
1462 }
1463
1464 static int
compare_removal(GtkTreeRowReference * ref,GtkTreePath * path)1465 compare_removal (GtkTreeRowReference *ref, GtkTreePath *path)
1466 {
1467 GtkTreePath *ref_path;
1468 int ret = -1;
1469
1470 ref_path = gtk_tree_row_reference_get_path (ref);
1471 if (gtk_tree_path_compare (path, ref_path) == 0)
1472 ret = 0;
1473 gtk_tree_path_free (ref_path);
1474 return ret;
1475 }
1476
1477 /* Whether the item in question will be removed */
1478 static gboolean
totem_playlist_item_to_be_removed(TotemPlaylist * playlist,GtkTreePath * path,ClearComparisonFunc func)1479 totem_playlist_item_to_be_removed (TotemPlaylist *playlist,
1480 GtkTreePath *path,
1481 ClearComparisonFunc func)
1482 {
1483 GList *ret;
1484
1485 if (func == NULL) {
1486 GtkTreeSelection *selection;
1487
1488 selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (playlist->priv->treeview));
1489 return gtk_tree_selection_path_is_selected (selection, path);
1490 }
1491
1492 ret = g_list_find_custom (playlist->priv->list, path, (GCompareFunc) compare_removal);
1493 return (ret != NULL);
1494 }
1495
1496 static void
totem_playlist_clear_with_compare(TotemPlaylist * playlist,ClearComparisonFunc func,gconstpointer data)1497 totem_playlist_clear_with_compare (TotemPlaylist *playlist,
1498 ClearComparisonFunc func,
1499 gconstpointer data)
1500 {
1501 GtkTreeRowReference *ref;
1502 GtkTreeRowReference *next;
1503
1504 ref = NULL;
1505 next = NULL;
1506
1507 if (func == NULL) {
1508 GtkTreeSelection *selection;
1509
1510 selection = gtk_tree_view_get_selection
1511 (GTK_TREE_VIEW (playlist->priv->treeview));
1512 if (selection == NULL)
1513 return;
1514
1515 gtk_tree_selection_selected_foreach (selection,
1516 totem_playlist_foreach_selected,
1517 (gpointer) playlist);
1518 } else {
1519 guint num_items, i;
1520
1521 num_items = PL_LEN;
1522 if (num_items == 0)
1523 return;
1524
1525 for (i = 0; i < num_items; i++) {
1526 GtkTreeIter iter;
1527 char *playlist_index;
1528
1529 playlist_index = g_strdup_printf ("%d", i);
1530 if (gtk_tree_model_get_iter_from_string (playlist->priv->model, &iter, playlist_index) == FALSE) {
1531 g_free (playlist_index);
1532 continue;
1533 }
1534 g_free (playlist_index);
1535
1536 if ((* func) (playlist, &iter, data) != FALSE) {
1537 GtkTreePath *path;
1538 GtkTreeRowReference *r;
1539
1540 path = gtk_tree_path_new_from_indices (i, -1);
1541 r = gtk_tree_row_reference_new (playlist->priv->model, path);
1542 if (playlist->priv->current_to_be_removed == FALSE && playlist->priv->current != NULL) {
1543 if (gtk_tree_path_compare (path, playlist->priv->current) == 0) {
1544 playlist->priv->current_to_be_removed = TRUE;
1545 }
1546 }
1547 playlist->priv->list = g_list_prepend (playlist->priv->list, r);
1548 gtk_tree_path_free (path);
1549 }
1550 }
1551
1552 if (playlist->priv->list == NULL)
1553 return;
1554 }
1555
1556 /* If the current item is to change, we need to keep an static
1557 * reference to it, TreeIter and TreePath don't allow that */
1558 if (playlist->priv->current_to_be_removed == FALSE &&
1559 playlist->priv->current != NULL) {
1560 ref = gtk_tree_row_reference_new (playlist->priv->model,
1561 playlist->priv->current);
1562 } else if (playlist->priv->current != NULL) {
1563 GtkTreePath *item;
1564
1565 item = gtk_tree_path_copy (playlist->priv->current);
1566 gtk_tree_path_next (item);
1567 next = gtk_tree_row_reference_new (playlist->priv->model, item);
1568 while (next != NULL) {
1569 if (totem_playlist_item_to_be_removed (playlist, item, func) == FALSE) {
1570 /* Found the item after the current one that
1571 * won't be removed, thus the new current */
1572 break;
1573 }
1574 gtk_tree_row_reference_free (next);
1575 gtk_tree_path_next (item);
1576 next = gtk_tree_row_reference_new (playlist->priv->model, item);
1577 }
1578 }
1579
1580 /* We destroy the items, one-by-one from the list built above */
1581 while (playlist->priv->list != NULL) {
1582 GtkTreePath *path;
1583 GtkTreeIter iter;
1584
1585 path = gtk_tree_row_reference_get_path
1586 ((GtkTreeRowReference *)(playlist->priv->list->data));
1587 gtk_tree_model_get_iter (playlist->priv->model, &iter, path);
1588 gtk_tree_path_free (path);
1589
1590 totem_playlist_emit_item_removed (playlist, &iter);
1591 gtk_list_store_remove (GTK_LIST_STORE (playlist->priv->model), &iter);
1592
1593 gtk_tree_row_reference_free
1594 ((GtkTreeRowReference *)(playlist->priv->list->data));
1595 playlist->priv->list = g_list_remove (playlist->priv->list,
1596 playlist->priv->list->data);
1597 }
1598 g_clear_pointer (&playlist->priv->list, g_list_free);
1599
1600 if (playlist->priv->current_to_be_removed != FALSE) {
1601 /* The current item was removed from the playlist */
1602 if (next != NULL) {
1603 playlist->priv->current = gtk_tree_row_reference_get_path (next);
1604 gtk_tree_row_reference_free (next);
1605 } else {
1606 playlist->priv->current = NULL;
1607 }
1608
1609 g_signal_emit (G_OBJECT (playlist),
1610 totem_playlist_table_signals[CURRENT_REMOVED],
1611 0, NULL);
1612 } else {
1613 if (ref != NULL) {
1614 /* The path to the current item changed */
1615 playlist->priv->current = gtk_tree_row_reference_get_path (ref);
1616 }
1617
1618 g_signal_emit (G_OBJECT (playlist),
1619 totem_playlist_table_signals[CHANGED], 0,
1620 NULL);
1621 }
1622 if (ref != NULL)
1623 gtk_tree_row_reference_free (ref);
1624 gtk_tree_view_columns_autosize (GTK_TREE_VIEW (playlist->priv->treeview));
1625
1626 playlist->priv->current_to_be_removed = FALSE;
1627 }
1628
1629 static char *
get_mount_default_location(GMount * mount)1630 get_mount_default_location (GMount *mount)
1631 {
1632 GFile *file;
1633 char *path;
1634
1635 file = g_mount_get_root (mount);
1636 if (file == NULL)
1637 return NULL;
1638 path = g_file_get_path (file);
1639 g_object_unref (file);
1640 return path;
1641 }
1642
1643 static gboolean
totem_playlist_compare_with_mount(TotemPlaylist * playlist,GtkTreeIter * iter,gconstpointer data)1644 totem_playlist_compare_with_mount (TotemPlaylist *playlist, GtkTreeIter *iter, gconstpointer data)
1645 {
1646 GMount *clear_mount = (GMount *) data;
1647 GMount *mount;
1648 char *mount_path, *clear_mount_path;
1649 gboolean retval = FALSE;
1650
1651 gtk_tree_model_get (playlist->priv->model, iter,
1652 MOUNT_COL, &mount, -1);
1653
1654 if (mount == NULL)
1655 return FALSE;
1656
1657 clear_mount_path = NULL;
1658
1659 mount_path = get_mount_default_location (mount);
1660 if (mount_path == NULL)
1661 goto bail;
1662
1663 clear_mount_path = get_mount_default_location (clear_mount);
1664 if (clear_mount_path == NULL)
1665 goto bail;
1666
1667 if (g_str_equal (mount_path, clear_mount_path))
1668 retval = TRUE;
1669
1670 bail:
1671 g_free (mount_path);
1672 g_free (clear_mount_path);
1673
1674 if (mount != NULL)
1675 g_object_unref (mount);
1676
1677 return retval;
1678 }
1679
1680 void
totem_playlist_clear_with_g_mount(TotemPlaylist * playlist,GMount * mount)1681 totem_playlist_clear_with_g_mount (TotemPlaylist *playlist,
1682 GMount *mount)
1683 {
1684 g_return_if_fail (mount != NULL);
1685
1686 totem_playlist_clear_with_compare (playlist,
1687 (ClearComparisonFunc) totem_playlist_compare_with_mount,
1688 mount);
1689 }
1690
1691 char *
totem_playlist_get_current_mrl(TotemPlaylist * playlist,char ** subtitle)1692 totem_playlist_get_current_mrl (TotemPlaylist *playlist, char **subtitle)
1693 {
1694 GtkTreeIter iter;
1695 char *path;
1696
1697 if (subtitle != NULL)
1698 *subtitle = NULL;
1699
1700 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), NULL);
1701
1702 if (update_current_from_playlist (playlist) == FALSE)
1703 return NULL;
1704
1705 if (gtk_tree_model_get_iter (playlist->priv->model, &iter,
1706 playlist->priv->current) == FALSE)
1707 return NULL;
1708
1709 if (subtitle != NULL) {
1710 gtk_tree_model_get (playlist->priv->model, &iter,
1711 URI_COL, &path,
1712 SUBTITLE_URI_COL, subtitle,
1713 -1);
1714 } else {
1715 gtk_tree_model_get (playlist->priv->model, &iter,
1716 URI_COL, &path,
1717 -1);
1718 }
1719
1720 return path;
1721 }
1722
1723 char *
totem_playlist_get_current_title(TotemPlaylist * playlist)1724 totem_playlist_get_current_title (TotemPlaylist *playlist)
1725 {
1726 GtkTreeIter iter;
1727 char *title;
1728
1729 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), NULL);
1730
1731 if (update_current_from_playlist (playlist) == FALSE)
1732 return NULL;
1733
1734 gtk_tree_model_get_iter (playlist->priv->model,
1735 &iter,
1736 playlist->priv->current);
1737
1738 gtk_tree_model_get (playlist->priv->model,
1739 &iter,
1740 FILENAME_COL, &title,
1741 -1);
1742 return title;
1743 }
1744
1745 char *
totem_playlist_get_current_content_type(TotemPlaylist * playlist)1746 totem_playlist_get_current_content_type (TotemPlaylist *playlist)
1747 {
1748 GtkTreeIter iter;
1749 char *content_type;
1750
1751 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), NULL);
1752
1753 if (update_current_from_playlist (playlist) == FALSE)
1754 return NULL;
1755
1756 gtk_tree_model_get_iter (playlist->priv->model,
1757 &iter,
1758 playlist->priv->current);
1759
1760 gtk_tree_model_get (playlist->priv->model,
1761 &iter,
1762 MIME_TYPE_COL, &content_type,
1763 -1);
1764
1765 return content_type;
1766 }
1767
1768 gint64
totem_playlist_steal_current_starttime(TotemPlaylist * playlist)1769 totem_playlist_steal_current_starttime (TotemPlaylist *playlist)
1770 {
1771 GtkTreeIter iter;
1772 gint64 starttime;
1773
1774 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), 0);
1775
1776 if (update_current_from_playlist (playlist) == FALSE)
1777 return 0;
1778
1779 gtk_tree_model_get_iter (playlist->priv->model,
1780 &iter,
1781 playlist->priv->current);
1782
1783 gtk_tree_model_get (playlist->priv->model,
1784 &iter,
1785 STARTTIME_COL, &starttime,
1786 -1);
1787
1788 /* And reset the starttime so it's only used once,
1789 * hence the "steal" in the API name */
1790 gtk_list_store_set (GTK_LIST_STORE (playlist->priv->model),
1791 &iter,
1792 STARTTIME_COL, 0,
1793 -1);
1794
1795 return starttime;
1796 }
1797
1798 char *
totem_playlist_get_title(TotemPlaylist * playlist,guint title_index)1799 totem_playlist_get_title (TotemPlaylist *playlist, guint title_index)
1800 {
1801 GtkTreeIter iter;
1802 GtkTreePath *path;
1803 char *title;
1804
1805 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), NULL);
1806
1807 path = gtk_tree_path_new_from_indices (title_index, -1);
1808
1809 gtk_tree_model_get_iter (playlist->priv->model,
1810 &iter, path);
1811
1812 gtk_tree_path_free (path);
1813
1814 gtk_tree_model_get (playlist->priv->model,
1815 &iter,
1816 FILENAME_COL, &title,
1817 -1);
1818
1819 return title;
1820 }
1821
1822 gboolean
totem_playlist_has_previous_mrl(TotemPlaylist * playlist)1823 totem_playlist_has_previous_mrl (TotemPlaylist *playlist)
1824 {
1825 GtkTreeIter iter;
1826
1827 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
1828
1829 if (update_current_from_playlist (playlist) == FALSE)
1830 return FALSE;
1831
1832 gtk_tree_model_get_iter (playlist->priv->model,
1833 &iter,
1834 playlist->priv->current);
1835
1836 return gtk_tree_model_iter_previous (playlist->priv->model, &iter);
1837 }
1838
1839 gboolean
totem_playlist_has_next_mrl(TotemPlaylist * playlist)1840 totem_playlist_has_next_mrl (TotemPlaylist *playlist)
1841 {
1842 GtkTreeIter iter;
1843
1844 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
1845
1846 if (update_current_from_playlist (playlist) == FALSE)
1847 return FALSE;
1848
1849 gtk_tree_model_get_iter (playlist->priv->model,
1850 &iter,
1851 playlist->priv->current);
1852
1853 return gtk_tree_model_iter_next (playlist->priv->model, &iter);
1854 }
1855
1856 gboolean
totem_playlist_set_title(TotemPlaylist * playlist,const char * title)1857 totem_playlist_set_title (TotemPlaylist *playlist, const char *title)
1858 {
1859 GtkListStore *store;
1860 GtkTreeIter iter;
1861 char *escaped_title;
1862
1863 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
1864
1865 if (update_current_from_playlist (playlist) == FALSE)
1866 return FALSE;
1867
1868 store = GTK_LIST_STORE (playlist->priv->model);
1869 gtk_tree_model_get_iter (playlist->priv->model,
1870 &iter,
1871 playlist->priv->current);
1872
1873 escaped_title = g_markup_escape_text (title, -1);
1874 gtk_list_store_set (store, &iter,
1875 FILENAME_COL, title,
1876 FILENAME_ESCAPED_COL, escaped_title,
1877 TITLE_CUSTOM_COL, TRUE,
1878 -1);
1879 g_free (escaped_title);
1880
1881 g_signal_emit (playlist,
1882 totem_playlist_table_signals[ACTIVE_NAME_CHANGED], 0);
1883
1884 return TRUE;
1885 }
1886
1887 gboolean
totem_playlist_set_playing(TotemPlaylist * playlist,TotemPlaylistStatus state)1888 totem_playlist_set_playing (TotemPlaylist *playlist, TotemPlaylistStatus state)
1889 {
1890 GtkListStore *store;
1891 GtkTreeIter iter;
1892 GtkTreePath *path;
1893
1894 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
1895
1896 if (update_current_from_playlist (playlist) == FALSE)
1897 return FALSE;
1898
1899 store = GTK_LIST_STORE (playlist->priv->model);
1900 gtk_tree_model_get_iter (playlist->priv->model,
1901 &iter,
1902 playlist->priv->current);
1903
1904 gtk_list_store_set (store, &iter,
1905 PLAYING_COL, state,
1906 -1);
1907
1908 if (state == FALSE)
1909 return TRUE;
1910
1911 path = gtk_tree_model_get_path (GTK_TREE_MODEL (store), &iter);
1912 gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (playlist->priv->treeview),
1913 path, NULL,
1914 TRUE, 0.5, 0);
1915 gtk_tree_path_free (path);
1916
1917 return TRUE;
1918 }
1919
1920 TotemPlaylistStatus
totem_playlist_get_playing(TotemPlaylist * playlist)1921 totem_playlist_get_playing (TotemPlaylist *playlist)
1922 {
1923 GtkTreeIter iter;
1924 TotemPlaylistStatus status;
1925
1926 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), TOTEM_PLAYLIST_STATUS_NONE);
1927
1928 if (gtk_tree_model_get_iter (playlist->priv->model, &iter, playlist->priv->current) == FALSE)
1929 return TOTEM_PLAYLIST_STATUS_NONE;
1930
1931 gtk_tree_model_get (playlist->priv->model,
1932 &iter,
1933 PLAYING_COL, &status,
1934 -1);
1935
1936 return status;
1937 }
1938
1939 void
totem_playlist_set_previous(TotemPlaylist * playlist)1940 totem_playlist_set_previous (TotemPlaylist *playlist)
1941 {
1942 GtkTreeIter iter;
1943 char *path;
1944
1945 g_return_if_fail (TOTEM_IS_PLAYLIST (playlist));
1946
1947 if (totem_playlist_has_previous_mrl (playlist) == FALSE)
1948 return;
1949
1950 totem_playlist_unset_playing (playlist);
1951
1952 path = gtk_tree_path_to_string (playlist->priv->current);
1953 if (g_str_equal (path, "0")) {
1954 totem_playlist_set_at_end (playlist);
1955 g_free (path);
1956 return;
1957 }
1958 g_free (path);
1959
1960 gtk_tree_model_get_iter (playlist->priv->model,
1961 &iter,
1962 playlist->priv->current);
1963
1964 if (!gtk_tree_model_iter_previous (playlist->priv->model, &iter))
1965 g_assert_not_reached ();
1966 gtk_tree_path_free (playlist->priv->current);
1967 playlist->priv->current = gtk_tree_model_get_path
1968 (playlist->priv->model, &iter);
1969 }
1970
1971 void
totem_playlist_set_next(TotemPlaylist * playlist)1972 totem_playlist_set_next (TotemPlaylist *playlist)
1973 {
1974 GtkTreeIter iter;
1975
1976 g_return_if_fail (TOTEM_IS_PLAYLIST (playlist));
1977
1978 if (totem_playlist_has_next_mrl (playlist) == FALSE) {
1979 totem_playlist_set_at_start (playlist);
1980 return;
1981 }
1982
1983 totem_playlist_unset_playing (playlist);
1984
1985 gtk_tree_model_get_iter (playlist->priv->model,
1986 &iter,
1987 playlist->priv->current);
1988
1989 if (!gtk_tree_model_iter_next (playlist->priv->model, &iter))
1990 g_assert_not_reached ();
1991 gtk_tree_path_free (playlist->priv->current);
1992 playlist->priv->current = gtk_tree_model_get_path (playlist->priv->model, &iter);
1993 }
1994
1995 gboolean
totem_playlist_get_repeat(TotemPlaylist * playlist)1996 totem_playlist_get_repeat (TotemPlaylist *playlist)
1997 {
1998 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
1999
2000 return playlist->priv->repeat;
2001 }
2002
2003 void
totem_playlist_set_repeat(TotemPlaylist * playlist,gboolean repeat)2004 totem_playlist_set_repeat (TotemPlaylist *playlist, gboolean repeat)
2005 {
2006 g_return_if_fail (TOTEM_IS_PLAYLIST (playlist));
2007
2008 g_settings_set_boolean (playlist->priv->settings, "repeat", repeat);
2009 }
2010
2011 void
totem_playlist_set_at_start(TotemPlaylist * playlist)2012 totem_playlist_set_at_start (TotemPlaylist *playlist)
2013 {
2014 g_return_if_fail (TOTEM_IS_PLAYLIST (playlist));
2015
2016 totem_playlist_unset_playing (playlist);
2017 g_clear_pointer (&playlist->priv->current, gtk_tree_path_free);
2018 update_current_from_playlist (playlist);
2019 }
2020
2021 void
totem_playlist_set_at_end(TotemPlaylist * playlist)2022 totem_playlist_set_at_end (TotemPlaylist *playlist)
2023 {
2024 int indice;
2025
2026 g_return_if_fail (TOTEM_IS_PLAYLIST (playlist));
2027
2028 totem_playlist_unset_playing (playlist);
2029 g_clear_pointer (&playlist->priv->current, gtk_tree_path_free);
2030
2031 if (PL_LEN) {
2032 indice = PL_LEN - 1;
2033 playlist->priv->current = gtk_tree_path_new_from_indices
2034 (indice, -1);
2035 }
2036 }
2037
2038 int
totem_playlist_get_current(TotemPlaylist * playlist)2039 totem_playlist_get_current (TotemPlaylist *playlist)
2040 {
2041 char *path;
2042 double current_index;
2043
2044 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), -1);
2045
2046 if (playlist->priv->current == NULL)
2047 return -1;
2048 path = gtk_tree_path_to_string (playlist->priv->current);
2049 if (path == NULL)
2050 return -1;
2051
2052 current_index = g_ascii_strtod (path, NULL);
2053 g_free (path);
2054
2055 return current_index;
2056 }
2057
2058 int
totem_playlist_get_last(TotemPlaylist * playlist)2059 totem_playlist_get_last (TotemPlaylist *playlist)
2060 {
2061 guint len = PL_LEN;
2062
2063 g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), -1);
2064
2065 if (len == 0)
2066 return -1;
2067
2068 return len - 1;
2069 }
2070
2071 void
totem_playlist_set_current(TotemPlaylist * playlist,guint current_index)2072 totem_playlist_set_current (TotemPlaylist *playlist, guint current_index)
2073 {
2074 g_return_if_fail (TOTEM_IS_PLAYLIST (playlist));
2075
2076 if (current_index >= (guint) PL_LEN)
2077 return;
2078
2079 totem_playlist_unset_playing (playlist);
2080 gtk_tree_path_free (playlist->priv->current);
2081 playlist->priv->current = gtk_tree_path_new_from_indices (current_index, -1);
2082 }
2083
2084 static void
totem_playlist_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)2085 totem_playlist_set_property (GObject *object,
2086 guint property_id,
2087 const GValue *value,
2088 GParamSpec *pspec)
2089 {
2090 TotemPlaylist *playlist;
2091
2092 playlist = TOTEM_PLAYLIST (object);
2093
2094 switch (property_id) {
2095 case PROP_REPEAT:
2096 g_settings_set_boolean (playlist->priv->settings, "repeat", g_value_get_boolean (value));
2097 break;
2098 default:
2099 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
2100 break;
2101 }
2102 }
2103
2104 static void
totem_playlist_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)2105 totem_playlist_get_property (GObject *object,
2106 guint property_id,
2107 GValue *value,
2108 GParamSpec *pspec)
2109 {
2110 TotemPlaylist *playlist;
2111
2112 playlist = TOTEM_PLAYLIST (object);
2113
2114 switch (property_id) {
2115 case PROP_REPEAT:
2116 g_value_set_boolean (value, playlist->priv->repeat);
2117 break;
2118 default:
2119 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
2120 break;
2121 }
2122 }
2123
2124 static void
totem_playlist_class_init(TotemPlaylistClass * klass)2125 totem_playlist_class_init (TotemPlaylistClass *klass)
2126 {
2127 GObjectClass *object_class = G_OBJECT_CLASS (klass);
2128
2129 object_class->set_property = totem_playlist_set_property;
2130 object_class->get_property = totem_playlist_get_property;
2131 object_class->dispose = totem_playlist_dispose;
2132
2133 /* Signals */
2134 totem_playlist_table_signals[CHANGED] =
2135 g_signal_new ("changed",
2136 G_TYPE_FROM_CLASS (klass),
2137 G_SIGNAL_RUN_LAST,
2138 G_STRUCT_OFFSET (TotemPlaylistClass, changed),
2139 NULL, NULL,
2140 g_cclosure_marshal_VOID__VOID,
2141 G_TYPE_NONE, 0);
2142 totem_playlist_table_signals[ITEM_ACTIVATED] =
2143 g_signal_new ("item-activated",
2144 G_TYPE_FROM_CLASS (klass),
2145 G_SIGNAL_RUN_LAST,
2146 G_STRUCT_OFFSET (TotemPlaylistClass, item_activated),
2147 NULL, NULL,
2148 g_cclosure_marshal_VOID__VOID,
2149 G_TYPE_NONE, 0);
2150 totem_playlist_table_signals[ACTIVE_NAME_CHANGED] =
2151 g_signal_new ("active-name-changed",
2152 G_TYPE_FROM_CLASS (klass),
2153 G_SIGNAL_RUN_LAST,
2154 G_STRUCT_OFFSET (TotemPlaylistClass, active_name_changed),
2155 NULL, NULL,
2156 g_cclosure_marshal_VOID__VOID,
2157 G_TYPE_NONE, 0);
2158 totem_playlist_table_signals[CURRENT_REMOVED] =
2159 g_signal_new ("current-removed",
2160 G_TYPE_FROM_CLASS (klass),
2161 G_SIGNAL_RUN_LAST,
2162 G_STRUCT_OFFSET (TotemPlaylistClass,
2163 current_removed),
2164 NULL, NULL,
2165 g_cclosure_marshal_VOID__VOID,
2166 G_TYPE_NONE, 0);
2167 totem_playlist_table_signals[SUBTITLE_CHANGED] =
2168 g_signal_new ("subtitle-changed",
2169 G_TYPE_FROM_CLASS (klass),
2170 G_SIGNAL_RUN_LAST,
2171 G_STRUCT_OFFSET (TotemPlaylistClass,
2172 subtitle_changed),
2173 NULL, NULL,
2174 g_cclosure_marshal_VOID__VOID,
2175 G_TYPE_NONE, 0);
2176 totem_playlist_table_signals[ITEM_ADDED] =
2177 g_signal_new ("item-added",
2178 G_TYPE_FROM_CLASS (klass),
2179 G_SIGNAL_RUN_LAST,
2180 G_STRUCT_OFFSET (TotemPlaylistClass,
2181 item_added),
2182 NULL, NULL,
2183 g_cclosure_marshal_generic,
2184 G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING);
2185 totem_playlist_table_signals[ITEM_REMOVED] =
2186 g_signal_new ("item-removed",
2187 G_TYPE_FROM_CLASS (klass),
2188 G_SIGNAL_RUN_LAST,
2189 G_STRUCT_OFFSET (TotemPlaylistClass,
2190 item_removed),
2191 NULL, NULL,
2192 g_cclosure_marshal_generic,
2193 G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING);
2194
2195 g_object_class_install_property (object_class, PROP_REPEAT,
2196 g_param_spec_boolean ("repeat", "Repeat",
2197 "Whether repeat mode is enabled.", FALSE,
2198 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
2199 }
2200