1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3 * Copyright (C) 2006-2010 Jonathan Matthew <jonathan@d14n.org>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * The Rhythmbox authors hereby grant permission for non-GPL compatible
11 * GStreamer plugins to be used and distributed together with GStreamer
12 * and Rhythmbox. This permission is above and beyond the permissions granted
13 * by the GPL license by which Rhythmbox is covered. If you modify this code
14 * you may extend this exception to your version of the code, but you are not
15 * obligated to do so. If you do not wish to do so, delete this exception
16 * statement from your version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program; if not, write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
26 *
27 */
28
29 #include <config.h>
30 #include <gtk/gtk.h>
31 #include <glib/gi18n.h>
32
33 #include "rb-entry-view.h"
34 #include "rb-import-errors-source.h"
35 #include "rb-util.h"
36 #include "rb-debug.h"
37 #include "rb-missing-plugins.h"
38 #include "rb-builder-helpers.h"
39
40 static void rb_import_errors_source_class_init (RBImportErrorsSourceClass *klass);
41 static void rb_import_errors_source_init (RBImportErrorsSource *source);
42 static void rb_import_errors_source_constructed (GObject *object);
43 static void rb_import_errors_source_dispose (GObject *object);
44
45 static void impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec);
46 static void impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);
47
48 static RBEntryView *impl_get_entry_view (RBSource *source);
49 static void impl_delete_selected (RBSource *source);
50 static void impl_get_status (RBDisplayPage *page, char **text, gboolean *busy);
51
52 static void rb_import_errors_source_songs_show_popup_cb (RBEntryView *view,
53 gboolean over_entry,
54 RBImportErrorsSource *source);
55 static void infobar_response_cb (GtkInfoBar *bar, gint response, RBImportErrorsSource *source);
56
57 static void missing_plugin_row_inserted_cb (GtkTreeModel *model,
58 GtkTreePath *path,
59 GtkTreeIter *iter,
60 RBImportErrorsSource *source);
61 static void missing_plugin_row_deleted_cb (GtkTreeModel *model,
62 GtkTreePath *path,
63 RBImportErrorsSource *source);
64
65 enum {
66 PROP_0,
67 PROP_NORMAL_ENTRY_TYPE,
68 PROP_IGNORE_ENTRY_TYPE
69 };
70
71 struct _RBImportErrorsSourcePrivate
72 {
73 RhythmDB *db;
74 RBEntryView *view;
75
76 RhythmDBQueryModel *missing_plugin_model;
77 GtkWidget *infobar;
78
79 RhythmDBEntryType *normal_entry_type;
80 RhythmDBEntryType *ignore_entry_type;
81
82 GMenuModel *popup;
83 };
84
85 G_DEFINE_TYPE (RBImportErrorsSource, rb_import_errors_source, RB_TYPE_SOURCE);
86
87 /**
88 * SECTION:rb-import-errors-source
89 * @short_description: source for displaying import errors
90 *
91 * This source is used to display the names of files that could not
92 * be imported into the library, along with any error messages from
93 * the import process. When there are no import errors to display,
94 * the source is hidden.
95 *
96 * The source allows the user to delete the import error entries,
97 * and to move the files to the trash.
98 *
99 * When a file import fails, a #RhythmDBEntry is created with a
100 * specific entry type for import errors. This source uses a query
101 * model that matches all such import error entries.
102 *
103 * To keep import errors from removable devices separate from those
104 * from the main library, multiple import error sources can be created,
105 * with separate entry types. The generic audio player plugin, for
106 * example, creates an import error source for each device and inserts
107 * it into the source list as a child of the main source for the device.
108 */
109
110 static void
rb_import_errors_source_class_init(RBImportErrorsSourceClass * klass)111 rb_import_errors_source_class_init (RBImportErrorsSourceClass *klass)
112 {
113 GObjectClass *object_class = G_OBJECT_CLASS (klass);
114 RBDisplayPageClass *page_class = RB_DISPLAY_PAGE_CLASS (klass);
115 RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
116
117 object_class->dispose = rb_import_errors_source_dispose;
118 object_class->constructed = rb_import_errors_source_constructed;
119 object_class->get_property = impl_get_property;
120 object_class->set_property = impl_set_property;
121
122 page_class->get_status = impl_get_status;
123
124 source_class->get_entry_view = impl_get_entry_view;
125 source_class->can_rename = (RBSourceFeatureFunc) rb_false_function;
126
127 source_class->can_cut = (RBSourceFeatureFunc) rb_false_function;
128 source_class->can_delete = (RBSourceFeatureFunc) rb_true_function;
129 source_class->can_move_to_trash = (RBSourceFeatureFunc) rb_true_function;
130 source_class->can_copy = (RBSourceFeatureFunc) rb_false_function;
131 source_class->can_add_to_queue = (RBSourceFeatureFunc) rb_false_function;
132
133 source_class->delete_selected = impl_delete_selected;
134
135 source_class->try_playlist = (RBSourceFeatureFunc) rb_false_function;
136 source_class->can_pause = (RBSourceFeatureFunc) rb_false_function;
137
138 g_object_class_install_property (object_class,
139 PROP_NORMAL_ENTRY_TYPE,
140 g_param_spec_object ("normal-entry-type",
141 "Normal entry type",
142 "Entry type for successfully imported entries of this type",
143 RHYTHMDB_TYPE_ENTRY_TYPE,
144 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
145 g_object_class_install_property (object_class,
146 PROP_IGNORE_ENTRY_TYPE,
147 g_param_spec_object ("ignore-entry-type",
148 "Ignore entry type",
149 "Entry type for entries of this type to be ignored",
150 RHYTHMDB_TYPE_ENTRY_TYPE,
151 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
152
153 g_type_class_add_private (klass, sizeof (RBImportErrorsSourcePrivate));
154 }
155
156 static void
rb_import_errors_source_init(RBImportErrorsSource * source)157 rb_import_errors_source_init (RBImportErrorsSource *source)
158 {
159 source->priv = G_TYPE_INSTANCE_GET_PRIVATE (source, RB_TYPE_IMPORT_ERRORS_SOURCE, RBImportErrorsSourcePrivate);
160 }
161
162 static void
rb_import_errors_source_constructed(GObject * object)163 rb_import_errors_source_constructed (GObject *object)
164 {
165 GObject *shell_player;
166 RBImportErrorsSource *source;
167 RBShell *shell;
168 GPtrArray *query;
169 RhythmDBQueryModel *model;
170 RhythmDBEntryType *entry_type;
171 GtkWidget *box;
172 GtkWidget *label;
173
174 RB_CHAIN_GOBJECT_METHOD (rb_import_errors_source_parent_class, constructed, object);
175
176 source = RB_IMPORT_ERRORS_SOURCE (object);
177
178 g_object_get (source,
179 "shell", &shell,
180 "entry-type", &entry_type,
181 NULL);
182 g_object_get (shell,
183 "db", &source->priv->db,
184 "shell-player", &shell_player,
185 NULL);
186 g_object_unref (shell);
187
188 /* construct real query */
189 query = rhythmdb_query_parse (source->priv->db,
190 RHYTHMDB_QUERY_PROP_EQUALS,
191 RHYTHMDB_PROP_TYPE,
192 entry_type,
193 RHYTHMDB_QUERY_END);
194
195 model = rhythmdb_query_model_new (source->priv->db, query,
196 (GCompareDataFunc) rhythmdb_query_model_string_sort_func,
197 GUINT_TO_POINTER (RHYTHMDB_PROP_LOCATION), NULL, FALSE);
198 rhythmdb_query_free (query);
199
200 /* set up entry view */
201 source->priv->view = rb_entry_view_new (source->priv->db, shell_player,
202 FALSE, FALSE);
203 g_object_unref (shell_player);
204
205 rb_entry_view_set_model (source->priv->view, model);
206
207 rb_entry_view_append_column (source->priv->view, RB_ENTRY_VIEW_COL_LOCATION, TRUE);
208 rb_entry_view_append_column (source->priv->view, RB_ENTRY_VIEW_COL_ERROR, TRUE);
209
210 g_signal_connect_object (source->priv->view, "show_popup",
211 G_CALLBACK (rb_import_errors_source_songs_show_popup_cb), source, 0);
212
213 g_object_set (source, "query-model", model, NULL);
214 g_object_unref (model);
215
216 /* set up query model for tracking missing plugin information */
217 query = rhythmdb_query_parse (source->priv->db,
218 RHYTHMDB_QUERY_PROP_EQUALS,
219 RHYTHMDB_PROP_TYPE,
220 entry_type,
221 RHYTHMDB_QUERY_PROP_NOT_EQUAL,
222 RHYTHMDB_PROP_COMMENT,
223 "",
224 RHYTHMDB_QUERY_END);
225
226 source->priv->missing_plugin_model = rhythmdb_query_model_new_empty (source->priv->db);
227 rhythmdb_do_full_query_async_parsed (source->priv->db,
228 RHYTHMDB_QUERY_RESULTS (source->priv->missing_plugin_model),
229 query);
230 rhythmdb_query_free (query);
231
232 /* set up info bar for triggering codec installation */
233 source->priv->infobar = gtk_info_bar_new_with_buttons (_("Install Additional Software"), GTK_RESPONSE_OK, NULL);
234 g_signal_connect_object (source->priv->infobar,
235 "response",
236 G_CALLBACK (infobar_response_cb),
237 source, 0);
238
239 label = gtk_label_new (_("Additional software is required to play some of these files."));
240 gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
241 gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (GTK_INFO_BAR (source->priv->infobar))),
242 label);
243
244 g_object_unref (entry_type);
245
246 box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
247 gtk_box_pack_start (GTK_BOX (box), GTK_WIDGET (source->priv->view), TRUE, TRUE, 0);
248 gtk_box_pack_start (GTK_BOX (box), source->priv->infobar, FALSE, FALSE, 0);
249
250 gtk_container_add (GTK_CONTAINER (source), box);
251 gtk_widget_show_all (GTK_WIDGET (source));
252 gtk_widget_hide (source->priv->infobar);
253
254 /* show the info bar when there are missing plugin entries */
255 g_signal_connect_object (source->priv->missing_plugin_model,
256 "row-inserted",
257 G_CALLBACK (missing_plugin_row_inserted_cb),
258 source, 0);
259 g_signal_connect_object (source->priv->missing_plugin_model,
260 "row-deleted",
261 G_CALLBACK (missing_plugin_row_deleted_cb),
262 source, 0);
263
264 rb_display_page_set_icon_name (RB_DISPLAY_PAGE (source), "dialog-error-symbolic");
265 }
266
267 static void
impl_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)268 impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
269 {
270 RBImportErrorsSource *source = RB_IMPORT_ERRORS_SOURCE (object);
271 switch (prop_id) {
272 case PROP_NORMAL_ENTRY_TYPE:
273 g_value_set_object (value, source->priv->normal_entry_type);
274 break;
275 case PROP_IGNORE_ENTRY_TYPE:
276 g_value_set_object (value, source->priv->ignore_entry_type);
277 break;
278 default:
279 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
280 break;
281 }
282 }
283
284 static void
impl_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)285 impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
286 {
287 RBImportErrorsSource *source = RB_IMPORT_ERRORS_SOURCE (object);
288 switch (prop_id) {
289 case PROP_NORMAL_ENTRY_TYPE:
290 source->priv->normal_entry_type = g_value_get_object (value);
291 break;
292 case PROP_IGNORE_ENTRY_TYPE:
293 source->priv->ignore_entry_type = g_value_get_object (value);
294 break;
295 default:
296 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
297 break;
298 }
299 }
300
301 static void
rb_import_errors_source_dispose(GObject * object)302 rb_import_errors_source_dispose (GObject *object)
303 {
304 RBImportErrorsSource *source = RB_IMPORT_ERRORS_SOURCE (object);
305
306 if (source->priv->db) {
307 g_object_unref (source->priv->db);
308 source->priv->db = NULL;
309 }
310 if (source->priv->missing_plugin_model) {
311 g_object_unref (source->priv->missing_plugin_model);
312 source->priv->missing_plugin_model = NULL;
313 }
314
315 G_OBJECT_CLASS (rb_import_errors_source_parent_class)->dispose (object);
316 }
317
318 static RBEntryView *
impl_get_entry_view(RBSource * asource)319 impl_get_entry_view (RBSource *asource)
320 {
321 RBImportErrorsSource *source = RB_IMPORT_ERRORS_SOURCE (asource);
322 return source->priv->view;
323 }
324
325 /**
326 * rb_import_errors_source_new:
327 * @shell: the #RBShell instance
328 * @entry_type: the entry type to display in the source
329 * @normal_entry_type: entry type for successfully imported entries of this type
330 * @ignore_entry_type: entry type for entries of this type to be ignored
331 *
332 * Creates a new source for displaying import errors of the
333 * specified type.
334 *
335 * Return value: a new import error source
336 */
337 RBSource *
rb_import_errors_source_new(RBShell * shell,RhythmDBEntryType * entry_type,RhythmDBEntryType * normal_entry_type,RhythmDBEntryType * ignore_entry_type)338 rb_import_errors_source_new (RBShell *shell,
339 RhythmDBEntryType *entry_type,
340 RhythmDBEntryType *normal_entry_type,
341 RhythmDBEntryType *ignore_entry_type)
342 {
343 RBSource *source;
344
345 source = RB_SOURCE (g_object_new (RB_TYPE_IMPORT_ERRORS_SOURCE,
346 "name", _("Import Errors"),
347 "shell", shell,
348 "visibility", FALSE,
349 "hidden-when-empty", TRUE,
350 "entry-type", entry_type,
351 "normal-entry-type", normal_entry_type,
352 "ignore-entry-type", ignore_entry_type,
353 NULL));
354 return source;
355 }
356
357 static void
impl_delete_selected(RBSource * asource)358 impl_delete_selected (RBSource *asource)
359 {
360 RBImportErrorsSource *source = RB_IMPORT_ERRORS_SOURCE (asource);
361 GList *sel, *tem;
362
363 sel = rb_entry_view_get_selected_entries (source->priv->view);
364 for (tem = sel; tem != NULL; tem = tem->next) {
365 rhythmdb_entry_delete (source->priv->db, tem->data);
366 rhythmdb_commit (source->priv->db);
367 }
368
369 g_list_foreach (sel, (GFunc)rhythmdb_entry_unref, NULL);
370 g_list_free (sel);
371 }
372
373 static void
impl_get_status(RBDisplayPage * page,char ** text,gboolean * busy)374 impl_get_status (RBDisplayPage *page, char **text, gboolean *busy)
375 {
376 RhythmDBQueryModel *model;
377 gint count;
378
379 g_object_get (page, "query-model", &model, NULL);
380 count = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (model), NULL);
381 g_object_unref (model);
382
383 *text = g_strdup_printf (ngettext ("%d import error", "%d import errors", count),
384 count);
385 }
386
387 static void
rb_import_errors_source_songs_show_popup_cb(RBEntryView * view,gboolean over_entry,RBImportErrorsSource * source)388 rb_import_errors_source_songs_show_popup_cb (RBEntryView *view,
389 gboolean over_entry,
390 RBImportErrorsSource *source)
391 {
392 GtkWidget *menu;
393 GtkBuilder *builder;
394
395 if (over_entry == FALSE)
396 return;
397
398 if (source->priv->popup == NULL) {
399 builder = rb_builder_load ("import-errors-popup.ui", NULL);
400 source->priv->popup = G_MENU_MODEL (gtk_builder_get_object (builder, "import-errors-popup"));
401 g_object_ref (source->priv->popup);
402 g_object_unref (builder);
403 }
404
405 menu = gtk_menu_new_from_model (source->priv->popup);
406 gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (source), NULL);
407 gtk_menu_popup (GTK_MENU (menu),
408 NULL,
409 NULL,
410 NULL,
411 NULL,
412 3,
413 gtk_get_current_event_time ());
414 }
415
416 static void
missing_plugin_row_inserted_cb(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,RBImportErrorsSource * source)417 missing_plugin_row_inserted_cb (GtkTreeModel *model,
418 GtkTreePath *path,
419 GtkTreeIter *iter,
420 RBImportErrorsSource *source)
421 {
422 gtk_widget_show (source->priv->infobar);
423 }
424
425 static void
missing_plugin_row_deleted_cb(GtkTreeModel * model,GtkTreePath * path,RBImportErrorsSource * source)426 missing_plugin_row_deleted_cb (GtkTreeModel *model,
427 GtkTreePath *path,
428 RBImportErrorsSource *source)
429 {
430 /* row hasn't been deleted from the model yet, so the count
431 * still includes it.
432 */
433 if (gtk_tree_model_iter_n_children (model, NULL) == 1) {
434 gtk_widget_hide (source->priv->infobar);
435 }
436 }
437
438 static void
missing_plugins_retry_cb(gpointer instance,gboolean installed,RBImportErrorsSource * source)439 missing_plugins_retry_cb (gpointer instance, gboolean installed, RBImportErrorsSource *source)
440 {
441 GtkTreeIter iter;
442 RhythmDBEntryType *error_entry_type;
443
444 gtk_info_bar_set_response_sensitive (GTK_INFO_BAR (source->priv->infobar),
445 GTK_RESPONSE_OK,
446 TRUE);
447
448 if (installed == FALSE) {
449 rb_debug ("installer failed, not retrying imports");
450 return;
451 }
452
453 if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (source->priv->missing_plugin_model), &iter) == FALSE) {
454 return;
455 }
456
457 g_object_get (source, "entry-type", &error_entry_type, NULL);
458 do {
459 RhythmDBEntry *entry;
460
461 entry = rhythmdb_query_model_iter_to_entry (source->priv->missing_plugin_model, &iter);
462 rhythmdb_add_uri_with_types (source->priv->db,
463 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION),
464 source->priv->normal_entry_type,
465 source->priv->ignore_entry_type,
466 error_entry_type);
467 } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (source->priv->missing_plugin_model), &iter));
468
469 g_object_unref (error_entry_type);
470 }
471
472 static void
missing_plugins_retry_cleanup(RBImportErrorsSource * source)473 missing_plugins_retry_cleanup (RBImportErrorsSource *source)
474 {
475 g_object_unref (source);
476 }
477
478 static void
infobar_response_cb(GtkInfoBar * infobar,gint response,RBImportErrorsSource * source)479 infobar_response_cb (GtkInfoBar *infobar, gint response, RBImportErrorsSource *source)
480 {
481 char **details = NULL;
482 GtkTreeIter iter;
483 GClosure *closure;
484 int i;
485
486 /* gather plugin installer detail strings */
487 if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (source->priv->missing_plugin_model), &iter) == FALSE) {
488 return;
489 }
490
491 i = 0;
492 do {
493 RhythmDBEntry *entry;
494 char **bits;
495 int j;
496
497 entry = rhythmdb_query_model_iter_to_entry (source->priv->missing_plugin_model, &iter);
498 bits = g_strsplit (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_COMMENT), "\n", 0);
499
500 for (j = 0; bits[j] != NULL; j++) {
501 if (rb_str_in_strv (bits[j], (const char **)details) == FALSE) {
502 details = g_realloc (details, sizeof (char *) * i+2);
503 details[i++] = g_strdup (bits[j]);
504 details[i] = NULL;
505 }
506 }
507
508 g_strfreev (bits);
509 } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (source->priv->missing_plugin_model), &iter));
510
511 /* run the installer */
512 closure = g_cclosure_new ((GCallback) missing_plugins_retry_cb,
513 g_object_ref (source),
514 (GClosureNotify) missing_plugins_retry_cleanup);
515 g_closure_set_marshal (closure, g_cclosure_marshal_VOID__BOOLEAN);
516 if (rb_missing_plugins_install ((const char **)details, TRUE, closure) == TRUE) {
517 /* disable the button while the installer is running */
518 gtk_info_bar_set_response_sensitive (infobar, response, FALSE);
519 }
520 g_closure_sink (closure);
521
522 g_strfreev (details);
523 }
524