1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  *  Copyright (C) 2003 Colin Walters <walters@verbum.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 /**
30  * SECTION:rb-entry-view
31  * @short_description: a #GtkTreeView for displaying track listings
32  *
33  * This class provides a predefined set of columns for displaying the
34  * common set of #RhythmDBEntry properties, but also allows custom columns
35  * to be appended.  The 'playing' column is always created as the first
36  * column in the tree view, displaying a playing or paused image next to
37  * the currently playing entry, and also an error image next to entries for
38  * which a playback error message has been set.  Clicking on the error
39  * image opens a dialog displaying the full message.
40  *
41  * All columns added to entry view columns should be expandable, or have a fixed
42  * minimum width set.  Otherwise, the tree view must measure the contents of each
43  * row to assign sizes, which is very slow for large track lists.  All the predefined
44  * column types handle this correctly.
45  */
46 
47 /**
48  * RBEntryViewColumn:
49  * @RB_ENTRY_VIEW_COL_TRACK_NUMBER: the track number column
50  * @RB_ENTRY_VIEW_COL_TITLE: the title column
51  * @RB_ENTRY_VIEW_COL_ARTIST: the artist column
52  * @RB_ENTRY_VIEW_COL_COMPOSER: the composer column
53  * @RB_ENTRY_VIEW_COL_ALBUM: the album column
54  * @RB_ENTRY_VIEW_COL_GENRE: the genre column
55  * @RB_ENTRY_VIEW_COL_DURATION: the duration column
56  * @RB_ENTRY_VIEW_COL_QUALITY: the quality (bitrate) column
57  * @RB_ENTRY_VIEW_COL_RATING: the rating column
58  * @RB_ENTRY_VIEW_COL_PLAY_COUNT: the play count column
59  * @RB_ENTRY_VIEW_COL_YEAR: the year (release date) column
60  * @RB_ENTRY_VIEW_COL_LAST_PLAYED: the last played time column
61  * @RB_ENTRY_VIEW_COL_FIRST_SEEN: the first seen (imported) column
62  * @RB_ENTRY_VIEW_COL_LAST_SEEN: the last seen column
63  * @RB_ENTRY_VIEW_COL_LOCATION: the location column
64  * @RB_ENTRY_VIEW_COL_BPM: the BPM column
65  * @RB_ENTRY_VIEW_COL_COMMENT: the comment column
66  * @RB_ENTRY_VIEW_COL_ERROR: the error column
67  *
68  * Predefined column types to use in #RBEntryView<!-- -->s.  Use
69  * #rb_entry_view_append_column to add these to an entry view.
70  * The predefined column names map directly to the #RhythmDBEntry properties
71  * the columns display.
72  */
73 
74 #include "config.h"
75 
76 #include <string.h>
77 #include <stdlib.h>
78 #include <math.h>
79 #include <time.h>
80 
81 #include <glib/gi18n.h>
82 #include <gtk/gtk.h>
83 #include <gio/gio.h>
84 #include <glib.h>
85 
86 #include "rb-tree-dnd.h"
87 #include "rb-entry-view.h"
88 #include "rb-dialog.h"
89 #include "rb-debug.h"
90 #include "rb-util.h"
91 #include "rhythmdb.h"
92 #include "rhythmdb-query-model.h"
93 #include "rb-cell-renderer-pixbuf.h"
94 #include "rb-cell-renderer-rating.h"
95 #include "rb-shell-player.h"
96 #include "rb-cut-and-paste-code.h"
97 #include "nautilus-floating-bar.h"
98 
99 static const GtkTargetEntry rb_entry_view_drag_types[] = {
100 	{ "application/x-rhythmbox-entry", 0, 0 },
101 	{ "text/uri-list", 0, 1 }
102 };
103 
104 struct RBEntryViewColumnSortData
105 {
106 	GCompareDataFunc func;
107 	gpointer data;
108 	GDestroyNotify data_destroy;
109 };
110 
111 /* GObject data item used to associate cell renderers with property IDs */
112 #define CELL_PROPID_ITEM "rb-cell-propid"
113 
114 static void rb_entry_view_class_init (RBEntryViewClass *klass);
115 static void rb_entry_view_init (RBEntryView *view);
116 static void rb_entry_view_constructed (GObject *object);
117 static void rb_entry_view_dispose (GObject *object);
118 static void rb_entry_view_finalize (GObject *object);
119 static void rb_entry_view_sort_data_finalize (gpointer column,
120 					      gpointer sort_data,
121 					      gpointer user_data);
122 static void rb_entry_view_set_property (GObject *object,
123 				       guint prop_id,
124 				       const GValue *value,
125 				       GParamSpec *pspec);
126 static void rb_entry_view_get_property (GObject *object,
127 				       guint prop_id,
128 				       GValue *value,
129 				       GParamSpec *pspec);
130 static void rb_entry_view_selection_changed_cb (GtkTreeSelection *selection,
131 				               RBEntryView *view);
132 static void rb_entry_view_grab_focus (GtkWidget *widget);
133 static void rb_entry_view_row_activated_cb (GtkTreeView *treeview,
134 			                   GtkTreePath *path,
135 			                   GtkTreeViewColumn *column,
136 			                   RBEntryView *view);
137 static void rb_entry_view_row_inserted_cb (GtkTreeModel *model,
138 					   GtkTreePath *path,
139 					   GtkTreeIter *iter,
140 					   RBEntryView *view);
141 static void rb_entry_view_row_deleted_cb (GtkTreeModel *model,
142 					  GtkTreePath *path,
143 					  RBEntryView *view);
144 static void rb_entry_view_rows_reordered_cb (GtkTreeModel *model,
145 					     GtkTreePath *path,
146 					     GtkTreeIter *iter,
147 					     gint *order,
148 					     RBEntryView *view);
149 static void rb_entry_view_sync_columns_visible (RBEntryView *view);
150 static void rb_entry_view_rated_cb (RBCellRendererRating *cellrating,
151 				   const char *path,
152 				   double rating,
153 				   RBEntryView *view);
154 static void rb_entry_view_pixbuf_clicked_cb (RBEntryView *view,
155 					     const char *path,
156 					     RBCellRendererPixbuf *cellpixbuf);
157 static void rb_entry_view_playing_column_clicked_cb (GtkTreeViewColumn *column,
158 						     RBEntryView *view);
159 static gboolean rb_entry_view_button_press_cb (GtkTreeView *treeview,
160 					      GdkEventButton *event,
161 					      RBEntryView *view);
162 static gboolean rb_entry_view_popup_menu_cb (GtkTreeView *treeview,
163 					     RBEntryView *view);
164 static void rb_entry_view_entry_is_visible (RBEntryView *view, RhythmDBEntry *entry,
165 					    gboolean *realized, gboolean *visible,
166 					    GtkTreeIter *iter);
167 static void rb_entry_view_scroll_to_iter (RBEntryView *view,
168 					  GtkTreeIter *iter);
169 static gboolean rb_entry_view_emit_row_changed (RBEntryView *view,
170 						RhythmDBEntry *entry);
171 static void rb_entry_view_playing_song_changed (RBShellPlayer *player,
172 						RhythmDBEntry *entry,
173 						RBEntryView *view);
174 
175 struct RBEntryViewPrivate
176 {
177 	RhythmDB *db;
178 	RBShellPlayer *shell_player;
179 
180 	RhythmDBQueryModel *model;
181 
182 	GtkWidget *overlay;
183 	GtkWidget *scrolled_window;
184 	GtkWidget *status;
185 	GtkWidget *treeview;
186 	GtkTreeSelection *selection;
187 
188 	RBEntryViewState playing_state;
189 	RhythmDBQueryModel *playing_model;
190 	RhythmDBEntry *playing_entry;
191 	gboolean playing_entry_in_view;
192 	guint selection_changed_id;
193 
194 	gboolean is_drag_source;
195 	gboolean is_drag_dest;
196 
197 	char *sorting_key;
198 	GtkTreeViewColumn *sorting_column;
199 	gint sorting_order;
200 	char *sorting_column_name;
201 	RhythmDBPropType type_ahead_propid;
202 	char **visible_columns;
203 
204 	gboolean have_selection, have_complete_selection;
205 
206 	GHashTable *column_key_map;
207 
208 	GHashTable *propid_column_map;
209 	GHashTable *column_sort_data_map;
210 };
211 
212 
213 enum
214 {
215 	ENTRY_ADDED,
216 	ENTRY_DELETED,
217 	ENTRIES_REPLACED,
218 	SELECTION_CHANGED,
219 	ENTRY_ACTIVATED,
220 	SHOW_POPUP,
221 	HAVE_SEL_CHANGED,
222 	LAST_SIGNAL
223 };
224 
225 enum
226 {
227 	PROP_0,
228 	PROP_DB,
229 	PROP_SHELL_PLAYER,
230 	PROP_MODEL,
231 	PROP_SORT_ORDER,
232 	PROP_IS_DRAG_SOURCE,
233 	PROP_IS_DRAG_DEST,
234 	PROP_PLAYING_STATE,
235 	PROP_VISIBLE_COLUMNS
236 };
237 
238 G_DEFINE_TYPE (RBEntryView, rb_entry_view, GTK_TYPE_BOX)
239 
240 static guint rb_entry_view_signals[LAST_SIGNAL] = { 0 };
241 
242 static GQuark rb_entry_view_column_always_visible;
243 
244 static gboolean
type_ahead_search_func(GtkTreeModel * model,gint column,const gchar * key,GtkTreeIter * iter,gpointer search_data)245 type_ahead_search_func (GtkTreeModel *model,
246 			gint column,
247 			const gchar *key,
248 			GtkTreeIter *iter,
249 			gpointer search_data)
250 {
251 	RBEntryView *view = RB_ENTRY_VIEW (search_data);
252 	RhythmDBEntry *entry;
253 	gchar *folded;
254 	const gchar *entry_folded;
255 	gboolean res;
256 
257 	gtk_tree_model_get (model, iter, 0, &entry, -1);
258 	folded = rb_search_fold (key);
259 	entry_folded = rb_refstring_get_folded (rhythmdb_entry_get_refstring (entry, view->priv->type_ahead_propid));
260 	rhythmdb_entry_unref (entry);
261 
262 	if (entry_folded == NULL || folded == NULL)
263 		return 1;
264 
265 	res = (strstr (entry_folded, folded) == NULL);
266 	g_free (folded);
267 
268 	return res;
269 }
270 
271 static void
rb_entry_view_class_init(RBEntryViewClass * klass)272 rb_entry_view_class_init (RBEntryViewClass *klass)
273 {
274 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
275 	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
276 
277 	object_class->dispose = rb_entry_view_dispose;
278 	object_class->finalize = rb_entry_view_finalize;
279 	object_class->constructed = rb_entry_view_constructed;
280 
281 	object_class->set_property = rb_entry_view_set_property;
282 	object_class->get_property = rb_entry_view_get_property;
283 
284 	widget_class->grab_focus = rb_entry_view_grab_focus;
285 
286 	/**
287 	 * RBEntryView:db:
288 	 *
289 	 * #RhythmDB instance
290 	 */
291 	g_object_class_install_property (object_class,
292 					 PROP_DB,
293 					 g_param_spec_object ("db",
294 							      "RhythmDB",
295 							      "RhythmDB database",
296 							      RHYTHMDB_TYPE,
297 							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
298 	/**
299 	 * RBEntryView:shell-player:
300 	 *
301 	 * #RBShellPlayer instance
302 	 */
303 	g_object_class_install_property (object_class,
304 					 PROP_SHELL_PLAYER,
305 					 g_param_spec_object ("shell-player",
306 							      "RBShellPlayer",
307 							      "RBShellPlayer object",
308 							      RB_TYPE_SHELL_PLAYER,
309 							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
310 	/**
311 	 * RBEntryView:model:
312 	 *
313 	 * The #RhythmDBQueryModel backing the view
314 	 */
315 	g_object_class_install_property (object_class,
316 					 PROP_MODEL,
317 					 g_param_spec_object ("model",
318 							      "RhythmDBQueryModel",
319 							      "RhythmDBQueryModel",
320 							      RHYTHMDB_TYPE_QUERY_MODEL,
321 							      G_PARAM_READWRITE));
322 	/**
323 	 * RBEntryView:sort-order:
324 	 *
325 	 * The sort order for the track listing.
326 	 */
327 	g_object_class_install_property (object_class,
328 					 PROP_SORT_ORDER,
329 					 g_param_spec_string ("sort-order",
330 							      "sorting order",
331 							      "sorting order",
332 							      NULL,
333 							      G_PARAM_READWRITE));
334 	/**
335 	 * RBEntryView:is-drag-source:
336 	 *
337 	 * If TRUE, the view acts as a data source for drag and drop operations.
338 	 */
339 	g_object_class_install_property (object_class,
340 					 PROP_IS_DRAG_SOURCE,
341 					 g_param_spec_boolean ("is-drag-source",
342 							       "is drag source",
343 							       "whether or not this is a drag source",
344 							       FALSE,
345 							       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
346 	/**
347 	 * RBEntryView:is-drag-dest:
348 	 *
349 	 * If TRUE, the view acts as a destination for drag and drop operations.
350 	 */
351 	g_object_class_install_property (object_class,
352 					 PROP_IS_DRAG_DEST,
353 					 g_param_spec_boolean ("is-drag-dest",
354 							       "is drag dest",
355 							       "whether or not this is a drag dest",
356 							       FALSE,
357 							       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
358 	/**
359 	 * RBEntryView:playing-state:
360 	 *
361 	 * Determines the icon to show in the 'playing' column next to the current
362 	 * playing entry.
363 	 */
364 	g_object_class_install_property (object_class,
365 					 PROP_PLAYING_STATE,
366 					 g_param_spec_int ("playing-state",
367 						 	   "playing state",
368 							   "playback state for this entry view",
369 							   RB_ENTRY_VIEW_NOT_PLAYING,
370 							   RB_ENTRY_VIEW_PAUSED,
371 							   RB_ENTRY_VIEW_NOT_PLAYING,
372 							   G_PARAM_READWRITE));
373 	/**
374 	 * RBEntryView:visible-columns:
375 	 *
376 	 * An array containing the names of the visible columns.
377 	 */
378 	g_object_class_install_property (object_class,
379 					 PROP_VISIBLE_COLUMNS,
380 					 g_param_spec_boxed ("visible-columns",
381 							     "visible columns",
382 							     "visible columns",
383 							     G_TYPE_STRV,
384 							     G_PARAM_READWRITE));
385 	/**
386 	 * RBEntryView::entry-added:
387 	 * @view: the #RBEntryView
388 	 * @entry: the #RhythmDBEntry that was added
389 	 *
390 	 * Emitted when an entry is added to the view
391 	 */
392 	rb_entry_view_signals[ENTRY_ADDED] =
393 		g_signal_new ("entry-added",
394 			      G_OBJECT_CLASS_TYPE (object_class),
395 			      G_SIGNAL_RUN_LAST,
396 			      G_STRUCT_OFFSET (RBEntryViewClass, entry_added),
397 			      NULL, NULL,
398 			      NULL,
399 			      G_TYPE_NONE,
400 			      1,
401 			      RHYTHMDB_TYPE_ENTRY);
402 	/**
403 	 * RBEntryView::entry-deleted:
404 	 * @view: the #RBEntryView
405 	 * @entry: the #RhythmDBEntry that was removed
406 	 *
407 	 * Emitted when an entry has been removed from the view
408 	 */
409 	rb_entry_view_signals[ENTRY_DELETED] =
410 		g_signal_new ("entry-deleted",
411 			      G_OBJECT_CLASS_TYPE (object_class),
412 			      G_SIGNAL_RUN_LAST,
413 			      G_STRUCT_OFFSET (RBEntryViewClass, entry_deleted),
414 			      NULL, NULL,
415 			      NULL,
416 			      G_TYPE_NONE,
417 			      1,
418 			      RHYTHMDB_TYPE_ENTRY);
419 	/**
420 	 * RBEntryView::entries-replaced:
421 	 * @view: the #RBEntryView
422 	 *
423 	 * Emitted when the model backing the entry view is replaced.
424 	 */
425 	rb_entry_view_signals[ENTRIES_REPLACED] =
426 		g_signal_new ("entries-replaced",
427 			      G_OBJECT_CLASS_TYPE (object_class),
428 			      G_SIGNAL_RUN_LAST,
429 			      G_STRUCT_OFFSET (RBEntryViewClass, entries_replaced),
430 			      NULL, NULL,
431 			      NULL,
432 			      G_TYPE_NONE,
433 			      0);
434 	/**
435 	 * RBEntryView::entry-activated:
436 	 * @view: the #RBEntryView
437 	 * @entry: the #RhythmDBEntry that was activated
438 	 *
439 	 * Emitted when an entry in the view is activated (by double clicking
440 	 * or by various key presses)
441 	 */
442 	rb_entry_view_signals[ENTRY_ACTIVATED] =
443 		g_signal_new ("entry-activated",
444 			      G_OBJECT_CLASS_TYPE (object_class),
445 			      G_SIGNAL_RUN_LAST,
446 			      G_STRUCT_OFFSET (RBEntryViewClass, entry_activated),
447 			      NULL, NULL,
448 			      NULL,
449 			      G_TYPE_NONE,
450 			      1,
451 			      RHYTHMDB_TYPE_ENTRY);
452 	/**
453 	 * RBEntryView::selection-changed:
454 	 * @view: the #RBEntryView
455 	 *
456 	 * Emitted when the set of selected entries changes
457 	 */
458 	rb_entry_view_signals[SELECTION_CHANGED] =
459 		g_signal_new ("selection-changed",
460 			      G_OBJECT_CLASS_TYPE (object_class),
461 			      G_SIGNAL_RUN_LAST,
462 			      G_STRUCT_OFFSET (RBEntryViewClass, selection_changed),
463 			      NULL, NULL,
464 			      NULL,
465 			      G_TYPE_NONE,
466 			      0);
467 	/**
468 	 * RBEntryView::show-popup:
469 	 * @view: the #RBEntryView
470 	 * @over_entry: if TRUE, the popup request was made while pointing
471 	 * at an entry in the view
472 	 *
473 	 * Emitted when the user performs an action that should result in a
474 	 * popup menu appearing.  If the action was a mouse button click,
475 	 * over_entry is FALSE if the mouse pointer was in the blank space after
476 	 * the last row in the view.  If the action was a key press, over_entry
477 	 * is FALSE if no rows in the view are selected.
478 	 */
479 	rb_entry_view_signals[SHOW_POPUP] =
480 		g_signal_new ("show_popup",
481 			      G_OBJECT_CLASS_TYPE (object_class),
482 			      G_SIGNAL_RUN_LAST,
483 			      G_STRUCT_OFFSET (RBEntryViewClass, show_popup),
484 			      NULL, NULL,
485 			      NULL,
486 			      G_TYPE_NONE,
487 			      1,
488 			      G_TYPE_BOOLEAN);
489 	/**
490 	 * RBEntryView::have-selection-changed:
491 	 * @view: the #RBEntryView
492 	 * @have_selection: TRUE if one or more rows are selected
493 	 *
494 	 * Emitted when the user first selects a row, or when no rows are selected
495 	 * any more.
496 	 */
497 	rb_entry_view_signals[HAVE_SEL_CHANGED] =
498 		g_signal_new ("have_selection_changed",
499 			      G_OBJECT_CLASS_TYPE (object_class),
500 			      G_SIGNAL_RUN_LAST,
501 			      G_STRUCT_OFFSET (RBEntryViewClass, have_selection_changed),
502 			      NULL, NULL,
503 			      NULL,
504 			      G_TYPE_NONE,
505 			      1,
506 			      G_TYPE_BOOLEAN);
507 
508 	g_type_class_add_private (klass, sizeof (RBEntryViewPrivate));
509 
510 	rb_entry_view_column_always_visible = g_quark_from_static_string ("rb_entry_view_column_always_visible");
511 }
512 
513 static void
rb_entry_view_init(RBEntryView * view)514 rb_entry_view_init (RBEntryView *view)
515 {
516 	view->priv = G_TYPE_INSTANCE_GET_PRIVATE (view, RB_TYPE_ENTRY_VIEW, RBEntryViewPrivate);
517 
518 	view->priv->propid_column_map = g_hash_table_new (NULL, NULL);
519 	view->priv->column_sort_data_map = g_hash_table_new_full (NULL, NULL, NULL, g_free);
520 	view->priv->column_key_map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
521 	view->priv->type_ahead_propid = RHYTHMDB_PROP_TITLE;
522 }
523 
524 static void
rb_entry_view_dispose(GObject * object)525 rb_entry_view_dispose (GObject *object)
526 {
527 	RBEntryView *view;
528 
529 	g_return_if_fail (object != NULL);
530 	g_return_if_fail (RB_IS_ENTRY_VIEW (object));
531 
532 	view = RB_ENTRY_VIEW (object);
533 
534 	g_return_if_fail (view->priv != NULL);
535 
536 	if (view->priv->selection_changed_id > 0) {
537 		g_source_remove (view->priv->selection_changed_id);
538 		view->priv->selection_changed_id = 0;
539 	}
540 
541 	if (view->priv->selection) {
542 		g_signal_handlers_disconnect_by_func (view->priv->selection,
543 						      G_CALLBACK (rb_entry_view_selection_changed_cb),
544 						      view);
545 		g_clear_object (&view->priv->selection);
546 	}
547 
548 	if (view->priv->playing_model != NULL) {
549 		g_object_unref (view->priv->playing_model);
550 		view->priv->playing_model = NULL;
551 	}
552 
553 	if (view->priv->model != NULL) {
554 		/* remove the model from the treeview so
555 		 * atk-bridge doesn't have to emit deletion events
556 		 * for each cell in the view.
557 		 */
558 		gtk_tree_view_set_model (GTK_TREE_VIEW (view->priv->treeview), NULL);
559 
560 		g_object_unref (view->priv->model);
561 		view->priv->model = NULL;
562 	}
563 
564 	G_OBJECT_CLASS (rb_entry_view_parent_class)->dispose (object);
565 }
566 
567 static void
rb_entry_view_finalize(GObject * object)568 rb_entry_view_finalize (GObject *object)
569 {
570 	RBEntryView *view;
571 
572 	g_return_if_fail (object != NULL);
573 	g_return_if_fail (RB_IS_ENTRY_VIEW (object));
574 
575 	view = RB_ENTRY_VIEW (object);
576 
577 	g_return_if_fail (view->priv != NULL);
578 
579 	g_hash_table_destroy (view->priv->propid_column_map);
580 	g_hash_table_foreach (view->priv->column_sort_data_map,
581 			      rb_entry_view_sort_data_finalize, NULL);
582 	g_hash_table_destroy (view->priv->column_sort_data_map);
583 	g_hash_table_destroy (view->priv->column_key_map);
584 
585 	g_free (view->priv->sorting_column_name);
586 	g_strfreev (view->priv->visible_columns);
587 
588 	G_OBJECT_CLASS (rb_entry_view_parent_class)->finalize (object);
589 }
590 
591 static void
rb_entry_view_sort_data_finalize(gpointer column,gpointer gsort_data,gpointer user_data)592 rb_entry_view_sort_data_finalize (gpointer column,
593 				  gpointer gsort_data,
594 				  gpointer user_data)
595 {
596 	struct RBEntryViewColumnSortData *sort_data = gsort_data;
597 
598 	if (sort_data->data_destroy) {
599 		sort_data->data_destroy (sort_data->data);
600 
601 		sort_data->data_destroy = NULL;
602 		sort_data->data = NULL;
603 		sort_data->func = NULL;
604 	}
605 }
606 
607 static void
rb_entry_view_set_shell_player_internal(RBEntryView * view,RBShellPlayer * player)608 rb_entry_view_set_shell_player_internal (RBEntryView   *view,
609 					 RBShellPlayer *player)
610 {
611 	if (view->priv->shell_player != NULL) {
612 		g_signal_handlers_disconnect_by_func (view->priv->shell_player,
613 						      G_CALLBACK (rb_entry_view_playing_song_changed),
614 						      view);
615 	}
616 
617 	view->priv->shell_player = player;
618 
619 	g_signal_connect_object (view->priv->shell_player,
620 				 "playing-song-changed",
621 				 G_CALLBACK (rb_entry_view_playing_song_changed),
622 				 view, 0);
623 }
624 
625 static void
rb_entry_view_set_model_internal(RBEntryView * view,RhythmDBQueryModel * model)626 rb_entry_view_set_model_internal (RBEntryView        *view,
627 				  RhythmDBQueryModel *model)
628 {
629 	if (view->priv->model != NULL) {
630 		g_signal_handlers_disconnect_by_func (view->priv->model,
631 						      G_CALLBACK (rb_entry_view_row_inserted_cb),
632 						      view);
633 		g_signal_handlers_disconnect_by_func (view->priv->model,
634 						      G_CALLBACK (rb_entry_view_row_deleted_cb),
635 						      view);
636 		g_signal_handlers_disconnect_by_func (view->priv->model,
637 						      G_CALLBACK (rb_entry_view_rows_reordered_cb),
638 						      view);
639 		g_object_unref (view->priv->model);
640 	}
641 
642 	gtk_tree_selection_unselect_all (view->priv->selection);
643 
644 	view->priv->model = model;
645 	if (view->priv->model != NULL) {
646 		g_object_ref (view->priv->model);
647 		g_signal_connect_object (view->priv->model,
648 					 "row_inserted",
649 					 G_CALLBACK (rb_entry_view_row_inserted_cb),
650 					 view,
651 					 0);
652 		g_signal_connect_object (view->priv->model,
653 					 "row_deleted",
654 					 G_CALLBACK (rb_entry_view_row_deleted_cb),
655 					 view,
656 					 0);
657 		g_signal_connect_object (view->priv->model,
658 					 "rows_reordered",
659 					 G_CALLBACK (rb_entry_view_rows_reordered_cb),
660 					 view,
661 					 0);
662 
663 		if (view->priv->sorting_column != NULL) {
664 			rb_entry_view_resort_model (view);
665 		}
666 
667 		gtk_tree_view_set_model (GTK_TREE_VIEW (view->priv->treeview),
668 					 GTK_TREE_MODEL (view->priv->model));
669 	}
670 
671 	view->priv->have_selection = FALSE;
672 	view->priv->have_complete_selection = FALSE;
673 
674 	g_signal_emit (G_OBJECT (view), rb_entry_view_signals[ENTRIES_REPLACED], 0);
675 }
676 
677 static void
rb_entry_view_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)678 rb_entry_view_set_property (GObject *object,
679 			    guint prop_id,
680 			    const GValue *value,
681 			    GParamSpec *pspec)
682 {
683 	RBEntryView *view = RB_ENTRY_VIEW (object);
684 
685 	switch (prop_id) {
686 	case PROP_DB:
687 		view->priv->db = g_value_get_object (value);
688 		break;
689 	case PROP_SHELL_PLAYER:
690 		rb_entry_view_set_shell_player_internal (view, g_value_get_object (value));
691 		break;
692 	case PROP_SORT_ORDER:
693 		rb_entry_view_set_sorting_type (view, g_value_get_string (value));
694 		break;
695 	case PROP_MODEL:
696 		rb_entry_view_set_model_internal (view, g_value_get_object (value));
697 		break;
698 	case PROP_IS_DRAG_SOURCE:
699 		view->priv->is_drag_source = g_value_get_boolean (value);
700 		break;
701 	case PROP_IS_DRAG_DEST:
702 		view->priv->is_drag_dest = g_value_get_boolean (value);
703 		break;
704 	case PROP_PLAYING_STATE:
705 		view->priv->playing_state = g_value_get_int (value);
706 
707 		/* redraw the playing entry, as the icon will have changed */
708 		if (view->priv->playing_entry != NULL) {
709 			rb_entry_view_emit_row_changed (view, view->priv->playing_entry);
710 		}
711 		break;
712 	case PROP_VISIBLE_COLUMNS:
713 		g_strfreev (view->priv->visible_columns);
714 		view->priv->visible_columns = g_value_dup_boxed (value);
715 		rb_entry_view_sync_columns_visible (view);
716 		break;
717 	default:
718 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
719 		break;
720 	}
721 }
722 
723 static void
rb_entry_view_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)724 rb_entry_view_get_property (GObject *object,
725 			    guint prop_id,
726 			    GValue *value,
727 			    GParamSpec *pspec)
728 {
729 	RBEntryView *view = RB_ENTRY_VIEW (object);
730 
731 	switch (prop_id) {
732 	case PROP_DB:
733 		g_value_set_object (value, view->priv->db);
734 		break;
735 	case PROP_SHELL_PLAYER:
736 		g_value_set_object (value, view->priv->shell_player);
737 		break;
738 	case PROP_SORT_ORDER:
739 		g_value_take_string (value, rb_entry_view_get_sorting_type (view));
740 		break;
741 	case PROP_MODEL:
742 		g_value_set_object (value, view->priv->model);
743 		break;
744 	case PROP_IS_DRAG_SOURCE:
745 		g_value_set_boolean (value, view->priv->is_drag_source);
746 		break;
747 	case PROP_IS_DRAG_DEST:
748 		g_value_set_boolean (value, view->priv->is_drag_dest);
749 		break;
750 	case PROP_PLAYING_STATE:
751 		g_value_set_int (value, view->priv->playing_state);
752 		break;
753 	case PROP_VISIBLE_COLUMNS:
754 		g_value_set_boxed (value, view->priv->visible_columns);
755 		break;
756 	default:
757 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
758 		break;
759 	}
760 }
761 
762 /**
763  * rb_entry_view_new:
764  * @db: the #RhythmDB instance
765  * @shell_player: the #RBShellPlayer instance
766  * @is_drag_source: if TRUE, the view should act as a drag and drop data source
767  * @is_drag_dest: if TRUE, the view should act as a drag and drop destination
768  *
769  * Creates a new entry view.  If it makes sense to allow the user to drag entries
770  * from this entry view to other sources, @is_drag_source should be TRUE.  If it
771  * makes sense to allow the user to drag entries from other sources to this view,
772  * @is_drag_dest should be TRUE.  Drag and drop in this sense is used for two purposes:
773  * to transfer tracks between the filesystem and removable devices, and to add tracks
774  * to playlists.
775  *
776  * Return value: the new entry view
777  */
778 RBEntryView *
rb_entry_view_new(RhythmDB * db,GObject * shell_player,gboolean is_drag_source,gboolean is_drag_dest)779 rb_entry_view_new (RhythmDB *db,
780 		   GObject *shell_player,
781 		   gboolean is_drag_source,
782 		   gboolean is_drag_dest)
783 {
784 	RBEntryView *view;
785 
786 	view = RB_ENTRY_VIEW (g_object_new (RB_TYPE_ENTRY_VIEW,
787 					   "orientation", GTK_ORIENTATION_VERTICAL,
788 					   "hexpand", TRUE,
789 					   "vexpand", TRUE,
790 					   "db", db,
791 					   "shell-player", RB_SHELL_PLAYER (shell_player),
792 					   "is-drag-source", is_drag_source,
793 					   "is-drag-dest", is_drag_dest,
794 					   NULL));
795 
796 	g_return_val_if_fail (view->priv != NULL, NULL);
797 
798 	return view;
799 }
800 
801 /**
802  * rb_entry_view_set_model:
803  * @view: the #RBEntryView
804  * @model: the new #RhythmDBQueryModel to use for the view
805  *
806  * Replaces the model backing the entry view.
807  */
808 void
rb_entry_view_set_model(RBEntryView * view,RhythmDBQueryModel * model)809 rb_entry_view_set_model (RBEntryView *view,
810 			 RhythmDBQueryModel *model)
811 {
812 	g_object_set (view, "model", model, NULL);
813 }
814 
815 /* Sweet name, eh? */
816 struct RBEntryViewCellDataFuncData {
817 	RBEntryView *view;
818 	RhythmDBPropType propid;
819 };
820 
821 static void
rb_entry_view_playing_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,RBEntryView * view)822 rb_entry_view_playing_cell_data_func (GtkTreeViewColumn *column,
823 				      GtkCellRenderer *renderer,
824 				      GtkTreeModel *tree_model,
825 				      GtkTreeIter *iter,
826 				      RBEntryView *view)
827 {
828 	RhythmDBEntry *entry;
829 	const char *name = NULL;
830 
831 	entry = rhythmdb_query_model_iter_to_entry (view->priv->model, iter);
832 
833 	if (entry == NULL) {
834 		return;
835 	}
836 
837 	if (entry == view->priv->playing_entry) {
838 		switch (view->priv->playing_state) {
839 		case RB_ENTRY_VIEW_PLAYING:
840 			name = "media-playback-start-symbolic";
841 			break;
842 		case RB_ENTRY_VIEW_PAUSED:
843 			name = "media-playback-pause-symbolic";
844 			break;
845 		default:
846 			name = NULL;
847 			break;
848 		}
849 	}
850 
851 	if (name == NULL && rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_PLAYBACK_ERROR)) {
852 		name = "dialog-error-symbolic";
853 	}
854 
855 	g_object_set (renderer, "icon-name", name, NULL);
856 
857 	rhythmdb_entry_unref (entry);
858 }
859 
860 static void
rb_entry_view_rating_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,RBEntryView * view)861 rb_entry_view_rating_cell_data_func (GtkTreeViewColumn *column,
862 				     GtkCellRenderer *renderer,
863 				     GtkTreeModel *tree_model,
864 				     GtkTreeIter *iter,
865 				     RBEntryView *view)
866 {
867 	RhythmDBEntry *entry;
868 
869 	entry = rhythmdb_query_model_iter_to_entry (view->priv->model, iter);
870 
871 	g_object_set (renderer,
872 		      "rating", rhythmdb_entry_get_double (entry, RHYTHMDB_PROP_RATING),
873 		      NULL);
874 
875 	rhythmdb_entry_unref (entry);
876 }
877 
878 static void
rb_entry_view_bpm_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,struct RBEntryViewCellDataFuncData * data)879 rb_entry_view_bpm_cell_data_func (GtkTreeViewColumn *column,
880 				   GtkCellRenderer *renderer,
881 				   GtkTreeModel *tree_model,
882 				   GtkTreeIter *iter,
883 				   struct RBEntryViewCellDataFuncData *data)
884 {
885 	RhythmDBEntry *entry;
886 	char *str;
887 	gdouble val;
888 
889 	entry = rhythmdb_query_model_iter_to_entry (data->view->priv->model, iter);
890 
891 	val = rhythmdb_entry_get_double (entry, data->propid);
892 
893 	if (val > 0.001)
894 		str = g_strdup_printf ("%.2f", val);
895 	else
896 		str = g_strdup ("");
897 
898 	g_object_set (renderer, "text", str, NULL);
899 	g_free (str);
900 	rhythmdb_entry_unref (entry);
901 }
902 
903 static void
rb_entry_view_long_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,struct RBEntryViewCellDataFuncData * data)904 rb_entry_view_long_cell_data_func (GtkTreeViewColumn *column,
905 				   GtkCellRenderer *renderer,
906 				   GtkTreeModel *tree_model,
907 				   GtkTreeIter *iter,
908 				   struct RBEntryViewCellDataFuncData *data)
909 {
910 	RhythmDBEntry *entry;
911 	char *str;
912 	gulong val;
913 
914 	entry = rhythmdb_query_model_iter_to_entry (data->view->priv->model, iter);
915 
916 	val = rhythmdb_entry_get_ulong (entry, data->propid);
917 
918 	if (val > 0)
919 		str = g_strdup_printf ("%lu", val);
920 	else
921 		str = g_strdup ("");
922 
923 	g_object_set (renderer, "text", str, NULL);
924 	g_free (str);
925 	rhythmdb_entry_unref (entry);
926 }
927 
928 static void
rb_entry_view_play_count_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,struct RBEntryViewCellDataFuncData * data)929 rb_entry_view_play_count_cell_data_func (GtkTreeViewColumn *column,
930 					 GtkCellRenderer *renderer,
931 					 GtkTreeModel *tree_model,
932 					 GtkTreeIter * iter,
933 					 struct RBEntryViewCellDataFuncData *data)
934 {
935 	RhythmDBEntry *entry;
936 	gulong i;
937 	char *str;
938 
939 	entry = rhythmdb_query_model_iter_to_entry (data->view->priv->model, iter);
940 
941 	i = rhythmdb_entry_get_ulong (entry, data->propid);
942 	if (i == 0)
943 		str = _("Never");
944 	else
945 		str = g_strdup_printf ("%ld", i);
946 
947 	g_object_set (renderer, "text", str, NULL);
948 	if (i != 0)
949 		g_free (str);
950 
951 	rhythmdb_entry_unref (entry);
952 }
953 
954 static void
rb_entry_view_duration_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,struct RBEntryViewCellDataFuncData * data)955 rb_entry_view_duration_cell_data_func (GtkTreeViewColumn *column,
956 				       GtkCellRenderer *renderer,
957 				       GtkTreeModel *tree_model,
958 				       GtkTreeIter *iter,
959 				       struct RBEntryViewCellDataFuncData *data)
960 {
961 	RhythmDBEntry *entry;
962 	gulong duration;
963 	char *str;
964 
965 	entry = rhythmdb_query_model_iter_to_entry (data->view->priv->model, iter);
966 	duration = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DURATION);
967 
968 	str = rb_make_duration_string (duration);
969 	g_object_set (renderer, "text", str, NULL);
970 	g_free (str);
971 	rhythmdb_entry_unref (entry);
972 }
973 
974 static void
rb_entry_view_year_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,struct RBEntryViewCellDataFuncData * data)975 rb_entry_view_year_cell_data_func (GtkTreeViewColumn *column,
976 				   GtkCellRenderer *renderer,
977 				   GtkTreeModel *tree_model,
978 				   GtkTreeIter *iter,
979 				   struct RBEntryViewCellDataFuncData *data)
980 {
981 	RhythmDBEntry *entry;
982 	char str[255];
983 	int julian;
984 	GDate *date;
985 
986 	entry = rhythmdb_query_model_iter_to_entry (data->view->priv->model, iter);
987 	julian = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DATE);
988 
989 	if (julian > 0) {
990 		date = g_date_new_julian (julian);
991 		g_date_strftime (str, sizeof (str), "%Y", date);
992 		g_object_set (renderer, "text", str, NULL);
993 		g_date_free (date);
994 	} else {
995 		g_object_set (renderer, "text", _("Unknown"), NULL);
996 	}
997 
998 	rhythmdb_entry_unref (entry);
999 }
1000 
1001 static void
rb_entry_view_quality_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,struct RBEntryViewCellDataFuncData * data)1002 rb_entry_view_quality_cell_data_func (GtkTreeViewColumn *column,
1003 				      GtkCellRenderer *renderer,
1004 				      GtkTreeModel *tree_model,
1005 				      GtkTreeIter *iter,
1006 				      struct RBEntryViewCellDataFuncData *data)
1007 {
1008 	RhythmDBEntry *entry;
1009 	gulong bitrate;
1010 
1011 	entry = rhythmdb_query_model_iter_to_entry (data->view->priv->model, iter);
1012 	bitrate = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_BITRATE);
1013 
1014 	if (rhythmdb_entry_is_lossless (entry)) {
1015 		g_object_set (renderer, "text", _("Lossless"), NULL);
1016 	} else if (bitrate == 0) {
1017 		g_object_set (renderer, "text", _("Unknown"), NULL);
1018 	} else {
1019 		char *s;
1020 
1021 		s = g_strdup_printf (_("%lu kbps"), bitrate);
1022 		g_object_set (renderer, "text", s, NULL);
1023 		g_free (s);
1024 	}
1025 
1026 	rhythmdb_entry_unref (entry);
1027 }
1028 
1029 static void
rb_entry_view_location_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,struct RBEntryViewCellDataFuncData * data)1030 rb_entry_view_location_cell_data_func (GtkTreeViewColumn *column,
1031 				       GtkCellRenderer *renderer,
1032 				       GtkTreeModel *tree_model,
1033 				       GtkTreeIter *iter,
1034 				       struct RBEntryViewCellDataFuncData *data)
1035 {
1036 	RhythmDBEntry *entry;
1037 	const char *location;
1038 	char *str;
1039 
1040 	entry = rhythmdb_query_model_iter_to_entry (data->view->priv->model, iter);
1041 
1042 	location = rhythmdb_entry_get_string (entry, data->propid);
1043 	str = g_uri_unescape_string (location, NULL);
1044 
1045 	g_object_set (renderer, "text", str, NULL);
1046 	g_free (str);
1047 
1048 	rhythmdb_entry_unref (entry);
1049 }
1050 
1051 static void
rb_entry_view_string_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,struct RBEntryViewCellDataFuncData * data)1052 rb_entry_view_string_cell_data_func (GtkTreeViewColumn *column,
1053 				     GtkCellRenderer *renderer,
1054 				     GtkTreeModel *tree_model,
1055 				     GtkTreeIter *iter,
1056 				     struct RBEntryViewCellDataFuncData *data)
1057 {
1058 	RhythmDBEntry *entry;
1059 	const char *str;
1060 
1061 	entry = rhythmdb_query_model_iter_to_entry (data->view->priv->model, iter);
1062 
1063 	str = rhythmdb_entry_get_string (entry, data->propid);
1064 	if (str != NULL) {
1065 		g_object_set (renderer, "text", str, NULL);
1066 	}
1067 
1068 	rhythmdb_entry_unref (entry);
1069 }
1070 
1071 static void
rb_entry_view_sync_sorting(RBEntryView * view)1072 rb_entry_view_sync_sorting (RBEntryView *view)
1073 {
1074 	GtkTreeViewColumn *column;
1075 	gint direction;
1076 	char *column_name;
1077 	RhythmDBPropType type_ahead_propid;
1078 	GList *renderers;
1079 
1080 	direction = GTK_SORT_ASCENDING;
1081 	column_name = NULL;
1082 	rb_entry_view_get_sorting_order (view, &column_name, &direction);
1083 
1084 	if (column_name == NULL) {
1085 		return;
1086 	}
1087 
1088 	column = g_hash_table_lookup (view->priv->column_key_map, column_name);
1089 	if (column == NULL) {
1090 		rb_debug ("couldn't find column %s", column_name);
1091 		g_free (column_name);
1092 		return;
1093 	}
1094 
1095 	rb_debug ("Updating EntryView sort order to %s:%d", column_name, direction);
1096 
1097 	/* remove the old sorting indicator */
1098 	if (view->priv->sorting_column)
1099 		gtk_tree_view_column_set_sort_indicator (view->priv->sorting_column, FALSE);
1100 
1101 	/* set the sorting order and indicator of the new sorting column */
1102 	view->priv->sorting_column = column;
1103 	gtk_tree_view_column_set_sort_indicator (column, TRUE);
1104 	gtk_tree_view_column_set_sort_order (column, direction);
1105 
1106 	/* set the property id to use for the typeahead search */
1107 	renderers = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (column));
1108 	type_ahead_propid = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (renderers->data), CELL_PROPID_ITEM));
1109 	g_list_free (renderers);
1110 	if (type_ahead_propid != 0 && rhythmdb_get_property_type (view->priv->db, type_ahead_propid) == G_TYPE_STRING)
1111 		view->priv->type_ahead_propid = type_ahead_propid;
1112 	else
1113 		view->priv->type_ahead_propid = RHYTHMDB_PROP_TITLE;
1114 
1115 	g_free (column_name);
1116 }
1117 
1118 /**
1119  * rb_entry_view_get_sorting_type:
1120  * @view: an #RBEntryView
1121  *
1122  * Constructs a string that describes the sort settings for the entry view.
1123  * This consists of a column name and an order ('ascending' or 'descending')
1124  * separated by a comma.
1125  *
1126  * Return value: (transfer full): sort order description
1127  */
1128 char *
rb_entry_view_get_sorting_type(RBEntryView * view)1129 rb_entry_view_get_sorting_type (RBEntryView *view)
1130 {
1131 	char *sorttype;
1132 	GString *key = g_string_new (view->priv->sorting_column_name);
1133 
1134 	g_string_append_c (key, ',');
1135 
1136 	switch (view->priv->sorting_order)
1137 	{
1138 	case GTK_SORT_ASCENDING:
1139 		g_string_append (key, "ascending");
1140 		break;
1141 	case GTK_SORT_DESCENDING:
1142 		g_string_append (key, "descending");
1143 		break;
1144 	default:
1145 		g_assert_not_reached ();
1146 	}
1147 
1148 	sorttype = key->str;
1149 	g_string_free (key, FALSE);
1150 
1151 	return sorttype;
1152 }
1153 
1154 /**
1155  * rb_entry_view_set_sorting_type:
1156  * @view: a #RBEntryView
1157  * @sorttype: sort order description
1158  *
1159  * Changes the sort order for the entry view.  The sort order
1160  * description must be a column name, followed by a comma, followed
1161  * by an order description ('ascending' or 'descending').
1162  */
1163 void
rb_entry_view_set_sorting_type(RBEntryView * view,const char * sorttype)1164 rb_entry_view_set_sorting_type (RBEntryView *view,
1165 				const char *sorttype)
1166 {
1167 	char **strs;
1168 
1169 	if (!sorttype || !strchr (sorttype, ',')) {
1170 		rb_debug ("malformed sort data: %s", (sorttype) ? sorttype : "(null)");
1171 		return;
1172 	}
1173 
1174 	strs = g_strsplit (sorttype, ",", 0);
1175 
1176 	g_free (view->priv->sorting_column_name);
1177 	view->priv->sorting_column_name = g_strdup(strs[0]);
1178 
1179 	if (!strcmp ("ascending", strs[1]))
1180 		view->priv->sorting_order = GTK_SORT_ASCENDING;
1181 	else if (!strcmp ("descending", strs[1]))
1182 		view->priv->sorting_order = GTK_SORT_DESCENDING;
1183 	else {
1184 		g_warning ("atttempting to sort in unknown direction");
1185 		view->priv->sorting_order = GTK_SORT_ASCENDING;
1186 	}
1187 
1188 	g_strfreev (strs);
1189 
1190 	rb_entry_view_sync_sorting (view);
1191 	g_object_notify (G_OBJECT (view), "sort-order");
1192 }
1193 
1194 /**
1195  * rb_entry_view_get_sorting_order:
1196  * @view: a #RBEntryView
1197  * @column_name: (out callee-allocates) (allow-none) (transfer full): returns the sort column name
1198  * @sort_order: (out) (allow-none): returns the sort ordering as a #GtkSortType value
1199  *
1200  * Retrieves the sort settings for the view.
1201  */
1202 void
rb_entry_view_get_sorting_order(RBEntryView * view,char ** column_name,gint * sort_order)1203 rb_entry_view_get_sorting_order (RBEntryView *view,
1204 				 char **column_name,
1205 				 gint *sort_order)
1206 {
1207  	g_return_if_fail (RB_IS_ENTRY_VIEW (view));
1208 
1209 	if (column_name != NULL) {
1210 		*column_name = g_strdup (view->priv->sorting_column_name);
1211 	}
1212 
1213 	if (sort_order != NULL) {
1214 		*sort_order = view->priv->sorting_order;
1215 	}
1216 }
1217 
1218 /**
1219  * rb_entry_view_set_sorting_order:
1220  * @view: a #RBEntryView
1221  * @column_name: name of the column to sort on
1222  * @sort_order: order to sort in, as a #GtkSortType
1223  *
1224  * Sets the sort order for the entry view.
1225  */
1226 void
rb_entry_view_set_sorting_order(RBEntryView * view,const char * column_name,gint sort_order)1227 rb_entry_view_set_sorting_order (RBEntryView *view,
1228 				 const char *column_name,
1229 				 gint sort_order)
1230 {
1231 	if (column_name == NULL)
1232 		return;
1233 
1234 	g_free (view->priv->sorting_column_name);
1235 	view->priv->sorting_column_name = g_strdup (column_name);
1236 	view->priv->sorting_order = sort_order;
1237 
1238 	rb_entry_view_sync_sorting (view);
1239 	g_object_notify (G_OBJECT (view), "sort-order");
1240 }
1241 
1242 static void
rb_entry_view_column_clicked_cb(GtkTreeViewColumn * column,RBEntryView * view)1243 rb_entry_view_column_clicked_cb (GtkTreeViewColumn *column, RBEntryView *view)
1244 {
1245 	gint sort_order;
1246 	char *clicked_column;
1247 
1248 	rb_debug ("sorting on column %p", column);
1249 
1250 	/* identify the clicked column, and then update the sorting order */
1251 	clicked_column = (char*) g_object_get_data (G_OBJECT (column), "rb-entry-view-key");
1252 	sort_order = view->priv->sorting_order;
1253 
1254 	if (view->priv->sorting_column_name
1255 	    && !strcmp(clicked_column, view->priv->sorting_column_name)
1256 	    && (sort_order == GTK_SORT_ASCENDING))
1257 		sort_order = GTK_SORT_DESCENDING;
1258 	else
1259 		sort_order = GTK_SORT_ASCENDING;
1260 
1261 	rb_entry_view_set_sorting_order (view, clicked_column, sort_order);
1262 }
1263 
1264 /**
1265  * rb_entry_view_get_column:
1266  * @view: a #RBEntryView
1267  * @coltype: type of column to retrieve
1268  *
1269  * Retrieves a predefined column from the entry view.  This can be used
1270  * to insert additional cell renderers into the column.
1271  *
1272  * Return value: (transfer none): a #GtkTreeViewColumn instance, or NULL
1273  */
1274 GtkTreeViewColumn *
rb_entry_view_get_column(RBEntryView * view,RBEntryViewColumn coltype)1275 rb_entry_view_get_column (RBEntryView *view, RBEntryViewColumn coltype)
1276 {
1277 	RhythmDBPropType propid;
1278 
1279 	/* convert column type to property ID */
1280 	switch (coltype) {
1281 	case RB_ENTRY_VIEW_COL_TRACK_NUMBER:
1282 		propid = RHYTHMDB_PROP_TRACK_NUMBER;
1283 		break;
1284 	case RB_ENTRY_VIEW_COL_TITLE:
1285 		propid = RHYTHMDB_PROP_TITLE;
1286 		break;
1287 	case RB_ENTRY_VIEW_COL_ARTIST:
1288 		propid = RHYTHMDB_PROP_ARTIST;
1289 		break;
1290 	case RB_ENTRY_VIEW_COL_ALBUM:
1291 		propid = RHYTHMDB_PROP_ALBUM;
1292 		break;
1293 	case RB_ENTRY_VIEW_COL_GENRE:
1294 		propid = RHYTHMDB_PROP_GENRE;
1295 		break;
1296 	case RB_ENTRY_VIEW_COL_COMMENT:
1297 		propid = RHYTHMDB_PROP_COMMENT;
1298 		break;
1299 	case RB_ENTRY_VIEW_COL_DURATION:
1300 		propid = RHYTHMDB_PROP_DURATION;
1301 		break;
1302 	case RB_ENTRY_VIEW_COL_YEAR:
1303 		propid = RHYTHMDB_PROP_DATE;
1304 		break;
1305 	case RB_ENTRY_VIEW_COL_QUALITY:
1306 		propid = RHYTHMDB_PROP_BITRATE;
1307 		break;
1308 	case RB_ENTRY_VIEW_COL_RATING:
1309 		propid = RHYTHMDB_PROP_RATING;
1310 		break;
1311 	case RB_ENTRY_VIEW_COL_PLAY_COUNT:
1312 		propid = RHYTHMDB_PROP_PLAY_COUNT;
1313 		break;
1314 	case RB_ENTRY_VIEW_COL_LAST_PLAYED:
1315 		propid = RHYTHMDB_PROP_LAST_PLAYED;
1316 		break;
1317 	case RB_ENTRY_VIEW_COL_FIRST_SEEN:
1318 		propid = RHYTHMDB_PROP_FIRST_SEEN;
1319 		break;
1320 	case RB_ENTRY_VIEW_COL_LAST_SEEN:
1321 		propid = RHYTHMDB_PROP_LAST_SEEN;
1322 		break;
1323 	case RB_ENTRY_VIEW_COL_LOCATION:
1324 		propid = RHYTHMDB_PROP_LOCATION;
1325 		break;
1326 	case RB_ENTRY_VIEW_COL_BPM:
1327 		propid = RHYTHMDB_PROP_BPM;
1328 		break;
1329 	case RB_ENTRY_VIEW_COL_ERROR:
1330 		propid = RHYTHMDB_PROP_PLAYBACK_ERROR;
1331 		break;
1332 	case RB_ENTRY_VIEW_COL_COMPOSER:
1333 		propid = RHYTHMDB_PROP_COMPOSER;
1334 		break;
1335 	default:
1336 		g_assert_not_reached ();
1337 		propid = -1;
1338 		break;
1339 	}
1340 
1341 	/* find the column */
1342 	return (GtkTreeViewColumn *)g_hash_table_lookup (view->priv->propid_column_map, GINT_TO_POINTER (propid));
1343 }
1344 
1345 static void
rb_entry_view_cell_edited_cb(GtkCellRendererText * renderer,char * path_str,char * new_text,RBEntryView * view)1346 rb_entry_view_cell_edited_cb (GtkCellRendererText *renderer,
1347 			      char *path_str,
1348 			      char *new_text,
1349 			      RBEntryView *view)
1350 {
1351 	RhythmDBPropType propid;
1352 	RhythmDBEntry *entry;
1353 	GValue value = {0,};
1354 	GtkTreePath *path;
1355 
1356 	/* get the property corresponding to the cell, filter out properties we can't edit */
1357 	propid = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (renderer), CELL_PROPID_ITEM));
1358 	switch (propid) {
1359 	case RHYTHMDB_PROP_TITLE:
1360 	case RHYTHMDB_PROP_GENRE:
1361 	case RHYTHMDB_PROP_ARTIST:
1362 	case RHYTHMDB_PROP_ALBUM:
1363 	case RHYTHMDB_PROP_COMMENT:
1364 	case RHYTHMDB_PROP_ARTIST_SORTNAME:
1365 	case RHYTHMDB_PROP_ALBUM_SORTNAME:
1366 		break;
1367 
1368 	default:
1369 		rb_debug ("can't edit property %s", rhythmdb_nice_elt_name_from_propid (view->priv->db, propid));
1370 		return;
1371 	}
1372 
1373 	/* find entry */
1374 	path = gtk_tree_path_new_from_string (path_str);
1375 	entry = rhythmdb_query_model_tree_path_to_entry (view->priv->model, path);
1376 	gtk_tree_path_free (path);
1377 
1378 	if (entry != NULL) {
1379 		/* update it */
1380 		g_value_init (&value, G_TYPE_STRING);
1381 		g_value_set_string (&value, new_text);
1382 		rhythmdb_entry_set (view->priv->db, entry, propid, &value);
1383 		g_value_unset (&value);
1384 
1385 		rhythmdb_commit (view->priv->db);
1386 		rhythmdb_entry_unref (entry);
1387 	}
1388 }
1389 
1390 
1391 /**
1392  * rb_entry_view_append_column:
1393  * @view: a #RBEntryView
1394  * @coltype: type of column to append
1395  * @always_visible: if TRUE, ignore the user's column visibility settings
1396  *
1397  * Appends a predefined column type to the set of columns already present
1398  * in the entry view.  If @always_visible is TRUE, the column will ignore
1399  * the user's coulmn visibility settings and will always be visible.
1400  * This should only be used when it is vital for the purpose of the
1401  * source that the column be visible.
1402  */
1403 void
rb_entry_view_append_column(RBEntryView * view,RBEntryViewColumn coltype,gboolean always_visible)1404 rb_entry_view_append_column (RBEntryView *view,
1405 			     RBEntryViewColumn coltype,
1406 			     gboolean always_visible)
1407 {
1408 	GtkTreeViewColumn *column;
1409 	GtkCellRenderer *renderer = NULL;
1410 	struct RBEntryViewCellDataFuncData *cell_data;
1411 	const char *title = NULL;
1412 	const char *key = NULL;
1413 	const char *strings[5] = {0};
1414 	GtkTreeCellDataFunc cell_data_func = NULL;
1415 	GCompareDataFunc sort_func = NULL;
1416 	RhythmDBPropType propid;
1417 	RhythmDBPropType sort_propid = RHYTHMDB_NUM_PROPERTIES;
1418 	gboolean ellipsize = FALSE;
1419 	gboolean resizable = TRUE;
1420 	gint column_width = -1;
1421 
1422 	column = gtk_tree_view_column_new ();
1423 
1424 	cell_data = g_new0 (struct RBEntryViewCellDataFuncData, 1);
1425 	cell_data->view = view;
1426 
1427 	switch (coltype) {
1428 	case RB_ENTRY_VIEW_COL_TRACK_NUMBER:
1429 		propid = RHYTHMDB_PROP_TRACK_NUMBER;
1430 		cell_data->propid = propid;
1431 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_long_cell_data_func;
1432 		sort_func = (GCompareDataFunc) rhythmdb_query_model_track_sort_func;
1433 		title = _("Track");
1434 		key = "Track";
1435 		strings[0] = title;
1436 		strings[1] = "9999";
1437 		break;
1438 	case RB_ENTRY_VIEW_COL_TITLE:
1439 		propid = RHYTHMDB_PROP_TITLE;
1440 		cell_data->propid = propid;
1441 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_string_cell_data_func;
1442 		sort_propid = RHYTHMDB_PROP_TITLE_SORT_KEY;
1443 		sort_func = (GCompareDataFunc) rhythmdb_query_model_string_sort_func;
1444 		title = _("Title");
1445 		key = "Title";
1446 		ellipsize = TRUE;
1447 		break;
1448 	case RB_ENTRY_VIEW_COL_ARTIST:
1449 		propid = RHYTHMDB_PROP_ARTIST;
1450 		cell_data->propid = propid;
1451 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_string_cell_data_func;
1452 		sort_propid = RHYTHMDB_PROP_ARTIST_SORT_KEY;
1453 		sort_func = (GCompareDataFunc) rhythmdb_query_model_artist_sort_func;
1454 		title = _("Artist");
1455 		key = "Artist";
1456 		ellipsize = TRUE;
1457 		break;
1458 	case RB_ENTRY_VIEW_COL_COMPOSER:
1459 		propid = RHYTHMDB_PROP_COMPOSER;
1460 		cell_data->propid = propid;
1461 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_string_cell_data_func;
1462 		sort_propid = RHYTHMDB_PROP_COMPOSER_SORT_KEY;
1463 		sort_func = (GCompareDataFunc) rhythmdb_query_model_composer_sort_func;
1464 		title = _("Composer");
1465 		key = "Composer";
1466 		ellipsize = TRUE;
1467 		break;
1468 	case RB_ENTRY_VIEW_COL_ALBUM:
1469 		propid = RHYTHMDB_PROP_ALBUM;
1470 		cell_data->propid = propid;
1471 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_string_cell_data_func;
1472 		sort_propid = RHYTHMDB_PROP_ALBUM_SORT_KEY;
1473 		sort_func = (GCompareDataFunc) rhythmdb_query_model_album_sort_func;
1474 		title = _("Album");
1475 		key = "Album";
1476 		ellipsize = TRUE;
1477 		break;
1478 	case RB_ENTRY_VIEW_COL_GENRE:
1479 		propid = RHYTHMDB_PROP_GENRE;
1480 		cell_data->propid = propid;
1481 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_string_cell_data_func;
1482 		sort_propid = RHYTHMDB_PROP_GENRE_SORT_KEY;
1483 		sort_func = (GCompareDataFunc) rhythmdb_query_model_genre_sort_func;
1484 		title = _("Genre");
1485 		key = "Genre";
1486 		ellipsize = TRUE;
1487 		break;
1488 	case RB_ENTRY_VIEW_COL_COMMENT:
1489 		propid = RHYTHMDB_PROP_COMMENT;
1490 		cell_data->propid = propid;
1491 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_string_cell_data_func;
1492 		sort_propid = cell_data->propid;
1493 		sort_func = (GCompareDataFunc) rhythmdb_query_model_string_sort_func;
1494 		title = _("Comment");
1495 		key = "Comment";
1496 		ellipsize = TRUE;
1497 		break;
1498 	case RB_ENTRY_VIEW_COL_DURATION:
1499 		propid = RHYTHMDB_PROP_DURATION;
1500 		cell_data->propid = propid;
1501 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_duration_cell_data_func;
1502 		sort_propid = cell_data->propid;
1503 		sort_func = (GCompareDataFunc) rhythmdb_query_model_ulong_sort_func;
1504 		title = _("Time");
1505 		key = "Time";
1506 		strings[0] = title;
1507 		strings[1] = "000:00";
1508 		strings[2] = _("Unknown");
1509 		break;
1510 	case RB_ENTRY_VIEW_COL_YEAR:
1511 		propid = RHYTHMDB_PROP_DATE;
1512 		cell_data->propid = propid;
1513 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_year_cell_data_func;
1514 		sort_propid = cell_data->propid;
1515 		sort_func = (GCompareDataFunc) rhythmdb_query_model_date_sort_func;
1516 		title = _("Year");
1517 		key = "Year";
1518 		strings[0] = title;
1519 		strings[1] = "0000";
1520 		strings[2] = _("Unknown");
1521 		break;
1522 	case RB_ENTRY_VIEW_COL_QUALITY:
1523 		propid = RHYTHMDB_PROP_BITRATE;
1524 		cell_data->propid = propid;
1525 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_quality_cell_data_func;
1526 		sort_propid = cell_data->propid;
1527 		sort_func = (GCompareDataFunc) rhythmdb_query_model_bitrate_sort_func;
1528 		title = _("Quality");
1529 		key = "Quality";
1530 		strings[0] = title;
1531 		strings[1] = _("000 kbps");
1532 		strings[2] = _("Unknown");
1533 		strings[3] = _("Lossless");
1534 		break;
1535 	case RB_ENTRY_VIEW_COL_RATING:
1536 		propid = RHYTHMDB_PROP_RATING;
1537 		sort_func = (GCompareDataFunc) rhythmdb_query_model_double_ceiling_sort_func;
1538 
1539 		gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &column_width, NULL);
1540 		column_width = column_width * 5 + 5;
1541 		resizable = FALSE;
1542 		title = _("Rating");
1543 		key = "Rating";
1544 
1545 		renderer = rb_cell_renderer_rating_new ();
1546 		gtk_tree_view_column_pack_start (column, renderer, TRUE);
1547 		gtk_tree_view_column_set_cell_data_func (column, renderer,
1548 							 (GtkTreeCellDataFunc)
1549 							 rb_entry_view_rating_cell_data_func,
1550 							 view,
1551 							 NULL);
1552 		g_signal_connect_object (renderer,
1553 					 "rated",
1554 					 G_CALLBACK (rb_entry_view_rated_cb),
1555 					 view,
1556 					 0);
1557 		break;
1558 	case RB_ENTRY_VIEW_COL_PLAY_COUNT:
1559 		propid = RHYTHMDB_PROP_PLAY_COUNT;
1560 		cell_data->propid = propid;
1561 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_play_count_cell_data_func;
1562 		sort_propid = cell_data->propid;
1563 		sort_func = (GCompareDataFunc) rhythmdb_query_model_ulong_sort_func;
1564 		title = _("Play Count");
1565 		key = "PlayCount";
1566 		strings[0] = title;
1567 		strings[1] = _("Never");
1568 		strings[2] = "9999";
1569 		break;
1570 	case RB_ENTRY_VIEW_COL_LAST_PLAYED:
1571 		propid = RHYTHMDB_PROP_LAST_PLAYED;
1572 		cell_data->propid = RHYTHMDB_PROP_LAST_PLAYED_STR;
1573 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_string_cell_data_func;
1574 		sort_propid = RHYTHMDB_PROP_LAST_PLAYED;
1575 		sort_func = (GCompareDataFunc) rhythmdb_query_model_ulong_sort_func;
1576 		title = _("Last Played");
1577 		key = "LastPlayed";
1578 		strings[0] = title;
1579 		strings[1] = rb_entry_view_get_time_date_column_sample ();
1580 		strings[2] = _("Never");
1581 		break;
1582 	case RB_ENTRY_VIEW_COL_FIRST_SEEN:
1583 		propid = RHYTHMDB_PROP_FIRST_SEEN;
1584 		cell_data->propid = RHYTHMDB_PROP_FIRST_SEEN_STR;
1585 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_string_cell_data_func;
1586 		sort_propid = RHYTHMDB_PROP_FIRST_SEEN;
1587 		sort_func = (GCompareDataFunc) rhythmdb_query_model_ulong_sort_func;
1588 		title = _("Date Added");
1589 		key = "FirstSeen";
1590 		strings[0] = title;
1591 		strings[1] = rb_entry_view_get_time_date_column_sample ();
1592 		break;
1593 	case RB_ENTRY_VIEW_COL_LAST_SEEN:
1594 		propid = RHYTHMDB_PROP_LAST_SEEN;
1595 		cell_data->propid = RHYTHMDB_PROP_LAST_SEEN_STR;
1596 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_string_cell_data_func;
1597 		sort_propid = RHYTHMDB_PROP_LAST_SEEN;
1598 		sort_func = (GCompareDataFunc) rhythmdb_query_model_ulong_sort_func;
1599 		title = _("Last Seen");
1600 		key = "LastSeen";
1601 		strings[0] = title;
1602 		strings[1] = rb_entry_view_get_time_date_column_sample ();
1603 		break;
1604 	case RB_ENTRY_VIEW_COL_LOCATION:
1605 		propid = RHYTHMDB_PROP_LOCATION;
1606 		cell_data->propid = RHYTHMDB_PROP_LOCATION;
1607 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_location_cell_data_func;
1608 		sort_propid = RHYTHMDB_PROP_LOCATION;
1609 		sort_func = (GCompareDataFunc) rhythmdb_query_model_location_sort_func;
1610 		title = _("Location");
1611 		key = "Location";
1612 		ellipsize = TRUE;
1613 		break;
1614 	case RB_ENTRY_VIEW_COL_BPM:
1615 		propid = RHYTHMDB_PROP_BPM;
1616 		cell_data->propid = propid;
1617 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_bpm_cell_data_func;
1618 		sort_func = (GCompareDataFunc) rhythmdb_query_model_double_ceiling_sort_func;
1619 		title = _("BPM");
1620 		key = "BPM";
1621 		strings[0] = title;
1622 		strings[1] = "999.99";
1623 		break;
1624 	case RB_ENTRY_VIEW_COL_ERROR:
1625 		propid = RHYTHMDB_PROP_PLAYBACK_ERROR;
1626 		cell_data->propid = RHYTHMDB_PROP_PLAYBACK_ERROR;
1627 		cell_data_func = (GtkTreeCellDataFunc) rb_entry_view_string_cell_data_func;
1628 		title = _("Error");
1629 		key = "Error";
1630 		ellipsize = TRUE;
1631 		break;
1632 	default:
1633 		g_assert_not_reached ();
1634 		propid = -1;
1635 		break;
1636 	}
1637 
1638 	if (sort_propid == RHYTHMDB_NUM_PROPERTIES)
1639 		sort_propid = propid;
1640 
1641 	if (renderer == NULL) {
1642 		renderer = gtk_cell_renderer_text_new ();
1643 		gtk_tree_view_column_pack_start (column, renderer, TRUE);
1644 		gtk_tree_view_column_set_cell_data_func (column, renderer,
1645 							 cell_data_func, cell_data, g_free);
1646 
1647 		g_object_set_data (G_OBJECT (renderer), CELL_PROPID_ITEM, GINT_TO_POINTER (propid));
1648 		g_signal_connect_object (renderer, "edited",
1649 					 G_CALLBACK (rb_entry_view_cell_edited_cb),
1650 					 view, 0);
1651 		g_object_set (renderer, "single-paragraph-mode", TRUE, NULL);
1652 	} else {
1653 		g_free (cell_data);
1654 	}
1655 
1656 	if (resizable)
1657 		gtk_tree_view_column_set_resizable (column, TRUE);
1658 
1659 	gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
1660 	gtk_tree_view_column_set_clickable (column, TRUE);
1661 
1662 	if (always_visible)
1663 		g_object_set_qdata (G_OBJECT (column),
1664 				    rb_entry_view_column_always_visible,
1665 				    GINT_TO_POINTER (1));
1666 
1667 	g_hash_table_insert (view->priv->propid_column_map, GINT_TO_POINTER (propid), column);
1668 
1669 	rb_entry_view_append_column_custom (view, column, title, key, sort_func, GINT_TO_POINTER (sort_propid), NULL);
1670 
1671 	/*
1672 	 * Columns must either be expanding (ellipsized) or have a
1673 	 * fixed minimum width specified.  Otherwise, gtk+ gives them a
1674 	 * width of 0.
1675 	 */
1676 	if (ellipsize) {
1677 		g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
1678 		gtk_tree_view_column_set_expand (GTK_TREE_VIEW_COLUMN (column), TRUE);
1679 	} else if (column_width != -1) {
1680 		gtk_tree_view_column_set_fixed_width (column, column_width);
1681 	} else {
1682 		rb_entry_view_set_fixed_column_width (view, column, renderer, strings);
1683 	}
1684 }
1685 
1686 /**
1687  * rb_entry_view_append_column_custom:
1688  * @view: a #RBEntryView
1689  * @column: (transfer full): a #GtkTreeViewColumn to append
1690  * @title: title for the column (translated)
1691  * @key: sort key for the column (not translated)
1692  * @sort_func: comparison function to use for sorting on the column
1693  * @data: (closure) (scope notified): data to pass to the sort function
1694  * @data_destroy: function to use to destroy the sort data
1695  *
1696  * Appends a custom column to the entry view.
1697  */
1698 void
rb_entry_view_append_column_custom(RBEntryView * view,GtkTreeViewColumn * column,const char * title,const char * key,GCompareDataFunc sort_func,gpointer data,GDestroyNotify data_destroy)1699 rb_entry_view_append_column_custom (RBEntryView *view,
1700 				    GtkTreeViewColumn *column,
1701 				    const char *title,
1702 				    const char *key,
1703 				    GCompareDataFunc sort_func,
1704 				    gpointer data,
1705 				    GDestroyNotify data_destroy)
1706 {
1707 	rb_entry_view_insert_column_custom (view, column, title, key, sort_func, data, data_destroy, -1);
1708 }
1709 
1710 /**
1711  * rb_entry_view_insert_column_custom:
1712  * @view: a #RBEntryView
1713  * @column: (transfer full): a #GtkTreeViewColumn to append
1714  * @title: title for the column (translated)
1715  * @key: sort key for the column (not translated)
1716  * @sort_func: comparison function to use for sorting on the column
1717  * @data: (closure) (scope notified): data to pass to the sort function
1718  * @data_destroy: function to use to destroy the sort data
1719  * @position: position at which to insert the column (-1 to insert at the end)
1720  *
1721  * Inserts a custom column at the specified position.
1722  */
1723 void
rb_entry_view_insert_column_custom(RBEntryView * view,GtkTreeViewColumn * column,const char * title,const char * key,GCompareDataFunc sort_func,gpointer data,GDestroyNotify data_destroy,gint position)1724 rb_entry_view_insert_column_custom (RBEntryView *view,
1725 				    GtkTreeViewColumn *column,
1726 				    const char *title,
1727 				    const char *key,
1728 				    GCompareDataFunc sort_func,
1729 				    gpointer data,
1730 				    GDestroyNotify data_destroy,
1731 				    gint position)
1732 {
1733 	struct RBEntryViewColumnSortData *sortdata;
1734 
1735 	gtk_tree_view_column_set_title (column, title);
1736 	gtk_tree_view_column_set_reorderable (column, FALSE);
1737 
1738 
1739 	g_object_set_data_full (G_OBJECT (column), "rb-entry-view-key",
1740 				g_strdup (key), g_free);
1741 
1742 	rb_debug ("appending column: %p (title: %s, key: %s)", column, title, key);
1743 
1744 	gtk_tree_view_insert_column (GTK_TREE_VIEW (view->priv->treeview), column, position);
1745 
1746 	if (sort_func != NULL) {
1747 		sortdata = g_new (struct RBEntryViewColumnSortData, 1);
1748 		sortdata->func = (GCompareDataFunc) sort_func;
1749 		sortdata->data = data;
1750 		sortdata->data_destroy = data_destroy;
1751 		g_hash_table_insert (view->priv->column_sort_data_map, column, sortdata);
1752 
1753 		g_signal_connect_object (column, "clicked",
1754 					 G_CALLBACK (rb_entry_view_column_clicked_cb),
1755 					 view, 0);
1756 	}
1757 	g_hash_table_insert (view->priv->column_key_map, g_strdup (key), column);
1758 
1759 	rb_entry_view_sync_columns_visible (view);
1760 	rb_entry_view_sync_sorting (view);
1761 }
1762 
1763 /**
1764  * rb_entry_view_set_columns_clickable:
1765  * @view: a #RBEntryView
1766  * @clickable: if TRUE, sortable columns will be made clickable
1767  *
1768  * Makes the headers for sortable columns (those for which a sort function was
1769  * provided) clickable, so the user can set the sort order.
1770  */
1771 void
rb_entry_view_set_columns_clickable(RBEntryView * view,gboolean clickable)1772 rb_entry_view_set_columns_clickable (RBEntryView *view,
1773 				     gboolean clickable)
1774 {
1775 	GList *columns, *tem;
1776 
1777   	columns = gtk_tree_view_get_columns (GTK_TREE_VIEW (view->priv->treeview));
1778 	for (tem = columns; tem; tem = tem->next) {
1779 		/* only columns we can sort on should be clickable */
1780 		GtkTreeViewColumn *column = (GtkTreeViewColumn *) tem->data;
1781 		if (g_hash_table_lookup (view->priv->column_sort_data_map, column) != NULL)
1782 			gtk_tree_view_column_set_clickable (tem->data, clickable);
1783 	}
1784 	g_list_free (columns);
1785 }
1786 
1787 static void
rb_entry_view_constructed(GObject * object)1788 rb_entry_view_constructed (GObject *object)
1789 {
1790 	RBEntryView *view;
1791 	RhythmDBQueryModel *query_model;
1792 
1793 	RB_CHAIN_GOBJECT_METHOD (rb_entry_view_parent_class, constructed, object);
1794 
1795 	view = RB_ENTRY_VIEW (object);
1796 
1797 	view->priv->overlay = gtk_overlay_new ();
1798 	gtk_widget_set_vexpand (view->priv->overlay, TRUE);
1799 	gtk_widget_set_hexpand (view->priv->overlay, TRUE);
1800 	gtk_container_add (GTK_CONTAINER (view), view->priv->overlay);
1801 	gtk_widget_show (view->priv->overlay);
1802 
1803 	/* NautilusFloatingBar needs enter and leavy notify events */
1804 	gtk_widget_add_events (view->priv->overlay, GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
1805 
1806 	view->priv->scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1807 	gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (view->priv->scrolled_window),
1808 					GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
1809 	gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (view->priv->scrolled_window), GTK_SHADOW_NONE);
1810 	gtk_widget_show (view->priv->scrolled_window);
1811 	gtk_container_add (GTK_CONTAINER (view->priv->overlay), view->priv->scrolled_window);
1812 
1813 	view->priv->treeview = gtk_tree_view_new ();
1814 	gtk_tree_view_set_fixed_height_mode (GTK_TREE_VIEW (view->priv->treeview), TRUE);
1815 
1816 	gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW (view->priv->treeview),
1817 					     type_ahead_search_func,
1818 					     view, NULL);
1819 
1820 	g_signal_connect_object (view->priv->treeview,
1821 			         "button_press_event",
1822 			         G_CALLBACK (rb_entry_view_button_press_cb),
1823 			         view,
1824 				 0);
1825 	g_signal_connect_object (view->priv->treeview,
1826 			         "row_activated",
1827 			         G_CALLBACK (rb_entry_view_row_activated_cb),
1828 			         view,
1829 				 0);
1830 	g_signal_connect_object (view->priv->treeview,
1831 				 "popup_menu",
1832 				 G_CALLBACK (rb_entry_view_popup_menu_cb),
1833 				 view,
1834 				 0);
1835 	view->priv->selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view->priv->treeview));
1836 	g_signal_connect_object (view->priv->selection,
1837 			         "changed",
1838 			         G_CALLBACK (rb_entry_view_selection_changed_cb),
1839 			         view,
1840 				 0);
1841 	g_object_ref (view->priv->selection);
1842 
1843 	gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (view->priv->treeview), TRUE);
1844 	gtk_tree_selection_set_mode (view->priv->selection, GTK_SELECTION_MULTIPLE);
1845 
1846 	if (view->priv->is_drag_source) {
1847 		rb_tree_dnd_add_drag_source_support (GTK_TREE_VIEW (view->priv->treeview),
1848 						     GDK_BUTTON1_MASK,
1849 						     rb_entry_view_drag_types,
1850 						     G_N_ELEMENTS (rb_entry_view_drag_types),
1851 						     GDK_ACTION_COPY);
1852 	}
1853 
1854 	if (view->priv->is_drag_dest) {
1855 		rb_tree_dnd_add_drag_dest_support (GTK_TREE_VIEW (view->priv->treeview),
1856 						   RB_TREE_DEST_CAN_DROP_BETWEEN | RB_TREE_DEST_EMPTY_VIEW_DROP,
1857 						   rb_entry_view_drag_types,
1858 						   G_N_ELEMENTS (rb_entry_view_drag_types),
1859 						   GDK_ACTION_COPY | GDK_ACTION_MOVE);
1860 	}
1861 
1862 	gtk_container_add (GTK_CONTAINER (view->priv->scrolled_window), view->priv->treeview);
1863 
1864 	{
1865 		GtkTreeViewColumn *column;
1866 		GtkCellRenderer *renderer;
1867 		GtkWidget *image_widget;
1868 
1869 		/* Playing icon column */
1870 		column = GTK_TREE_VIEW_COLUMN (gtk_tree_view_column_new ());
1871 		renderer = rb_cell_renderer_pixbuf_new ();
1872 		g_object_set (renderer, "stock-size", GTK_ICON_SIZE_MENU, NULL);
1873 		if (gtk_check_version (3, 16, 0) != NULL) {
1874 			g_object_set (renderer, "follow-state", TRUE, NULL);
1875 		}
1876 
1877 		gtk_tree_view_column_pack_start (column, renderer, TRUE);
1878 		gtk_tree_view_column_set_cell_data_func (column, renderer,
1879 							 (GtkTreeCellDataFunc)
1880 							 rb_entry_view_playing_cell_data_func,
1881 							 view,
1882 							 NULL);
1883 
1884 		image_widget = gtk_image_new_from_icon_name ("audio-volume-high-symbolic", GTK_ICON_SIZE_MENU);
1885 		gtk_tree_view_column_set_widget (column, image_widget);
1886 		gtk_widget_show_all (image_widget);
1887 
1888 		gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
1889 		gtk_tree_view_append_column (GTK_TREE_VIEW (view->priv->treeview), column);
1890 		g_signal_connect_swapped (renderer,
1891 					  "pixbuf-clicked",
1892 					  G_CALLBACK (rb_entry_view_pixbuf_clicked_cb),
1893 					  view);
1894 
1895 		gtk_widget_set_tooltip_text (gtk_tree_view_column_get_widget (column),
1896 					     _("Now Playing"));
1897 
1898 		g_signal_connect (column,
1899 				  "clicked",
1900 				  G_CALLBACK (rb_entry_view_playing_column_clicked_cb),
1901 				  view);
1902 		gtk_tree_view_column_set_clickable (column, TRUE);
1903 	}
1904 
1905 	query_model = rhythmdb_query_model_new_empty (view->priv->db);
1906 	rb_entry_view_set_model (view, RHYTHMDB_QUERY_MODEL (query_model));
1907 	g_object_unref (query_model);
1908 
1909 	view->priv->status = nautilus_floating_bar_new (NULL, NULL, FALSE);
1910 	gtk_widget_set_no_show_all (view->priv->status, TRUE);
1911 	gtk_widget_set_halign (view->priv->status, GTK_ALIGN_END);
1912 	gtk_widget_set_valign (view->priv->status, GTK_ALIGN_END);
1913 	gtk_overlay_add_overlay (GTK_OVERLAY (view->priv->overlay), view->priv->status);
1914 }
1915 
1916 static void
rb_entry_view_rated_cb(RBCellRendererRating * cellrating,const char * path_string,double rating,RBEntryView * view)1917 rb_entry_view_rated_cb (RBCellRendererRating *cellrating,
1918 			const char *path_string,
1919 			double rating,
1920 			RBEntryView *view)
1921 {
1922 	GtkTreePath *path;
1923 	RhythmDBEntry *entry;
1924 	GValue value = { 0, };
1925 
1926 	g_return_if_fail (rating >= 0 && rating <= 5 );
1927 	g_return_if_fail (path_string != NULL);
1928 
1929 	path = gtk_tree_path_new_from_string (path_string);
1930 	entry = rhythmdb_query_model_tree_path_to_entry (view->priv->model, path);
1931 	gtk_tree_path_free (path);
1932 
1933 	g_value_init (&value, G_TYPE_DOUBLE);
1934 	g_value_set_double (&value, rating);
1935 	rhythmdb_entry_set (view->priv->db, entry, RHYTHMDB_PROP_RATING, &value);
1936 	g_value_unset (&value);
1937 
1938 	rhythmdb_commit (view->priv->db);
1939 
1940 	rhythmdb_entry_unref (entry);
1941 }
1942 
1943 static void
rb_entry_view_pixbuf_clicked_cb(RBEntryView * view,const char * path_string,RBCellRendererPixbuf * cellpixbuf)1944 rb_entry_view_pixbuf_clicked_cb (RBEntryView          *view,
1945 				 const char           *path_string,
1946 				 RBCellRendererPixbuf *cellpixbuf)
1947 {
1948 	GtkTreePath *path;
1949 	RhythmDBEntry *entry;
1950 	const gchar *error;
1951 
1952 	g_return_if_fail (path_string != NULL);
1953 
1954 	path = gtk_tree_path_new_from_string (path_string);
1955 	entry = rhythmdb_query_model_tree_path_to_entry (view->priv->model, path);
1956 
1957 	gtk_tree_path_free (path);
1958 
1959 	error = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_PLAYBACK_ERROR);
1960 	if (error) {
1961 		rb_error_dialog (NULL, _("Playback Error"), "%s", error);
1962 	}
1963 
1964 	rhythmdb_entry_unref (entry);
1965 }
1966 
1967 static void
rb_entry_view_playing_column_clicked_cb(GtkTreeViewColumn * column,RBEntryView * view)1968 rb_entry_view_playing_column_clicked_cb (GtkTreeViewColumn *column,
1969 					 RBEntryView *view)
1970 {
1971 	if (view->priv->playing_entry) {
1972 		rb_entry_view_scroll_to_entry (view, view->priv->playing_entry);
1973 	}
1974 }
1975 
1976 static void
rb_entry_view_playing_song_changed(RBShellPlayer * player,RhythmDBEntry * entry,RBEntryView * view)1977 rb_entry_view_playing_song_changed (RBShellPlayer *player,
1978 				    RhythmDBEntry *entry,
1979 				    RBEntryView *view)
1980 {
1981  	gboolean realized, visible;
1982   	GtkTreeIter iter;
1983 
1984  	g_return_if_fail (RB_IS_ENTRY_VIEW (view));
1985 
1986 	if (view->priv->playing_entry != NULL) {
1987 		if (view->priv->playing_state != RB_ENTRY_VIEW_NOT_PLAYING)
1988 			rb_entry_view_emit_row_changed (view, view->priv->playing_entry);
1989 		g_object_unref (view->priv->playing_model);
1990 	}
1991 
1992  	view->priv->playing_entry = entry;
1993  	view->priv->playing_model = view->priv->model;
1994  	g_object_ref (view->priv->playing_model);
1995 
1996  	if (view->priv->playing_state != RB_ENTRY_VIEW_NOT_PLAYING) {
1997 		if (view->priv->playing_entry != NULL) {
1998 			view->priv->playing_entry_in_view =
1999 				rb_entry_view_emit_row_changed (view, view->priv->playing_entry);
2000 		}
2001 
2002 		if (view->priv->playing_entry
2003 		    && view->priv->playing_entry_in_view) {
2004 		    rb_entry_view_entry_is_visible (view, view->priv->playing_entry,
2005 						    &realized, &visible, &iter);
2006 		    if (realized && !visible)
2007 			    rb_entry_view_scroll_to_iter (view, &iter);
2008 		}
2009 	}
2010 }
2011 
2012 static gboolean
harvest_entries(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,GList ** list)2013 harvest_entries (GtkTreeModel *model,
2014 		 GtkTreePath *path,
2015 		 GtkTreeIter *iter,
2016 		 GList      **list)
2017 {
2018 	RhythmDBEntry *entry;
2019 
2020 	gtk_tree_model_get (model, iter, 0, &entry, -1);
2021 
2022 	*list = g_list_prepend (*list, entry);
2023 
2024 	return FALSE;
2025 }
2026 
2027 /**
2028  * rb_entry_view_get_selected_entries:
2029  * @view: a #RBEntryView
2030  *
2031  * Gathers the selected entries from the view.
2032  *
2033  * Return value: (element-type RhythmDBEntry) (transfer full): a #GList of
2034  * selected entries in the view.
2035  */
2036 GList *
rb_entry_view_get_selected_entries(RBEntryView * view)2037 rb_entry_view_get_selected_entries (RBEntryView *view)
2038 {
2039 	GList *list = NULL;
2040 
2041 	gtk_tree_selection_selected_foreach (view->priv->selection,
2042 					     (GtkTreeSelectionForeachFunc) harvest_entries,
2043 					     (gpointer) &list);
2044 
2045 	list = g_list_reverse (list);
2046 	return list;
2047 }
2048 
2049 static gboolean
rb_entry_view_button_press_cb(GtkTreeView * treeview,GdkEventButton * event,RBEntryView * view)2050 rb_entry_view_button_press_cb (GtkTreeView *treeview,
2051 			       GdkEventButton *event,
2052 			       RBEntryView *view)
2053 {
2054 	if (event->button == 3) {
2055 		GtkTreePath *path;
2056 		RhythmDBEntry *entry;
2057 
2058 		gtk_tree_view_get_path_at_pos (treeview, event->x, event->y, &path, NULL, NULL, NULL);
2059 		if (path != NULL) {
2060 			GList *selected;
2061 			entry = rhythmdb_query_model_tree_path_to_entry (view->priv->model, path);
2062 
2063 			selected = rb_entry_view_get_selected_entries (view);
2064 
2065 			if (!g_list_find (selected, entry))
2066 				rb_entry_view_select_entry (view, entry);
2067 
2068 			g_list_free (selected);
2069 
2070 			rhythmdb_entry_unref (entry);
2071 		}
2072 		g_signal_emit (G_OBJECT (view), rb_entry_view_signals[SHOW_POPUP], 0, (path != NULL));
2073 		return TRUE;
2074 	}
2075 
2076 	return FALSE;
2077 }
2078 
2079 static gboolean
rb_entry_view_popup_menu_cb(GtkTreeView * treeview,RBEntryView * view)2080 rb_entry_view_popup_menu_cb (GtkTreeView *treeview,
2081 			     RBEntryView *view)
2082 {
2083 	if (gtk_tree_selection_count_selected_rows (gtk_tree_view_get_selection (treeview)) == 0)
2084 		return FALSE;
2085 
2086 	g_signal_emit (G_OBJECT (view), rb_entry_view_signals[SHOW_POPUP], 0);
2087 	return TRUE;
2088 }
2089 
2090 static gboolean
rb_entry_view_emit_selection_changed(RBEntryView * view)2091 rb_entry_view_emit_selection_changed (RBEntryView *view)
2092 {
2093 	gboolean available;
2094 	gint sel_count;
2095 
2096 	sel_count = gtk_tree_selection_count_selected_rows (view->priv->selection);
2097 	available = (sel_count > 0);
2098 
2099 	if (available != view->priv->have_selection) {
2100 		gint entry_count;
2101 
2102 		entry_count = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (view->priv->model), NULL);
2103 		view->priv->have_complete_selection = (sel_count == entry_count);
2104 
2105 		view->priv->have_selection = available;
2106 
2107 		g_signal_emit (G_OBJECT (view), rb_entry_view_signals[HAVE_SEL_CHANGED], 0, available);
2108 	}
2109 
2110 	view->priv->selection_changed_id = 0;
2111 	g_signal_emit (G_OBJECT (view), rb_entry_view_signals[SELECTION_CHANGED], 0);
2112 	return FALSE;
2113 }
2114 
2115 static void
rb_entry_view_selection_changed_cb(GtkTreeSelection * selection,RBEntryView * view)2116 rb_entry_view_selection_changed_cb (GtkTreeSelection *selection,
2117 				    RBEntryView *view)
2118 {
2119 	if (view->priv->selection_changed_id == 0)
2120 		view->priv->selection_changed_id = g_idle_add ((GSourceFunc)rb_entry_view_emit_selection_changed, view);
2121 }
2122 
2123 /**
2124  * rb_entry_view_have_selection:
2125  * @view: a #RBEntryView
2126  *
2127  * Determines whether there is an active selection in the view.
2128  *
2129  * Return value: TRUE if one or more rows are selected
2130  */
2131 gboolean
rb_entry_view_have_selection(RBEntryView * view)2132 rb_entry_view_have_selection (RBEntryView *view)
2133 {
2134 	return view->priv->have_selection;
2135 }
2136 
2137 /**
2138  * rb_entry_view_have_complete_selection:
2139  * @view: a #RBEntryView
2140  *
2141  * Determines whether all entries in the view are selected.
2142  *
2143  * Return value: TRUE if all rows in the view are selected
2144  */
2145 gboolean
rb_entry_view_have_complete_selection(RBEntryView * view)2146 rb_entry_view_have_complete_selection (RBEntryView *view)
2147 {
2148 	return view->priv->have_complete_selection;
2149 }
2150 
2151 static void
rb_entry_view_row_activated_cb(GtkTreeView * treeview,GtkTreePath * path,GtkTreeViewColumn * column,RBEntryView * view)2152 rb_entry_view_row_activated_cb (GtkTreeView *treeview,
2153 			       GtkTreePath *path,
2154 			       GtkTreeViewColumn *column,
2155 			       RBEntryView *view)
2156 {
2157 	RhythmDBEntry *entry;
2158 
2159 	rb_debug ("row activated");
2160 
2161 	entry = rhythmdb_query_model_tree_path_to_entry (view->priv->model, path);
2162 
2163 	rb_debug ("emitting entry activated");
2164 	g_signal_emit (G_OBJECT (view), rb_entry_view_signals[ENTRY_ACTIVATED], 0, entry);
2165 
2166 	rhythmdb_entry_unref (entry);
2167 }
2168 
2169 static void
rb_entry_view_row_inserted_cb(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,RBEntryView * view)2170 rb_entry_view_row_inserted_cb (GtkTreeModel *model,
2171 			       GtkTreePath *path,
2172 			       GtkTreeIter *iter,
2173 			       RBEntryView *view)
2174 {
2175 	RhythmDBEntry *entry = rhythmdb_query_model_tree_path_to_entry (RHYTHMDB_QUERY_MODEL (model), path);
2176 
2177 	rb_debug ("row added");
2178 	g_signal_emit (G_OBJECT (view), rb_entry_view_signals[ENTRY_ADDED], 0, entry);
2179 	rhythmdb_entry_unref (entry);
2180 }
2181 
2182 static void
rb_entry_view_row_deleted_cb(GtkTreeModel * model,GtkTreePath * path,RBEntryView * view)2183 rb_entry_view_row_deleted_cb (GtkTreeModel *model,
2184 			      GtkTreePath *path,
2185 			      RBEntryView *view)
2186 {
2187 	RhythmDBEntry *entry = rhythmdb_query_model_tree_path_to_entry (RHYTHMDB_QUERY_MODEL (model), path);
2188 
2189 	rb_debug ("row deleted");
2190 	g_signal_emit (G_OBJECT (view), rb_entry_view_signals[ENTRY_DELETED], 0, entry);
2191 	rhythmdb_entry_unref (entry);
2192 }
2193 
2194 static void
rb_entry_view_rows_reordered_cb(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gint * order,RBEntryView * view)2195 rb_entry_view_rows_reordered_cb (GtkTreeModel *model,
2196 				 GtkTreePath *path,
2197 				 GtkTreeIter *iter,
2198 				 gint *order,
2199 				 RBEntryView *view)
2200 {
2201 	GList *selected_rows;
2202 	GList *i;
2203 	gint model_size;
2204 	gboolean scrolled = FALSE;
2205 
2206 	rb_debug ("rows reordered");
2207 
2208 	model_size = gtk_tree_model_iter_n_children (model, NULL);
2209 
2210 	/* check if a selected row was moved; if so, we'll
2211 	 * need to move the selection too.
2212 	 */
2213 	selected_rows = gtk_tree_selection_get_selected_rows (view->priv->selection,
2214 							      NULL);
2215 	for (i = selected_rows; i != NULL; i = i->next) {
2216 		GtkTreePath *path = (GtkTreePath *)i->data;
2217 		gint index = gtk_tree_path_get_indices (path)[0];
2218 		gint newindex;
2219 		if (order[index] != index) {
2220 			GtkTreePath *newpath;
2221 			gtk_tree_selection_unselect_path (view->priv->selection, path);
2222 
2223 			for (newindex = 0; newindex < model_size; newindex++) {
2224 				if (order[newindex] == index) {
2225 					newpath = gtk_tree_path_new_from_indices (newindex, -1);
2226 					gtk_tree_selection_select_path (view->priv->selection, newpath);
2227 					if (!scrolled) {
2228 						GtkTreeViewColumn *col;
2229 						GtkTreeView *treeview = GTK_TREE_VIEW (view->priv->treeview);
2230 
2231 						col = gtk_tree_view_get_column (treeview, 0);
2232 						gtk_tree_view_scroll_to_cell (treeview, newpath, col, TRUE, 0.5, 0.0);
2233 						scrolled = TRUE;
2234 					}
2235 					gtk_tree_path_free (newpath);
2236 					break;
2237 				}
2238 			}
2239 
2240 		}
2241 	}
2242 
2243 	g_list_foreach (selected_rows, (GFunc) gtk_tree_path_free, NULL);
2244 	g_list_free (selected_rows);
2245 
2246 	gtk_widget_queue_draw (GTK_WIDGET (view));
2247 }
2248 
2249 /**
2250  * rb_entry_view_select_all:
2251  * @view: a #RBEntryView
2252  *
2253  * Selects all rows in the view
2254  */
2255 void
rb_entry_view_select_all(RBEntryView * view)2256 rb_entry_view_select_all (RBEntryView *view)
2257 {
2258 	gtk_tree_selection_select_all (view->priv->selection);
2259 }
2260 
2261 /**
2262  * rb_entry_view_select_none:
2263  * @view: a #RBEntryView
2264  *
2265  * Deselects all rows in the view.
2266  */
2267 void
rb_entry_view_select_none(RBEntryView * view)2268 rb_entry_view_select_none (RBEntryView *view)
2269 {
2270 	gtk_tree_selection_unselect_all (view->priv->selection);
2271 }
2272 
2273 /**
2274  * rb_entry_view_select_entry:
2275  * @view: a #RBEntryView
2276  * @entry: a #RhythmDBEntry to select
2277  *
2278  * If the specified entry is present in the view, it is added
2279  * to the selection.
2280  */
2281 void
rb_entry_view_select_entry(RBEntryView * view,RhythmDBEntry * entry)2282 rb_entry_view_select_entry (RBEntryView *view,
2283 			    RhythmDBEntry *entry)
2284 {
2285 	GtkTreeIter iter;
2286 
2287 	if (entry == NULL)
2288 		return;
2289 
2290 	rb_entry_view_select_none (view);
2291 
2292 	if (rhythmdb_query_model_entry_to_iter (view->priv->model,
2293 						entry, &iter)) {
2294 		gtk_tree_selection_select_iter (view->priv->selection, &iter);
2295 	}
2296 }
2297 
2298 /**
2299  * rb_entry_view_scroll_to_entry:
2300  * @view: a #RBEntryView
2301  * @entry: a #RhythmDBEntry to scroll to
2302  *
2303  * If the specified entry is present in the view, the view will be
2304  * scrolled so that the entry is visible.
2305  */
2306 void
rb_entry_view_scroll_to_entry(RBEntryView * view,RhythmDBEntry * entry)2307 rb_entry_view_scroll_to_entry (RBEntryView *view,
2308 			       RhythmDBEntry *entry)
2309 {
2310 	GtkTreeIter iter;
2311 
2312 	if (rhythmdb_query_model_entry_to_iter (view->priv->model,
2313 						entry, &iter)) {
2314 		rb_entry_view_scroll_to_iter (view, &iter);
2315 	}
2316 }
2317 
2318 static void
rb_entry_view_scroll_to_iter(RBEntryView * view,GtkTreeIter * iter)2319 rb_entry_view_scroll_to_iter (RBEntryView *view,
2320 			      GtkTreeIter *iter)
2321 {
2322 	GtkTreePath *path;
2323 
2324 	/* It's possible to we can be asked to scroll the play queue's entry
2325 	 * view to the playing entry before the view has ever been displayed.
2326 	 * This will result in gtk+ warnings, so we avoid it in this case.
2327 	 */
2328 	if (!gtk_widget_get_realized (GTK_WIDGET (view)))
2329 		return;
2330 
2331 	path = gtk_tree_model_get_path (GTK_TREE_MODEL (view->priv->model), iter);
2332 	gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (view->priv->treeview), path,
2333 				      gtk_tree_view_get_column (GTK_TREE_VIEW (view->priv->treeview), 0),
2334 				      TRUE, 0.5, 0.0);
2335 
2336 	if ((gtk_tree_selection_count_selected_rows (view->priv->selection) != 1) ||
2337 	    (gtk_tree_selection_path_is_selected (view->priv->selection, path) == FALSE)) {
2338 		gtk_tree_selection_unselect_all (view->priv->selection);
2339 		gtk_tree_selection_select_iter (view->priv->selection, iter);
2340 	}
2341 
2342 	gtk_tree_path_free (path);
2343 }
2344 
2345 /**
2346  * rb_entry_view_get_entry_visible:
2347  * @view: a #RBEntryView
2348  * @entry: a #RhythmDBEntry to check
2349  *
2350  * Determines whether a specified entry is present in the view
2351  * and is currently visible.
2352  *
2353  * Return value: TRUE if the entry is visible
2354  */
2355 gboolean
rb_entry_view_get_entry_visible(RBEntryView * view,RhythmDBEntry * entry)2356 rb_entry_view_get_entry_visible (RBEntryView *view,
2357 				 RhythmDBEntry *entry)
2358 {
2359 	GtkTreeIter unused;
2360 	gboolean realized, visible;
2361 
2362 	if (view->priv->playing_model != view->priv->model)
2363 		return FALSE;
2364 
2365 	rb_entry_view_entry_is_visible (view, entry, &realized, &visible,
2366 					&unused);
2367 	return realized && visible;
2368 }
2369 
2370 /**
2371  * rb_entry_view_get_entry_contained:
2372  * @view: a #RBEntryView
2373  * @entry: a #RhythmDBEntry to check
2374  *
2375  * Determines whether a specified entry is present in the view.
2376  *
2377  * Return value: TRUE if the entry is present in the view
2378  */
2379 gboolean
rb_entry_view_get_entry_contained(RBEntryView * view,RhythmDBEntry * entry)2380 rb_entry_view_get_entry_contained (RBEntryView *view,
2381 				   RhythmDBEntry *entry)
2382 {
2383 	GtkTreeIter unused;
2384 
2385 	return rhythmdb_query_model_entry_to_iter (view->priv->model,
2386 						   entry, &unused);
2387 }
2388 
2389 static void
rb_entry_view_entry_is_visible(RBEntryView * view,RhythmDBEntry * entry,gboolean * realized,gboolean * visible,GtkTreeIter * iter)2390 rb_entry_view_entry_is_visible (RBEntryView *view,
2391 				RhythmDBEntry *entry,
2392 				gboolean *realized,
2393 				gboolean *visible,
2394 				GtkTreeIter *iter)
2395 {
2396 	GtkTreePath *path;
2397 	GdkRectangle rect;
2398 
2399 	*realized = FALSE;
2400 	*visible = FALSE;
2401 
2402 	g_return_if_fail (entry != NULL);
2403 
2404 	if (!gtk_widget_get_realized (GTK_WIDGET (view)))
2405 		return;
2406 
2407 	*realized = TRUE;
2408 
2409 	if (!rhythmdb_query_model_entry_to_iter (view->priv->model,
2410 						 entry, iter))
2411 		return;
2412 
2413 	path = gtk_tree_model_get_path (GTK_TREE_MODEL (view->priv->model), iter);
2414 	gtk_tree_view_get_cell_area (GTK_TREE_VIEW (view->priv->treeview),
2415 				     path,
2416 				     gtk_tree_view_get_column (GTK_TREE_VIEW (view->priv->treeview), 0),
2417 				     &rect);
2418 
2419 	gtk_tree_path_free (path);
2420 
2421 	*visible = (rect.y != 0 && rect.height != 0);
2422 }
2423 
2424 /**
2425  * rb_entry_view_enable_drag_source:
2426  * @view: a #RBEntryView
2427  * @targets: an array of #GtkTargetEntry structures defining the drag data targets
2428  * @n_targets: the number of entries in the target array
2429  *
2430  * Enables the entry view to act as a data source for drag an drop operations,
2431  * using a specified set of data targets.
2432  */
2433 void
rb_entry_view_enable_drag_source(RBEntryView * view,const GtkTargetEntry * targets,int n_targets)2434 rb_entry_view_enable_drag_source (RBEntryView *view,
2435 				 const GtkTargetEntry *targets,
2436 				 int n_targets)
2437 {
2438 	g_return_if_fail (view != NULL);
2439 
2440 	rb_tree_dnd_add_drag_source_support (GTK_TREE_VIEW (view->priv->treeview),
2441 					 GDK_BUTTON1_MASK | GDK_BUTTON3_MASK,
2442 					 targets, n_targets, GDK_ACTION_COPY);
2443 }
2444 
2445 static void
set_column_visibility(guint propid,GtkTreeViewColumn * column,GList * visible_props)2446 set_column_visibility (guint propid,
2447 		       GtkTreeViewColumn *column,
2448 		       GList *visible_props)
2449 {
2450 	gboolean visible;
2451 
2452 	if (g_object_get_qdata (G_OBJECT (column),
2453 				rb_entry_view_column_always_visible) == GINT_TO_POINTER (1))
2454 		visible = TRUE;
2455 	else
2456 		visible = (g_list_find (visible_props, GINT_TO_POINTER (propid)) != NULL);
2457 
2458 	gtk_tree_view_column_set_visible (column, visible);
2459 }
2460 
2461 static void
rb_entry_view_sync_columns_visible(RBEntryView * view)2462 rb_entry_view_sync_columns_visible (RBEntryView *view)
2463 {
2464 	GList *visible_properties = NULL;
2465 
2466 	g_return_if_fail (view != NULL);
2467 
2468 	if (view->priv->visible_columns != NULL) {
2469 		int i;
2470 		for (i = 0; view->priv->visible_columns[i] != NULL && *(view->priv->visible_columns[i]); i++) {
2471 			int value = rhythmdb_propid_from_nice_elt_name (view->priv->db, (const xmlChar *)view->priv->visible_columns[i]);
2472 			rb_debug ("visible columns: %s => %d", view->priv->visible_columns[i], value);
2473 
2474 			if ((value >= 0) && (value < RHYTHMDB_NUM_PROPERTIES))
2475 				visible_properties = g_list_prepend (visible_properties, GINT_TO_POINTER (value));
2476 		}
2477 	}
2478 
2479 	g_hash_table_foreach (view->priv->propid_column_map, (GHFunc) set_column_visibility, visible_properties);
2480 	g_list_free (visible_properties);
2481 }
2482 
2483 /**
2484  * rb_entry_view_set_state:
2485  * @view: a #RBEntryView
2486  * @state: the new playing entry state
2487  *
2488  * Sets the icon to be drawn in the 'playing' column next to the
2489  * current playing entry.  RB_ENTRY_VIEW_PLAYING and RB_ENTRY_VIEW_PAUSED
2490  * should be used when the source containing the entry view is playing,
2491  * and RB_ENTRY_VIEW_NOT_PLAYING otherwise.
2492  */
2493 void
rb_entry_view_set_state(RBEntryView * view,RBEntryViewState state)2494 rb_entry_view_set_state (RBEntryView *view,
2495 			 RBEntryViewState state)
2496 {
2497 	g_return_if_fail (RB_IS_ENTRY_VIEW (view));
2498 	g_object_set (view, "playing-state", state, NULL);
2499 }
2500 
2501 static void
rb_entry_view_grab_focus(GtkWidget * widget)2502 rb_entry_view_grab_focus (GtkWidget *widget)
2503 {
2504 	RBEntryView *view = RB_ENTRY_VIEW (widget);
2505 
2506 	gtk_widget_grab_focus (GTK_WIDGET (view->priv->treeview));
2507 }
2508 
2509 static gboolean
rb_entry_view_emit_row_changed(RBEntryView * view,RhythmDBEntry * entry)2510 rb_entry_view_emit_row_changed (RBEntryView *view,
2511 				RhythmDBEntry *entry)
2512 {
2513 	GtkTreeIter iter;
2514 	GtkTreePath *path;
2515 
2516 	if (!rhythmdb_query_model_entry_to_iter (view->priv->model, entry, &iter))
2517 		return FALSE;
2518 
2519 	path = gtk_tree_model_get_path (GTK_TREE_MODEL (view->priv->model),
2520 					&iter);
2521 	gtk_tree_model_row_changed (GTK_TREE_MODEL (view->priv->model),
2522 				    path, &iter);
2523 	gtk_tree_path_free (path);
2524 	return TRUE;
2525 }
2526 
2527 /**
2528  * rb_entry_view_set_fixed_column_width:
2529  * @view: a #RBEntryView
2530  * @column: the column to set the width for
2531  * @renderer: a temporary cell renderer to use
2532  * @strings: (array zero-terminated=1): a NULL-terminated array of strings that will be displayed in the column
2533  *
2534  * Helper function for calling @rb_set_tree_view_column_fixed_width on
2535  * a column.  This is important for performance reasons, as having the
2536  * tree view measure the strings in each of 20000 rows is very slow.
2537  */
2538 void
rb_entry_view_set_fixed_column_width(RBEntryView * view,GtkTreeViewColumn * column,GtkCellRenderer * renderer,const gchar ** strings)2539 rb_entry_view_set_fixed_column_width (RBEntryView *view,
2540 				      GtkTreeViewColumn *column,
2541 				      GtkCellRenderer *renderer,
2542 				      const gchar **strings)
2543 {
2544 	rb_set_tree_view_column_fixed_width (view->priv->treeview,
2545 					     column,
2546 					     renderer,
2547 					     strings,
2548 					     5);
2549 }
2550 
2551 /**
2552  * rb_entry_view_get_time_date_column_sample:
2553  *
2554  * Returns a sample string for use in columns displaying times
2555  * and dates in 'friendly' form (see @rb_utf_friendly_time).
2556  * For use with @rb_entry_view_set_fixed_column_width.
2557  *
2558  * Return value: sample date string
2559  */
2560 const char *
rb_entry_view_get_time_date_column_sample()2561 rb_entry_view_get_time_date_column_sample ()
2562 {
2563 	static const char *sample = NULL;
2564 	if (sample == NULL) {
2565  		time_t then;
2566 
2567  		/* A reasonable estimate of the widest friendly date
2568  		   is "Yesterday NN:NN PM" */
2569  		then = time (NULL) - 86400;
2570  		sample = rb_utf_friendly_time (then);
2571   	}
2572 
2573 	return sample;
2574 }
2575 
2576 /**
2577  * rb_entry_view_resort_model:
2578  * @view: a #RBEntryView to resort
2579  *
2580  * Resorts the entries in the entry view.  Mostly to be used
2581  * when a new model is associated with the view.
2582  */
2583 void
rb_entry_view_resort_model(RBEntryView * view)2584 rb_entry_view_resort_model (RBEntryView *view)
2585 {
2586 	struct RBEntryViewColumnSortData *sort_data;
2587 
2588 	if (view->priv->sorting_column == NULL) {
2589 		rb_debug ("can't sort yet, the sorting column isn't here");
2590 		return;
2591 	}
2592 
2593 	sort_data = g_hash_table_lookup (view->priv->column_sort_data_map,
2594 					 view->priv->sorting_column);
2595 	g_assert (sort_data);
2596 
2597 	rhythmdb_query_model_set_sort_order (view->priv->model,
2598 					     sort_data->func,
2599 					     sort_data->data,
2600 					     NULL,
2601 					     (view->priv->sorting_order == GTK_SORT_DESCENDING));
2602 }
2603 
2604 /**
2605  * rb_entry_view_set_column_editable:
2606  * @view: a #RBEntryView
2607  * @column: a #RBEntryViewColumn to update
2608  * @editable: %TRUE to make the column editable, %FALSE otherwise
2609  *
2610  * Enables in-place editing of the values in a column.
2611  * The underlying %RhythmDBEntry is updated when editing is complete.
2612  */
2613 void
rb_entry_view_set_column_editable(RBEntryView * view,RBEntryViewColumn column_type,gboolean editable)2614 rb_entry_view_set_column_editable (RBEntryView *view,
2615 				   RBEntryViewColumn column_type,
2616 				   gboolean editable)
2617 {
2618 	GtkTreeViewColumn *column;
2619 	GList *renderers;
2620 
2621 	column = rb_entry_view_get_column (view, column_type);
2622 	if (column == NULL)
2623 		return;
2624 
2625 	renderers = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (column));
2626 	g_object_set (renderers->data, "editable", editable, NULL);
2627 	g_list_free (renderers);
2628 }
2629 
2630 /**
2631  * rb_entry_view_set_status:
2632  * @view: a #RBEntryView
2633  * @status: status text to display, or NULL
2634  * @busy: whether the source is busy
2635  *
2636  * Sets the status text to be displayed inside the entry view, and
2637  * shows the spinner if busy.
2638  */
2639 void
rb_entry_view_set_status(RBEntryView * view,const char * status,gboolean busy)2640 rb_entry_view_set_status (RBEntryView *view, const char *status, gboolean busy)
2641 {
2642 	if (status == NULL) {
2643 		gtk_widget_hide (view->priv->status);
2644 	} else {
2645 		nautilus_floating_bar_set_primary_label (NAUTILUS_FLOATING_BAR (view->priv->status), status);
2646 		nautilus_floating_bar_set_show_spinner (NAUTILUS_FLOATING_BAR (view->priv->status), busy);
2647 		gtk_widget_show (view->priv->status);
2648 	}
2649 }
2650 
2651 /* This should really be standard. */
2652 #define ENUM_ENTRY(NAME, DESC) { NAME, "" #NAME "", DESC }
2653 
2654 GType
rb_entry_view_column_get_type(void)2655 rb_entry_view_column_get_type (void)
2656 {
2657 	static GType etype = 0;
2658 
2659 	if (etype == 0)	{
2660 		static const GEnumValue values[] = {
2661 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_TRACK_NUMBER, "track-number"),
2662 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_TITLE, "title"),
2663 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_ARTIST, "artist"),
2664 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_ALBUM, "album"),
2665 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_GENRE, "genre"),
2666 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_COMMENT, "comment"),
2667 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_DURATION, "duration"),
2668 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_QUALITY, "quality"),
2669 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_RATING, "rating"),
2670 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_PLAY_COUNT, "play-count"),
2671 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_YEAR, "year"),
2672 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_LAST_PLAYED, "last-played"),
2673 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_FIRST_SEEN, "first-seen"),
2674 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_LAST_SEEN, "last-seen"),
2675 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_LOCATION, "location"),
2676 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_BPM, "bpm"),
2677 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_ERROR, "error"),
2678 			ENUM_ENTRY (RB_ENTRY_VIEW_COL_COMPOSER, "composer"),
2679 			{ 0, 0, 0 }
2680 		};
2681 
2682 		etype = g_enum_register_static ("RBEntryViewColumn", values);
2683 	}
2684 
2685 	return etype;
2686 }
2687 
2688 GType
rb_entry_view_state_get_type(void)2689 rb_entry_view_state_get_type (void)
2690 {
2691 	static GType etype = 0;
2692 
2693 	if (etype == 0)	{
2694 		static const GEnumValue values[] = {
2695 			ENUM_ENTRY (RB_ENTRY_VIEW_NOT_PLAYING, "not-playing"),
2696 			ENUM_ENTRY (RB_ENTRY_VIEW_PLAYING, "playing"),
2697 			ENUM_ENTRY (RB_ENTRY_VIEW_PAUSED, "paused"),
2698 			{ 0, 0, 0 }
2699 		};
2700 
2701 		etype = g_enum_register_static ("RBEntryViewState", values);
2702 	}
2703 
2704 	return etype;
2705 }
2706