hythmbox-2.98/sources/rb-browser-source.c

No issues found

  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 
 66 static void rb_browser_source_class_init (RBBrowserSourceClass *klass);
 67 static void rb_browser_source_init (RBBrowserSource *source);
 68 static void rb_browser_source_constructed (GObject *object);
 69 static void rb_browser_source_dispose (GObject *object);
 70 static void rb_browser_source_finalize (GObject *object);
 71 static void rb_browser_source_set_property (GObject *object,
 72 			                  guint prop_id,
 73 			                  const GValue *value,
 74 			                  GParamSpec *pspec);
 75 static void rb_browser_source_get_property (GObject *object,
 76 			                  guint prop_id,
 77 			                  GValue *value,
 78 			                  GParamSpec *pspec);
 79 static void rb_browser_source_cmd_choose_genre (GtkAction *action, RBSource *source);
 80 static void rb_browser_source_cmd_choose_artist (GtkAction *action, RBSource *source);
 81 static void rb_browser_source_cmd_choose_album (GtkAction *action, RBSource *source);
 82 static void songs_view_sort_order_changed_cb (GObject *object, GParamSpec *pspec, RBBrowserSource *source);
 83 static void rb_browser_source_browser_changed_cb (RBLibraryBrowser *entry,
 84 						  GParamSpec *param,
 85 						  RBBrowserSource *source);
 86 
 87 /* source methods */
 88 static RBEntryView *impl_get_entry_view (RBSource *source);
 89 static GList *impl_get_property_views (RBSource *source);
 90 static void impl_delete (RBSource *source);
 91 static void impl_search (RBSource *source, RBSourceSearch *search, const char *cur_text, const char *new_text);
 92 static void impl_reset_filters (RBSource *source);
 93 static void impl_song_properties (RBSource *source);
 94 static void default_show_entry_popup (RBBrowserSource *source);
 95 static void default_pack_content (RBBrowserSource *source, GtkWidget *content);
 96 
 97 void rb_browser_source_browser_views_activated_cb (GtkWidget *widget,
 98 						 RBBrowserSource *source);
 99 static void songs_view_drag_data_received_cb (GtkWidget *widget,
100 					      GdkDragContext *dc,
101 					      gint x, gint y,
102 					      GtkSelectionData *data,
103 					      guint info, guint time,
104 					      RBBrowserSource *source);
105 static void rb_browser_source_do_query (RBBrowserSource *source,
106 					gboolean subset);
107 static void rb_browser_source_populate (RBBrowserSource *source);
108 
109 struct RBBrowserSourcePrivate
110 {
111 	RhythmDB *db;
112 
113 	RBLibraryBrowser *browser;
114 	RBEntryView *songs;
115 	RBSourceToolbar *toolbar;
116 
117 	RhythmDBQueryModel *cached_all_query;
118 	RhythmDBQuery *search_query;
119 	RhythmDBPropType search_prop;
120 	gboolean populate;
121 	gboolean query_active;
122 	gboolean search_on_completion;
123 	RBSourceSearch *default_search;
124 
125 	GtkActionGroup *action_group;
126 	GtkActionGroup *search_action_group;
127 
128 	gboolean dispose_has_run;
129 };
130 
131 #define RB_BROWSER_SOURCE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_BROWSER_SOURCE, RBBrowserSourcePrivate))
132 
133 static GtkActionEntry rb_browser_source_actions [] =
134 {
135 	{ "BrowserSrcChooseGenre", NULL, N_("Browse This _Genre"), NULL,
136 	  N_("Set the browser to view only this genre"),
137 	  G_CALLBACK (rb_browser_source_cmd_choose_genre) },
138 	{ "BrowserSrcChooseArtist", NULL , N_("Browse This _Artist"), NULL,
139 	  N_("Set the browser to view only this artist"),
140 	  G_CALLBACK (rb_browser_source_cmd_choose_artist) },
141 	{ "BrowserSrcChooseAlbum", NULL, N_("Browse This A_lbum"), NULL,
142 	  N_("Set the browser to view only this album"),
143 	  G_CALLBACK (rb_browser_source_cmd_choose_album) }
144 };
145 
146 static GtkRadioActionEntry rb_browser_source_radio_actions [] =
147 {
148 	{ "BrowserSourceSearchAll", NULL, N_("Search all fields"), NULL, NULL, RHYTHMDB_PROP_SEARCH_MATCH },
149 	{ "BrowserSourceSearchArtists", NULL, N_("Search artists"), NULL, NULL, RHYTHMDB_PROP_ARTIST_FOLDED },
150 	{ "BrowserSourceSearchAlbums", NULL, N_("Search albums"), NULL, NULL, RHYTHMDB_PROP_ALBUM_FOLDED },
151 	{ "BrowserSourceSearchTitles", NULL, N_("Search titles"), NULL, NULL, RHYTHMDB_PROP_TITLE_FOLDED }
152 };
153 
154 static const GtkTargetEntry songs_view_drag_types[] = {
155 	{ "application/x-rhythmbox-entry", 0, 0 },
156 	{ "text/uri-list", 0, 1 }
157 };
158 
159 enum
160 {
161 	PROP_0,
162 	PROP_BASE_QUERY_MODEL,
163 	PROP_POPULATE,
164 	PROP_SHOW_BROWSER
165 };
166 
167 G_DEFINE_ABSTRACT_TYPE (RBBrowserSource, rb_browser_source, RB_TYPE_SOURCE)
168 
169 static void
170 rb_browser_source_class_init (RBBrowserSourceClass *klass)
171 {
172 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
173 	RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
174 
175 	object_class->dispose = rb_browser_source_dispose;
176 	object_class->finalize = rb_browser_source_finalize;
177 	object_class->constructed = rb_browser_source_constructed;
178 
179 	object_class->set_property = rb_browser_source_set_property;
180 	object_class->get_property = rb_browser_source_get_property;
181 
182 	source_class->impl_search = impl_search;
183 	source_class->impl_get_entry_view = impl_get_entry_view;
184 	source_class->impl_get_property_views = impl_get_property_views;
185 	source_class->impl_reset_filters = impl_reset_filters;
186 	source_class->impl_song_properties = impl_song_properties;
187 	source_class->impl_can_cut = (RBSourceFeatureFunc) rb_false_function;
188 	source_class->impl_can_copy = (RBSourceFeatureFunc) rb_true_function;
189 	source_class->impl_can_delete = (RBSourceFeatureFunc) rb_true_function;
190 	source_class->impl_can_add_to_queue = (RBSourceFeatureFunc) rb_true_function;
191 	source_class->impl_can_move_to_trash = (RBSourceFeatureFunc) rb_true_function;
192 	source_class->impl_delete = impl_delete;
193 
194 	klass->pack_content = default_pack_content;
195 	klass->has_drop_support = (RBBrowserSourceFeatureFunc) rb_false_function;
196 	klass->show_entry_popup = default_show_entry_popup;
197 
198 	g_object_class_override_property (object_class,
199 					  PROP_BASE_QUERY_MODEL,
200 					  "base-query-model");
201 	
202 	g_object_class_install_property (object_class,
203 					 PROP_POPULATE,
204 					 g_param_spec_boolean ("populate",
205 						 	       "populate",
206 							       "whether to populate the source",
207 							       TRUE,
208 							       G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
209 
210 	g_object_class_override_property (object_class,
211 					  PROP_SHOW_BROWSER,
212 					  "show-browser");
213 
214 	g_type_class_add_private (klass, sizeof (RBBrowserSourcePrivate));
215 }
216 
217 static void
218 rb_browser_source_init (RBBrowserSource *source)
219 {
220 	source->priv = RB_BROWSER_SOURCE_GET_PRIVATE (source);
221 }
222 
223 static void
224 rb_browser_source_dispose (GObject *object)
225 {
226 	RBBrowserSource *source;
227 	source = RB_BROWSER_SOURCE (object);
228 
229 	if (source->priv->dispose_has_run) {
230 		/* If dispose did already run, return. */
231 		return;
232 	}
233 	/* Make sure dispose does not run twice. */
234 	source->priv->dispose_has_run = TRUE;
235 
236 	if (source->priv->db != NULL) {
237 		g_object_unref (source->priv->db);
238 		source->priv->db = NULL;
239 	}
240 
241 	if (source->priv->search_query != NULL) {
242 		rhythmdb_query_free (source->priv->search_query);
243 		source->priv->search_query = NULL;
244 	}
245 
246 	if (source->priv->cached_all_query != NULL) {
247 		g_object_unref (source->priv->cached_all_query);
248 		source->priv->cached_all_query = NULL;
249 	}
250 
251 	if (source->priv->action_group != NULL) {
252 		g_object_unref (source->priv->action_group);
253 		source->priv->action_group = NULL;
254 	}
255 
256 	if (source->priv->default_search != NULL) {
257 		g_object_unref (source->priv->default_search);
258 		source->priv->default_search = NULL;
259 	}
260 
261 	G_OBJECT_CLASS (rb_browser_source_parent_class)->dispose (object);
262 }
263 
264 static void
265 rb_browser_source_finalize (GObject *object)
266 {
267 	RBBrowserSource *source;
268 
269 	g_return_if_fail (object != NULL);
270 	g_return_if_fail (RB_IS_BROWSER_SOURCE (object));
271 
272 	source = RB_BROWSER_SOURCE (object);
273 
274 	g_return_if_fail (source->priv != NULL);
275 
276 	G_OBJECT_CLASS (rb_browser_source_parent_class)->finalize (object);
277 }
278 
279 static void
280 rb_browser_source_songs_show_popup_cb (RBEntryView *view,
281 				       gboolean over_entry,
282 				       RBBrowserSource *source)
283 {
284 	if (over_entry) {
285 		RBBrowserSourceClass *klass = RB_BROWSER_SOURCE_GET_CLASS (source);
286 
287 		klass->show_entry_popup (source);
288 	} else {
289 		rb_display_page_show_popup (RB_DISPLAY_PAGE (source));
290 	}
291 }
292 
293 static void
294 default_show_entry_popup (RBBrowserSource *source)
295 {
296 	_rb_display_page_show_popup (RB_DISPLAY_PAGE (source), "/BrowserSourceViewPopup");
297 }
298 
299 static void
300 rb_browser_source_constructed (GObject *object)
301 {
302 	RBBrowserSource *source;
303 	RBBrowserSourceClass *klass;
304 	RBShell *shell;
305 	GObject *shell_player;
306 	GtkUIManager *ui_manager;
307 	RhythmDBEntryType *entry_type;
308 	GtkWidget *content;
309 	GtkWidget *paned;
310 
311 	RB_CHAIN_GOBJECT_METHOD (rb_browser_source_parent_class, constructed, object);
312 
313 	source = RB_BROWSER_SOURCE (object);
314 
315 	g_object_get (source,
316 		      "shell", &shell,
317 		      "entry-type", &entry_type,
318 		      NULL);
319 	g_object_get (shell,
320 		      "db", &source->priv->db,
321 		      "shell-player", &shell_player,
322 		      "ui-manager", &ui_manager,
323 		      NULL);
324 
325 	source->priv->action_group = _rb_display_page_register_action_group (RB_DISPLAY_PAGE (source),
326 									     "BrowserSourceActions",
327 									     NULL, 0, NULL);
328 	_rb_action_group_add_display_page_actions (source->priv->action_group,
329 						   G_OBJECT (shell),
330 						   rb_browser_source_actions,
331 						   G_N_ELEMENTS (rb_browser_source_actions));
332 
333 	/* only add the actions if we haven't already */
334 	if (gtk_action_group_get_action (source->priv->action_group,
335 					 rb_browser_source_radio_actions[0].name) == NULL) {
336 		gtk_action_group_add_radio_actions (source->priv->action_group,
337 						    rb_browser_source_radio_actions,
338 						    G_N_ELEMENTS (rb_browser_source_radio_actions),
339 						    0,
340 						    NULL,
341 						    NULL);
342 
343 		rb_source_search_basic_create_for_actions (source->priv->action_group,
344 							   rb_browser_source_radio_actions,
345 							   G_N_ELEMENTS (rb_browser_source_radio_actions));
346 	}
347 	g_object_unref (shell);
348 
349 	source->priv->default_search = rb_source_search_basic_new (RHYTHMDB_PROP_SEARCH_MATCH);
350 
351 	paned = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
352 
353 	source->priv->browser = rb_library_browser_new (source->priv->db, entry_type);
354 	gtk_widget_set_no_show_all (GTK_WIDGET (source->priv->browser), TRUE);
355 	gtk_paned_pack1 (GTK_PANED (paned), GTK_WIDGET (source->priv->browser), TRUE, FALSE);
356 	gtk_container_child_set (GTK_CONTAINER (paned),
357 				 GTK_WIDGET (source->priv->browser),
358 				 "resize", FALSE,
359 				 NULL);
360 	g_signal_connect_object (G_OBJECT (source->priv->browser), "notify::output-model",
361 				 G_CALLBACK (rb_browser_source_browser_changed_cb),
362 				 source, 0);
363 
364 	/* set up songs tree view */
365 	source->priv->songs = rb_entry_view_new (source->priv->db, shell_player,
366 						 TRUE, FALSE);
367 
368 	rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_TRACK_NUMBER, FALSE);
369 	rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_TITLE, TRUE);
370 	rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_GENRE, FALSE);
371 	rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_ARTIST, FALSE);
372 	rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_ALBUM, FALSE);
373 	rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_YEAR, FALSE);
374 	rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_DURATION, FALSE);
375  	rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_QUALITY, FALSE);
376 	rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_PLAY_COUNT, FALSE);
377 	rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_BPM, FALSE);
378 	rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_COMMENT, FALSE);
379 	rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_LOCATION, FALSE);
380 
381 	g_signal_connect_object (G_OBJECT (source->priv->songs), "show_popup",
382 				 G_CALLBACK (rb_browser_source_songs_show_popup_cb), source, 0);
383 	g_signal_connect_object (source->priv->songs,
384 				 "notify::sort-order",
385 				 G_CALLBACK (songs_view_sort_order_changed_cb),
386 				 source, 0);
387 
388 	rb_source_bind_settings (RB_SOURCE (source),
389 				 GTK_WIDGET (source->priv->songs),
390 				 paned,
391 				 GTK_WIDGET (source->priv->browser));
392 
393 	if (rb_browser_source_has_drop_support (source)) {
394 		gtk_drag_dest_set (GTK_WIDGET (source->priv->songs),
395 				   GTK_DEST_DEFAULT_ALL,
396 				   songs_view_drag_types, G_N_ELEMENTS (songs_view_drag_types),
397 				   GDK_ACTION_COPY | GDK_ACTION_MOVE);	/* really accept move actions? */
398 
399 		/* set up drag and drop for the song tree view.
400 		 * we don't use RBEntryView's DnD support because it does too much.
401 		 * we just want to be able to drop songs in to add them to the
402 		 * library.
403 		 */
404 		g_signal_connect_object (G_OBJECT (source->priv->songs),
405 					 "drag_data_received",
406 					 G_CALLBACK (songs_view_drag_data_received_cb),
407 					 source, 0);
408 	}
409 
410 	gtk_paned_pack2 (GTK_PANED (paned), GTK_WIDGET (source->priv->songs), TRUE, FALSE);
411 
412 	/* set up toolbar */
413 	source->priv->toolbar = rb_source_toolbar_new (RB_DISPLAY_PAGE (source), ui_manager);
414 	rb_source_toolbar_add_search_entry (source->priv->toolbar, "/BrowserSourceSearchMenu", NULL);
415 
416 	content = gtk_grid_new ();
417 	gtk_grid_set_column_spacing (GTK_GRID (content), 6);
418 	gtk_grid_set_row_spacing (GTK_GRID (content), 6);
419 	gtk_widget_set_margin_top (content, 6);
420 	gtk_grid_attach (GTK_GRID (content), GTK_WIDGET (source->priv->toolbar), 0, 0, 1, 1);
421 	gtk_widget_set_vexpand (paned, TRUE);
422 	gtk_widget_set_hexpand (paned, TRUE);
423 	gtk_grid_attach (GTK_GRID (content), paned, 0, 1, 1, 1);
424 
425 	klass = RB_BROWSER_SOURCE_GET_CLASS (source);
426 	klass->pack_content (source, content);
427 
428 	gtk_widget_show_all (GTK_WIDGET (source));
429 
430 	/* use a throwaway model until the real one is ready */
431 	rb_library_browser_set_model (source->priv->browser,
432 				      rhythmdb_query_model_new_empty (source->priv->db),
433 				      FALSE);
434 
435 	source->priv->cached_all_query = rhythmdb_query_model_new_empty (source->priv->db);
436 	rb_browser_source_populate (source);
437 
438 	g_object_unref (entry_type);
439 	g_object_unref (shell_player);
440 }
441 
442 static void
443 rb_browser_source_set_property (GObject *object,
444 				guint prop_id,
445 				const GValue *value,
446 				GParamSpec *pspec)
447 {
448 	RBBrowserSource *source = RB_BROWSER_SOURCE (object);
449 
450 	switch (prop_id) {
451 	case PROP_POPULATE:
452 		source->priv->populate = g_value_get_boolean (value);
453 
454 		/* if being set after construction, run the query now.  otherwise the constructor will do it. */
455 		if (source->priv->songs != NULL) {
456 			rb_browser_source_populate (source);
457 		}
458 		break;
459 	case PROP_SHOW_BROWSER:
460 		if (g_value_get_boolean (value)) {
461 			gtk_widget_show (GTK_WIDGET (source->priv->browser));
462 		} else {
463 			gtk_widget_hide (GTK_WIDGET (source->priv->browser));
464 			rb_library_browser_reset (source->priv->browser);
465 		}
466 		break;
467 	default:
468 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
469 		break;
470 	}
471 }
472 
473 static void
474 rb_browser_source_get_property (GObject *object,
475 				guint prop_id,
476 				GValue *value,
477 				GParamSpec *pspec)
478 {
479 	RBBrowserSource *source = RB_BROWSER_SOURCE (object);
480 
481 	switch (prop_id) {
482 	case PROP_BASE_QUERY_MODEL:
483 		g_value_set_object (value, source->priv->cached_all_query);
484 		break;
485 	case PROP_POPULATE:
486 		g_value_set_boolean (value, source->priv->populate);
487 		break;
488 	case PROP_SHOW_BROWSER:
489 		g_value_set_boolean (value, gtk_widget_get_visible (GTK_WIDGET (source->priv->browser)));
490 		break;
491 	default:
492 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
493 		break;
494 	}
495 }
496 
497 static void
498 cached_all_query_complete_cb (RhythmDBQueryModel *model, RBBrowserSource *source)
499 {
500 	rb_library_browser_set_model (source->priv->browser,
501 				      source->priv->cached_all_query,
502 				      FALSE);
503 }
504 
505 static void
506 rb_browser_source_populate (RBBrowserSource *source)
507 {
508 	RhythmDBEntryType *entry_type;
509 
510 	if (source->priv->populate == FALSE)
511 		return;
512 
513 	/* only connect the model to the browser when it's complete.  this avoids
514 	 * thousands of row-added signals, which is ridiculously slow with a11y enabled.
515 	 */
516 	g_signal_connect_object (source->priv->cached_all_query,
517 				 "complete",
518 				 G_CALLBACK (cached_all_query_complete_cb),
519 				 source, 0);
520 
521 	g_object_get (source, "entry-type", &entry_type, NULL);
522 	rhythmdb_do_full_query_async (source->priv->db,
523 				      RHYTHMDB_QUERY_RESULTS (source->priv->cached_all_query),
524 				      RHYTHMDB_QUERY_PROP_EQUALS, RHYTHMDB_PROP_TYPE, entry_type,
525 				      RHYTHMDB_QUERY_END);
526 	g_object_unref (entry_type);
527 }
528 
529 static void
530 browse_property (RBBrowserSource *source, RhythmDBPropType prop)
531 {
532 	GList *props;
533 	RBPropertyView *view;
534 
535 	props = rb_source_gather_selected_properties (RB_SOURCE (source), prop);
536 	view = rb_library_browser_get_property_view (source->priv->browser, prop);
537 	if (view) {
538 		rb_property_view_set_selection (view, props);
539 	}
540 
541 	rb_list_deep_free (props);
542 }
543 
544 static void
545 rb_browser_source_cmd_choose_genre (GtkAction *action, RBSource *source)
546 {
547 	rb_debug ("choosing genre");
548 
549 	if (RB_IS_BROWSER_SOURCE (source)) {
550 		browse_property (RB_BROWSER_SOURCE (source), RHYTHMDB_PROP_GENRE);
551 	}
552 }
553 
554 static void
555 rb_browser_source_cmd_choose_artist (GtkAction *action, RBSource *source)
556 {
557 	rb_debug ("choosing artist");
558 
559 	if (RB_IS_BROWSER_SOURCE (source)) {
560 		browse_property (RB_BROWSER_SOURCE (source), RHYTHMDB_PROP_ARTIST);
561 	}
562 }
563 
564 static void
565 rb_browser_source_cmd_choose_album (GtkAction *action, RBSource *source)
566 {
567 	rb_debug ("choosing album");
568 
569 	if (RB_IS_BROWSER_SOURCE (source)) {
570 		browse_property (RB_BROWSER_SOURCE (source), RHYTHMDB_PROP_ALBUM);
571 	}
572 }
573 
574 static void
575 songs_view_sort_order_changed_cb (GObject *object, GParamSpec *pspec, RBBrowserSource *source)
576 {
577 	rb_debug ("sort order changed");
578 	rb_entry_view_resort_model (RB_ENTRY_VIEW (object));
579 }
580 
581 static void
582 impl_search (RBSource *asource, RBSourceSearch *search, const char *cur_text, const char *new_text)
583 {
584 	RBBrowserSource *source = RB_BROWSER_SOURCE (asource);
585 	gboolean subset;
586 
587 	if (search == NULL) {
588 		search = source->priv->default_search;
589 	}
590 
591 	/* replace our search query */
592 	if (source->priv->search_query != NULL) {
593 		rhythmdb_query_free (source->priv->search_query);
594 		source->priv->search_query = NULL;
595 	}
596 	source->priv->search_query = rb_source_search_create_query (search, source->priv->db, new_text);
597 
598 	/* for subset searches, we have to wait until the query
599 	 * has finished before we can refine the results.
600 	 */
601 	subset = rb_source_search_is_subset (search, cur_text, new_text);
602 	if (source->priv->query_active && subset) {
603 		rb_debug ("deferring search for \"%s\" until query completion", new_text ? new_text : "<NULL>");
604 		source->priv->search_on_completion = TRUE;
605 	} else {
606 		rb_debug ("doing search for \"%s\"", new_text ? new_text : "<NULL>");
607 		rb_browser_source_do_query (source, subset);
608 	}
609 }
610 
611 static RBEntryView *
612 impl_get_entry_view (RBSource *asource)
613 {
614 	RBBrowserSource *source = RB_BROWSER_SOURCE (asource);
615 
616 	return source->priv->songs;
617 }
618 
619 static GList *
620 impl_get_property_views (RBSource *asource)
621 {
622 	GList *ret;
623 	RBBrowserSource *source = RB_BROWSER_SOURCE (asource);
624 
625 	ret =  rb_library_browser_get_property_views (source->priv->browser);
626 	return ret;
627 }
628 
629 static void
630 impl_reset_filters (RBSource *asource)
631 {
632 	RBBrowserSource *source = RB_BROWSER_SOURCE (asource);
633 	gboolean changed = FALSE;
634 
635 	rb_debug ("Resetting search filters");
636 
637 	if (rb_library_browser_reset (source->priv->browser))
638 		changed = TRUE;
639 
640 	if (source->priv->search_query != NULL) {
641 		rhythmdb_query_free (source->priv->search_query);
642 		source->priv->search_query = NULL;
643 		changed = TRUE;
644 	}
645 
646 	rb_source_toolbar_clear_search_entry (source->priv->toolbar);
647 
648 	if (changed)
649 		rb_browser_source_do_query (source, FALSE);
650 }
651 
652 static void
653 impl_delete (RBSource *asource)
654 {
655 	RBBrowserSource *source = RB_BROWSER_SOURCE (asource);
656 	GList *sel, *tem;
657 
658 	sel = rb_entry_view_get_selected_entries (source->priv->songs);
659 	for (tem = sel; tem != NULL; tem = tem->next) {
660 		rhythmdb_entry_delete (source->priv->db, tem->data);
661 		rhythmdb_commit (source->priv->db);
662 	}
663 	g_list_foreach (sel, (GFunc)rhythmdb_entry_unref, NULL);
664 	g_list_free (sel);
665 }
666 
667 static void
668 impl_song_properties (RBSource *asource)
669 {
670 	RBBrowserSource *source = RB_BROWSER_SOURCE (asource);
671  	GtkWidget *song_info = NULL;
672 
673 	g_return_if_fail (source->priv->songs != NULL);
674 
675  	song_info = rb_song_info_new (asource, NULL);
676 
677         g_return_if_fail (song_info != NULL);
678 
679  	if (song_info)
680  		gtk_widget_show_all (song_info);
681  	else
682 		rb_debug ("failed to create dialog, or no selection!");
683 }
684 
685 /**
686  * rb_browser_source_has_drop_support:
687  * @source: a #RBBrowserSource
688  *
689  * This is a virtual method that should be implemented by subclasses.  It returns %TRUE
690  * if drag and drop target support for the source should be activated.
691  *
692  * Return value: %TRUE if drop support should be activated
693  */
694 gboolean
695 rb_browser_source_has_drop_support (RBBrowserSource *source)
696 {
697 	RBBrowserSourceClass *klass = RB_BROWSER_SOURCE_GET_CLASS (source);
698 
699 	return klass->has_drop_support (source);
700 }
701 
702 static void
703 songs_view_drag_data_received_cb (GtkWidget *widget,
704 				  GdkDragContext *dc,
705 				  gint x, gint y,
706 				  GtkSelectionData *selection_data,
707 				  guint info, guint time,
708 				  RBBrowserSource *source)
709 {
710 	rb_debug ("data dropped on the library source song view");
711 	rb_display_page_receive_drag (RB_DISPLAY_PAGE (source), selection_data);
712 }
713 
714 static void
715 rb_browser_source_browser_changed_cb (RBLibraryBrowser *browser,
716 				      GParamSpec *pspec,
717 				      RBBrowserSource *source)
718 {
719 	RhythmDBQueryModel *query_model;
720 
721 	g_object_get (browser, "output-model", &query_model, NULL);
722 	rb_entry_view_set_model (source->priv->songs, query_model);
723 	g_object_set (source, "query-model", query_model, NULL);
724 	g_object_unref (query_model);
725 
726 	rb_source_notify_filter_changed (RB_SOURCE (source));
727 }
728 
729 static void
730 rb_browser_source_query_complete_cb (RhythmDBQueryModel *query_model,
731 				     RBBrowserSource *source)
732 {
733 	rb_library_browser_set_model (source->priv->browser, query_model, FALSE);
734 
735 	source->priv->query_active = FALSE;
736 	if (source->priv->search_on_completion) {
737 		rb_debug ("performing deferred search");
738 		source->priv->search_on_completion = FALSE;
739 		/* this is only done for subset queries */
740 		rb_browser_source_do_query (source, TRUE);
741 	}
742 }
743 
744 static void
745 rb_browser_source_do_query (RBBrowserSource *source, gboolean subset)
746 {
747 	RhythmDBQueryModel *query_model;
748 	GPtrArray *query;
749 	RhythmDBEntryType *entry_type;
750 
751 	/* use the cached 'all' query to optimise the no-search case */
752 	if (source->priv->search_query == NULL) {
753 		rb_library_browser_set_model (source->priv->browser,
754 					      source->priv->cached_all_query,
755 					      FALSE);
756 		return;
757 	}
758 
759 	g_object_get (source, "entry-type", &entry_type, NULL);
760 	query = rhythmdb_query_parse (source->priv->db,
761 				      RHYTHMDB_QUERY_PROP_EQUALS,
762 				      RHYTHMDB_PROP_TYPE,
763 				      entry_type,
764 				      RHYTHMDB_QUERY_SUBQUERY,
765 				      source->priv->search_query,
766 				      RHYTHMDB_QUERY_END);
767 	g_object_unref (entry_type);
768 
769 	if (subset) {
770 		/* if we're appending text to an existing search string, the results will be a subset
771 		 * of the existing results, so rather than doing a whole new query, we can copy the
772 		 * results to a new query model with a more restrictive query.
773 		 */
774 		RhythmDBQueryModel *old;
775 		g_object_get (source->priv->browser, "input-model", &old, NULL);
776 
777 		query_model = rhythmdb_query_model_new_empty (source->priv->db);
778 		g_object_set (query_model, "query", query, NULL);
779 		rhythmdb_query_model_copy_contents (query_model, old);
780 		g_object_unref (old);
781 
782 		rb_library_browser_set_model (source->priv->browser, query_model, FALSE);
783 		g_object_unref (query_model);
784 
785 	} else {
786 		/* otherwise build a query based on the search text, and feed it to the browser
787 		 * when the query finishes.
788 		 */ 
789 		query_model = rhythmdb_query_model_new_empty (source->priv->db);
790 		source->priv->query_active = TRUE;
791 		source->priv->search_on_completion = FALSE;
792 		g_signal_connect_object (query_model,
793 					 "complete", G_CALLBACK (rb_browser_source_query_complete_cb),
794 					 source, 0);
795 		rhythmdb_do_full_query_async_parsed (source->priv->db,
796 						     RHYTHMDB_QUERY_RESULTS (query_model),
797 						     query);
798 		g_object_unref (query_model);
799 	}
800 
801 	rhythmdb_query_free (query);
802 }
803 
804 static void
805 default_pack_content (RBBrowserSource *source, GtkWidget *content)
806 {
807 	gtk_container_add (GTK_CONTAINER (source), content);
808 }