1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2002 Jorn Baayen <jorn@nl.linux.org>
4 * Copyright (C) 2003,2004 Colin Walters <walters@verbum.org>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * The Rhythmbox authors hereby grant permission for non-GPL compatible
12 * GStreamer plugins to be used and distributed together with GStreamer
13 * and Rhythmbox. This permission is above and beyond the permissions granted
14 * by the GPL license by which Rhythmbox is covered. If you modify this code
15 * you may extend this exception to your version of the code, but you are not
16 * obligated to do so. If you do not wish to do so, delete this exception
17 * statement from your version.
18 *
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with this program; if not, write to the Free Software
26 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
27 *
28 */
29
30 /**
31 * SECTION:rb-browser-source
32 * @short_description: base class for sources that include genre/artist/album browsers
33 *
34 * This class simplifies implementation of sources that include genre/artist/album browsers.
35 * It also handles searching (using the search box) and a few other UI niceties.
36 *
37 * Instances of browser sources will use a query that will match all entries of
38 * the entry type assigned to the source, so it's mostly suited for sources that
39 * have an entry type of their own.
40 */
41
42 #include "config.h"
43
44 #include <string.h>
45
46 #include <gtk/gtk.h>
47 #include <glib/gi18n.h>
48
49 #include "rb-source.h"
50 #include "rb-library-source.h"
51 #include "rb-source-search-basic.h"
52
53 #include "rhythmdb-query-model.h"
54 #include "rb-property-view.h"
55 #include "rb-entry-view.h"
56 #include "rb-library-browser.h"
57 #include "rb-util.h"
58 #include "rb-file-helpers.h"
59 #include "rb-dialog.h"
60 #include "rb-debug.h"
61 #include "rb-song-info.h"
62 #include "rb-search-entry.h"
63 #include "rb-source-toolbar.h"
64 #include "rb-shell-preferences.h"
65 #include "rb-builder-helpers.h"
66 #include "rb-application.h"
67
68 static void rb_browser_source_class_init (RBBrowserSourceClass *klass);
69 static void rb_browser_source_init (RBBrowserSource *source);
70 static void rb_browser_source_constructed (GObject *object);
71 static void rb_browser_source_dispose (GObject *object);
72 static void rb_browser_source_finalize (GObject *object);
73 static void rb_browser_source_set_property (GObject *object,
74 guint prop_id,
75 const GValue *value,
76 GParamSpec *pspec);
77 static void rb_browser_source_get_property (GObject *object,
78 guint prop_id,
79 GValue *value,
80 GParamSpec *pspec);
81 static void select_genre_action_cb (GSimpleAction *action, GVariant *parameters, gpointer data);
82 static void select_artist_action_cb (GSimpleAction *action, GVariant *parameters, gpointer data);
83 static void select_album_action_cb (GSimpleAction *action, GVariant *parameters, gpointer data);
84
85 static void songs_view_sort_order_changed_cb (GObject *object, GParamSpec *pspec, RBBrowserSource *source);
86 static void rb_browser_source_browser_changed_cb (RBLibraryBrowser *entry,
87 GParamSpec *param,
88 RBBrowserSource *source);
89
90 /* source methods */
91 static RBEntryView *impl_get_entry_view (RBSource *source);
92 static GList *impl_get_property_views (RBSource *source);
93 static void impl_delete_selected (RBSource *source);
94 static void impl_search (RBSource *source, RBSourceSearch *search, const char *cur_text, const char *new_text);
95 static void impl_reset_filters (RBSource *source);
96 static void impl_song_properties (RBSource *source);
97 static void default_show_entry_popup (RBBrowserSource *source);
98 static void default_pack_content (RBBrowserSource *source, GtkWidget *content);
99
100 void rb_browser_source_browser_views_activated_cb (GtkWidget *widget,
101 RBBrowserSource *source);
102 static void songs_view_drag_data_received_cb (GtkWidget *widget,
103 GdkDragContext *dc,
104 gint x, gint y,
105 GtkSelectionData *data,
106 guint info, guint time,
107 RBBrowserSource *source);
108 static void rb_browser_source_do_query (RBBrowserSource *source,
109 gboolean subset);
110 static void rb_browser_source_populate (RBBrowserSource *source);
111
112 struct RBBrowserSourcePrivate
113 {
114 RhythmDB *db;
115
116 RBLibraryBrowser *browser;
117 RBEntryView *songs;
118 RBSourceToolbar *toolbar;
119
120 RhythmDBQueryModel *cached_all_query;
121 RhythmDBQuery *search_query;
122 RhythmDBPropType search_prop;
123 gboolean populate;
124 gboolean query_active;
125 gboolean search_on_completion;
126 RBSourceSearch *default_search;
127
128 GMenu *popup;
129 GMenu *search_popup;
130 GAction *search_action;
131 };
132
133 #define RB_BROWSER_SOURCE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_BROWSER_SOURCE, RBBrowserSourcePrivate))
134
135 static const GtkTargetEntry songs_view_drag_types[] = {
136 { "application/x-rhythmbox-entry", 0, 0 },
137 { "text/uri-list", 0, 1 }
138 };
139
140 enum
141 {
142 PROP_0,
143 PROP_BASE_QUERY_MODEL,
144 PROP_POPULATE,
145 PROP_SHOW_BROWSER
146 };
147
G_DEFINE_ABSTRACT_TYPE(RBBrowserSource,rb_browser_source,RB_TYPE_SOURCE)148 G_DEFINE_ABSTRACT_TYPE (RBBrowserSource, rb_browser_source, RB_TYPE_SOURCE)
149
150 static void
151 rb_browser_source_class_init (RBBrowserSourceClass *klass)
152 {
153 GObjectClass *object_class = G_OBJECT_CLASS (klass);
154 RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
155
156 object_class->dispose = rb_browser_source_dispose;
157 object_class->finalize = rb_browser_source_finalize;
158 object_class->constructed = rb_browser_source_constructed;
159
160 object_class->set_property = rb_browser_source_set_property;
161 object_class->get_property = rb_browser_source_get_property;
162
163 source_class->reset_filters = impl_reset_filters;
164 source_class->search = impl_search;
165 source_class->get_entry_view = impl_get_entry_view;
166 source_class->get_property_views = impl_get_property_views;
167 source_class->song_properties = impl_song_properties;
168 source_class->can_cut = (RBSourceFeatureFunc) rb_false_function;
169 source_class->can_copy = (RBSourceFeatureFunc) rb_true_function;
170 source_class->can_delete = (RBSourceFeatureFunc) rb_true_function;
171 source_class->can_add_to_queue = (RBSourceFeatureFunc) rb_true_function;
172 source_class->can_move_to_trash = (RBSourceFeatureFunc) rb_true_function;
173 source_class->delete_selected = impl_delete_selected;
174
175 klass->pack_content = default_pack_content;
176 klass->has_drop_support = (RBBrowserSourceFeatureFunc) rb_false_function;
177 klass->show_entry_popup = default_show_entry_popup;
178
179 g_object_class_override_property (object_class,
180 PROP_BASE_QUERY_MODEL,
181 "base-query-model");
182
183 g_object_class_install_property (object_class,
184 PROP_POPULATE,
185 g_param_spec_boolean ("populate",
186 "populate",
187 "whether to populate the source",
188 TRUE,
189 G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
190
191 g_object_class_override_property (object_class,
192 PROP_SHOW_BROWSER,
193 "show-browser");
194
195 g_type_class_add_private (klass, sizeof (RBBrowserSourcePrivate));
196 }
197
198 static void
rb_browser_source_init(RBBrowserSource * source)199 rb_browser_source_init (RBBrowserSource *source)
200 {
201 source->priv = RB_BROWSER_SOURCE_GET_PRIVATE (source);
202 }
203
204 static void
rb_browser_source_dispose(GObject * object)205 rb_browser_source_dispose (GObject *object)
206 {
207 RBBrowserSource *source;
208 source = RB_BROWSER_SOURCE (object);
209
210 g_clear_pointer (&source->priv->search_query, rhythmdb_query_free);
211 g_clear_object (&source->priv->db);
212 g_clear_object (&source->priv->cached_all_query);
213 g_clear_object (&source->priv->default_search);
214 g_clear_object (&source->priv->popup);
215 g_clear_object (&source->priv->search_popup);
216 g_clear_object (&source->priv->search_action);
217
218 G_OBJECT_CLASS (rb_browser_source_parent_class)->dispose (object);
219 }
220
221 static void
rb_browser_source_finalize(GObject * object)222 rb_browser_source_finalize (GObject *object)
223 {
224 RBBrowserSource *source;
225
226 g_return_if_fail (object != NULL);
227 g_return_if_fail (RB_IS_BROWSER_SOURCE (object));
228
229 source = RB_BROWSER_SOURCE (object);
230
231 g_return_if_fail (source->priv != NULL);
232
233 G_OBJECT_CLASS (rb_browser_source_parent_class)->finalize (object);
234 }
235
236 static void
rb_browser_source_songs_show_popup_cb(RBEntryView * view,gboolean over_entry,RBBrowserSource * source)237 rb_browser_source_songs_show_popup_cb (RBEntryView *view,
238 gboolean over_entry,
239 RBBrowserSource *source)
240 {
241 if (over_entry) {
242 RBBrowserSourceClass *klass = RB_BROWSER_SOURCE_GET_CLASS (source);
243
244 klass->show_entry_popup (source);
245 }
246 }
247
248 static void
default_show_entry_popup(RBBrowserSource * source)249 default_show_entry_popup (RBBrowserSource *source)
250 {
251 GtkWidget *menu;
252 GMenuModel *playlist_menu;
253
254 /* update add to playlist menu links */
255 g_object_get (source, "playlist-menu", &playlist_menu, NULL);
256 rb_menu_update_link (source->priv->popup, "rb-playlist-menu-link", playlist_menu);
257 g_clear_object (&playlist_menu);
258
259 menu = gtk_menu_new_from_model (G_MENU_MODEL (source->priv->popup));
260 gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (source), NULL);
261 gtk_menu_popup (GTK_MENU (menu),
262 NULL,
263 NULL,
264 NULL,
265 NULL,
266 3,
267 gtk_get_current_event_time ());
268 }
269
270 static void
rb_browser_source_constructed(GObject * object)271 rb_browser_source_constructed (GObject *object)
272 {
273 RBBrowserSource *source;
274 RBBrowserSourceClass *klass;
275 RBShell *shell;
276 GObject *shell_player;
277 GtkAccelGroup *accel_group;
278 RhythmDBEntryType *entry_type;
279 GtkWidget *content;
280 GtkWidget *paned;
281 GtkBuilder *builder;
282 GMenu *section;
283 GActionEntry actions[] = {
284 { "browser-select-genre", select_genre_action_cb },
285 { "browser-select-artist", select_artist_action_cb },
286 { "browser-select-album", select_album_action_cb }
287 };
288
289 RB_CHAIN_GOBJECT_METHOD (rb_browser_source_parent_class, constructed, object);
290
291 source = RB_BROWSER_SOURCE (object);
292
293 g_object_get (source,
294 "shell", &shell,
295 "entry-type", &entry_type,
296 NULL);
297 g_object_get (shell,
298 "db", &source->priv->db,
299 "shell-player", &shell_player,
300 "accel-group", &accel_group,
301 NULL);
302
303 _rb_add_display_page_actions (G_ACTION_MAP (g_application_get_default ()),
304 G_OBJECT (shell),
305 actions,
306 G_N_ELEMENTS (actions));
307 g_object_unref (shell);
308
309
310 source->priv->search_action = rb_source_create_search_action (RB_SOURCE (source));
311 g_action_map_add_action (G_ACTION_MAP (g_application_get_default ()), source->priv->search_action);
312
313 /* ensure search instances exist */
314 rb_source_search_basic_register (RHYTHMDB_PROP_SEARCH_MATCH, "search-match", _("Search all fields"));
315 rb_source_search_basic_register (RHYTHMDB_PROP_ARTIST_FOLDED, "artist", _("Search artists"));
316 rb_source_search_basic_register (RHYTHMDB_PROP_COMPOSER_FOLDED, "composer", _("Search composers"));
317 rb_source_search_basic_register (RHYTHMDB_PROP_ALBUM_FOLDED, "album", _("Search albums"));
318 rb_source_search_basic_register (RHYTHMDB_PROP_TITLE_FOLDED, "title", _("Search titles"));
319 rb_source_search_basic_register (RHYTHMDB_PROP_GENRE_FOLDED, "genre", _("Search genres"));
320
321 section = g_menu_new ();
322 rb_source_search_add_to_menu (section, "app", source->priv->search_action, "search-match");
323 rb_source_search_add_to_menu (section, "app", source->priv->search_action, "genre");
324 rb_source_search_add_to_menu (section, "app", source->priv->search_action, "artist");
325 rb_source_search_add_to_menu (section, "app", source->priv->search_action, "composer");
326 rb_source_search_add_to_menu (section, "app", source->priv->search_action, "album");
327 rb_source_search_add_to_menu (section, "app", source->priv->search_action, "title");
328
329 source->priv->search_popup = g_menu_new ();
330 g_menu_append_section (source->priv->search_popup, NULL, G_MENU_MODEL (section));
331
332 source->priv->default_search = rb_source_search_basic_new (RHYTHMDB_PROP_SEARCH_MATCH, _("Search all fields"));
333
334 paned = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
335
336 source->priv->browser = rb_library_browser_new (source->priv->db, entry_type);
337 gtk_widget_set_no_show_all (GTK_WIDGET (source->priv->browser), TRUE);
338 gtk_paned_pack1 (GTK_PANED (paned), GTK_WIDGET (source->priv->browser), TRUE, FALSE);
339 gtk_container_child_set (GTK_CONTAINER (paned),
340 GTK_WIDGET (source->priv->browser),
341 "resize", FALSE,
342 NULL);
343 g_signal_connect_object (G_OBJECT (source->priv->browser), "notify::output-model",
344 G_CALLBACK (rb_browser_source_browser_changed_cb),
345 source, 0);
346
347 /* set up songs tree view */
348 source->priv->songs = rb_entry_view_new (source->priv->db, shell_player,
349 TRUE, FALSE);
350
351 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_TRACK_NUMBER, FALSE);
352 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_TITLE, TRUE);
353 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_GENRE, FALSE);
354 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_ARTIST, FALSE);
355 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_ALBUM, FALSE);
356 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_COMPOSER, FALSE);
357 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_YEAR, FALSE);
358 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_DURATION, FALSE);
359 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_QUALITY, FALSE);
360 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_PLAY_COUNT, FALSE);
361 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_BPM, FALSE);
362 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_COMMENT, FALSE);
363 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_LOCATION, FALSE);
364
365 g_signal_connect_object (G_OBJECT (source->priv->songs), "show_popup",
366 G_CALLBACK (rb_browser_source_songs_show_popup_cb), source, 0);
367 g_signal_connect_object (source->priv->songs,
368 "notify::sort-order",
369 G_CALLBACK (songs_view_sort_order_changed_cb),
370 source, 0);
371
372 rb_source_bind_settings (RB_SOURCE (source),
373 GTK_WIDGET (source->priv->songs),
374 paned,
375 GTK_WIDGET (source->priv->browser),
376 TRUE);
377
378 if (rb_browser_source_has_drop_support (source)) {
379 gtk_drag_dest_set (GTK_WIDGET (source->priv->songs),
380 GTK_DEST_DEFAULT_ALL,
381 songs_view_drag_types, G_N_ELEMENTS (songs_view_drag_types),
382 GDK_ACTION_COPY | GDK_ACTION_MOVE); /* really accept move actions? */
383
384 /* set up drag and drop for the song tree view.
385 * we don't use RBEntryView's DnD support because it does too much.
386 * we just want to be able to drop songs in to add them to the
387 * library.
388 */
389 g_signal_connect_object (G_OBJECT (source->priv->songs),
390 "drag_data_received",
391 G_CALLBACK (songs_view_drag_data_received_cb),
392 source, 0);
393 }
394
395 gtk_paned_pack2 (GTK_PANED (paned), GTK_WIDGET (source->priv->songs), TRUE, FALSE);
396
397 /* set up toolbar */
398 source->priv->toolbar = rb_source_toolbar_new (RB_DISPLAY_PAGE (source), accel_group);
399 rb_source_toolbar_add_search_entry_menu (source->priv->toolbar, G_MENU_MODEL (source->priv->search_popup), source->priv->search_action);
400
401 content = gtk_grid_new ();
402 gtk_grid_set_column_spacing (GTK_GRID (content), 6);
403 gtk_grid_set_row_spacing (GTK_GRID (content), 6);
404 gtk_widget_set_margin_top (content, 6);
405 gtk_grid_attach (GTK_GRID (content), GTK_WIDGET (source->priv->toolbar), 0, 0, 1, 1);
406 gtk_widget_set_vexpand (paned, TRUE);
407 gtk_widget_set_hexpand (paned, TRUE);
408 gtk_grid_attach (GTK_GRID (content), paned, 0, 1, 1, 1);
409
410 klass = RB_BROWSER_SOURCE_GET_CLASS (source);
411 klass->pack_content (source, content);
412
413 gtk_widget_show_all (GTK_WIDGET (source));
414
415 /* use a throwaway model until the real one is ready */
416 rb_library_browser_set_model (source->priv->browser,
417 rhythmdb_query_model_new_empty (source->priv->db),
418 FALSE);
419
420 source->priv->cached_all_query = rhythmdb_query_model_new_empty (source->priv->db);
421 rb_browser_source_populate (source);
422
423 builder = rb_builder_load ("browser-popup.ui", NULL);
424 source->priv->popup = G_MENU (gtk_builder_get_object (builder, "browser-popup"));
425 rb_application_link_shared_menus (RB_APPLICATION (g_application_get_default ()),
426 source->priv->popup);
427 g_object_ref (source->priv->popup);
428 g_object_unref (builder);
429
430 g_object_unref (entry_type);
431 g_object_unref (shell_player);
432 g_object_unref (accel_group);
433 }
434
435 static void
rb_browser_source_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)436 rb_browser_source_set_property (GObject *object,
437 guint prop_id,
438 const GValue *value,
439 GParamSpec *pspec)
440 {
441 RBBrowserSource *source = RB_BROWSER_SOURCE (object);
442
443 switch (prop_id) {
444 case PROP_POPULATE:
445 source->priv->populate = g_value_get_boolean (value);
446
447 /* if being set after construction, run the query now. otherwise the constructor will do it. */
448 if (source->priv->songs != NULL) {
449 rb_browser_source_populate (source);
450 }
451 break;
452 case PROP_SHOW_BROWSER:
453 if (g_value_get_boolean (value)) {
454 gtk_widget_show (GTK_WIDGET (source->priv->browser));
455 } else {
456 gtk_widget_hide (GTK_WIDGET (source->priv->browser));
457 rb_library_browser_reset (source->priv->browser);
458 }
459 break;
460 default:
461 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
462 break;
463 }
464 }
465
466 static void
rb_browser_source_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)467 rb_browser_source_get_property (GObject *object,
468 guint prop_id,
469 GValue *value,
470 GParamSpec *pspec)
471 {
472 RBBrowserSource *source = RB_BROWSER_SOURCE (object);
473
474 switch (prop_id) {
475 case PROP_BASE_QUERY_MODEL:
476 g_value_set_object (value, source->priv->cached_all_query);
477 break;
478 case PROP_POPULATE:
479 g_value_set_boolean (value, source->priv->populate);
480 break;
481 case PROP_SHOW_BROWSER:
482 g_value_set_boolean (value, gtk_widget_get_visible (GTK_WIDGET (source->priv->browser)));
483 break;
484 default:
485 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
486 break;
487 }
488 }
489
490 static void
cached_all_query_complete_cb(RhythmDBQueryModel * model,RBBrowserSource * source)491 cached_all_query_complete_cb (RhythmDBQueryModel *model, RBBrowserSource *source)
492 {
493 rb_library_browser_set_model (source->priv->browser,
494 source->priv->cached_all_query,
495 FALSE);
496 }
497
498 static void
rb_browser_source_populate(RBBrowserSource * source)499 rb_browser_source_populate (RBBrowserSource *source)
500 {
501 RhythmDBEntryType *entry_type;
502
503 if (source->priv->populate == FALSE)
504 return;
505
506 /* only connect the model to the browser when it's complete. this avoids
507 * thousands of row-added signals, which is ridiculously slow with a11y enabled.
508 */
509 g_signal_connect_object (source->priv->cached_all_query,
510 "complete",
511 G_CALLBACK (cached_all_query_complete_cb),
512 source, 0);
513
514 g_object_get (source, "entry-type", &entry_type, NULL);
515 rhythmdb_do_full_query_async (source->priv->db,
516 RHYTHMDB_QUERY_RESULTS (source->priv->cached_all_query),
517 RHYTHMDB_QUERY_PROP_EQUALS, RHYTHMDB_PROP_TYPE, entry_type,
518 RHYTHMDB_QUERY_END);
519 g_object_unref (entry_type);
520 }
521
522 static void
browse_property(RBBrowserSource * source,RhythmDBPropType prop)523 browse_property (RBBrowserSource *source, RhythmDBPropType prop)
524 {
525 GList *props;
526 RBPropertyView *view;
527
528 props = rb_source_gather_selected_properties (RB_SOURCE (source), prop);
529 view = rb_library_browser_get_property_view (source->priv->browser, prop);
530 if (view) {
531 rb_property_view_set_selection (view, props);
532 }
533
534 rb_list_deep_free (props);
535 }
536
537 static void
select_genre_action_cb(GSimpleAction * action,GVariant * parameters,gpointer data)538 select_genre_action_cb (GSimpleAction *action, GVariant *parameters, gpointer data)
539 {
540 rb_debug ("choosing genre");
541
542 if (RB_IS_BROWSER_SOURCE (data)) {
543 browse_property (RB_BROWSER_SOURCE (data), RHYTHMDB_PROP_GENRE);
544 }
545 }
546
547 static void
select_artist_action_cb(GSimpleAction * action,GVariant * parameters,gpointer data)548 select_artist_action_cb (GSimpleAction *action, GVariant *parameters, gpointer data)
549 {
550 rb_debug ("choosing artist");
551
552 if (RB_IS_BROWSER_SOURCE (data)) {
553 browse_property (RB_BROWSER_SOURCE (data), RHYTHMDB_PROP_ARTIST);
554 }
555 }
556
557 static void
select_album_action_cb(GSimpleAction * action,GVariant * parameters,gpointer data)558 select_album_action_cb (GSimpleAction *action, GVariant *parameters, gpointer data)
559 {
560 rb_debug ("choosing album");
561
562 if (RB_IS_BROWSER_SOURCE (data)) {
563 browse_property (RB_BROWSER_SOURCE (data), RHYTHMDB_PROP_ALBUM);
564 }
565 }
566
567 static void
songs_view_sort_order_changed_cb(GObject * object,GParamSpec * pspec,RBBrowserSource * source)568 songs_view_sort_order_changed_cb (GObject *object, GParamSpec *pspec, RBBrowserSource *source)
569 {
570 rb_debug ("sort order changed");
571 rb_entry_view_resort_model (RB_ENTRY_VIEW (object));
572 }
573
574 static void
impl_search(RBSource * asource,RBSourceSearch * search,const char * cur_text,const char * new_text)575 impl_search (RBSource *asource, RBSourceSearch *search, const char *cur_text, const char *new_text)
576 {
577 RBBrowserSource *source = RB_BROWSER_SOURCE (asource);
578 gboolean subset;
579
580 if (search == NULL) {
581 search = source->priv->default_search;
582 }
583
584 /* replace our search query */
585 if (source->priv->search_query != NULL) {
586 rhythmdb_query_free (source->priv->search_query);
587 source->priv->search_query = NULL;
588 }
589 source->priv->search_query = rb_source_search_create_query (search, source->priv->db, new_text);
590
591 /* for subset searches, we have to wait until the query
592 * has finished before we can refine the results.
593 */
594 subset = rb_source_search_is_subset (search, cur_text, new_text);
595 if (source->priv->query_active && subset) {
596 rb_debug ("deferring search for \"%s\" until query completion", new_text ? new_text : "<NULL>");
597 source->priv->search_on_completion = TRUE;
598 } else {
599 rb_debug ("doing search for \"%s\"", new_text ? new_text : "<NULL>");
600 rb_browser_source_do_query (source, subset);
601 }
602 }
603
604 static RBEntryView *
impl_get_entry_view(RBSource * asource)605 impl_get_entry_view (RBSource *asource)
606 {
607 RBBrowserSource *source = RB_BROWSER_SOURCE (asource);
608
609 return source->priv->songs;
610 }
611
612 static GList *
impl_get_property_views(RBSource * asource)613 impl_get_property_views (RBSource *asource)
614 {
615 GList *ret;
616 RBBrowserSource *source = RB_BROWSER_SOURCE (asource);
617
618 ret = rb_library_browser_get_property_views (source->priv->browser);
619 return ret;
620 }
621
622 static void
impl_reset_filters(RBSource * asource)623 impl_reset_filters (RBSource *asource)
624 {
625 RBBrowserSource *source = RB_BROWSER_SOURCE (asource);
626 gboolean changed = FALSE;
627
628 rb_debug ("Resetting search filters");
629
630 if (rb_library_browser_reset (source->priv->browser))
631 changed = TRUE;
632
633 if (source->priv->search_query != NULL) {
634 rhythmdb_query_free (source->priv->search_query);
635 source->priv->search_query = NULL;
636 changed = TRUE;
637 }
638
639 rb_source_toolbar_clear_search_entry (source->priv->toolbar);
640
641 if (changed)
642 rb_browser_source_do_query (source, FALSE);
643 }
644
645 static void
impl_delete_selected(RBSource * asource)646 impl_delete_selected (RBSource *asource)
647 {
648 RBBrowserSource *source = RB_BROWSER_SOURCE (asource);
649 GList *sel, *tem;
650
651 sel = rb_entry_view_get_selected_entries (source->priv->songs);
652 for (tem = sel; tem != NULL; tem = tem->next) {
653 rhythmdb_entry_delete (source->priv->db, tem->data);
654 rhythmdb_commit (source->priv->db);
655 }
656 g_list_foreach (sel, (GFunc)rhythmdb_entry_unref, NULL);
657 g_list_free (sel);
658 }
659
660 static void
impl_song_properties(RBSource * asource)661 impl_song_properties (RBSource *asource)
662 {
663 RBBrowserSource *source = RB_BROWSER_SOURCE (asource);
664 GtkWidget *song_info = NULL;
665
666 g_return_if_fail (source->priv->songs != NULL);
667
668 song_info = rb_song_info_new (asource, NULL);
669
670 g_return_if_fail (song_info != NULL);
671
672 if (song_info)
673 gtk_widget_show_all (song_info);
674 else
675 rb_debug ("failed to create dialog, or no selection!");
676 }
677
678 /**
679 * rb_browser_source_has_drop_support:
680 * @source: a #RBBrowserSource
681 *
682 * This is a virtual method that should be implemented by subclasses. It returns %TRUE
683 * if drag and drop target support for the source should be activated.
684 *
685 * Return value: %TRUE if drop support should be activated
686 */
687 gboolean
rb_browser_source_has_drop_support(RBBrowserSource * source)688 rb_browser_source_has_drop_support (RBBrowserSource *source)
689 {
690 RBBrowserSourceClass *klass = RB_BROWSER_SOURCE_GET_CLASS (source);
691
692 return klass->has_drop_support (source);
693 }
694
695 static void
songs_view_drag_data_received_cb(GtkWidget * widget,GdkDragContext * dc,gint x,gint y,GtkSelectionData * selection_data,guint info,guint time,RBBrowserSource * source)696 songs_view_drag_data_received_cb (GtkWidget *widget,
697 GdkDragContext *dc,
698 gint x, gint y,
699 GtkSelectionData *selection_data,
700 guint info, guint time,
701 RBBrowserSource *source)
702 {
703 rb_debug ("data dropped on the library source song view");
704 rb_display_page_receive_drag (RB_DISPLAY_PAGE (source), selection_data);
705 }
706
707 static void
rb_browser_source_browser_changed_cb(RBLibraryBrowser * browser,GParamSpec * pspec,RBBrowserSource * source)708 rb_browser_source_browser_changed_cb (RBLibraryBrowser *browser,
709 GParamSpec *pspec,
710 RBBrowserSource *source)
711 {
712 RhythmDBQueryModel *query_model;
713
714 g_object_get (browser, "output-model", &query_model, NULL);
715 rb_entry_view_set_model (source->priv->songs, query_model);
716 g_object_set (source, "query-model", query_model, NULL);
717 g_object_unref (query_model);
718
719 rb_source_notify_filter_changed (RB_SOURCE (source));
720 }
721
722 static void
rb_browser_source_query_complete_cb(RhythmDBQueryModel * query_model,RBBrowserSource * source)723 rb_browser_source_query_complete_cb (RhythmDBQueryModel *query_model,
724 RBBrowserSource *source)
725 {
726 rb_library_browser_set_model (source->priv->browser, query_model, FALSE);
727
728 source->priv->query_active = FALSE;
729 if (source->priv->search_on_completion) {
730 rb_debug ("performing deferred search");
731 source->priv->search_on_completion = FALSE;
732 /* this is only done for subset queries */
733 rb_browser_source_do_query (source, TRUE);
734 }
735 }
736
737 static void
rb_browser_source_do_query(RBBrowserSource * source,gboolean subset)738 rb_browser_source_do_query (RBBrowserSource *source, gboolean subset)
739 {
740 RhythmDBQueryModel *query_model;
741 GPtrArray *query;
742 RhythmDBEntryType *entry_type;
743
744 /* use the cached 'all' query to optimise the no-search case */
745 if (source->priv->search_query == NULL) {
746 rb_library_browser_set_model (source->priv->browser,
747 source->priv->cached_all_query,
748 FALSE);
749 return;
750 }
751
752 g_object_get (source, "entry-type", &entry_type, NULL);
753 query = rhythmdb_query_parse (source->priv->db,
754 RHYTHMDB_QUERY_PROP_EQUALS,
755 RHYTHMDB_PROP_TYPE,
756 entry_type,
757 RHYTHMDB_QUERY_SUBQUERY,
758 source->priv->search_query,
759 RHYTHMDB_QUERY_END);
760 g_object_unref (entry_type);
761
762 if (subset) {
763 /* if we're appending text to an existing search string, the results will be a subset
764 * of the existing results, so rather than doing a whole new query, we can copy the
765 * results to a new query model with a more restrictive query.
766 */
767 RhythmDBQueryModel *old;
768 g_object_get (source->priv->browser, "input-model", &old, NULL);
769
770 query_model = rhythmdb_query_model_new_empty (source->priv->db);
771 g_object_set (query_model, "query", query, NULL);
772 rhythmdb_query_model_copy_contents (query_model, old);
773 g_object_unref (old);
774
775 rb_library_browser_set_model (source->priv->browser, query_model, FALSE);
776 g_object_unref (query_model);
777
778 } else {
779 /* otherwise build a query based on the search text, and feed it to the browser
780 * when the query finishes.
781 */
782 query_model = rhythmdb_query_model_new_empty (source->priv->db);
783 source->priv->query_active = TRUE;
784 source->priv->search_on_completion = FALSE;
785 g_signal_connect_object (query_model,
786 "complete", G_CALLBACK (rb_browser_source_query_complete_cb),
787 source, 0);
788 rhythmdb_do_full_query_async_parsed (source->priv->db,
789 RHYTHMDB_QUERY_RESULTS (query_model),
790 query);
791 g_object_unref (query_model);
792 }
793
794 rhythmdb_query_free (query);
795 }
796
797 static void
default_pack_content(RBBrowserSource * source,GtkWidget * content)798 default_pack_content (RBBrowserSource *source, GtkWidget *content)
799 {
800 gtk_container_add (GTK_CONTAINER (source), content);
801 }
802