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