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 #include "config.h"
30
31 #include <string.h>
32 #include <stdlib.h>
33
34 #include <glib/gi18n.h>
35 #include <gdk/gdkkeysyms.h>
36 #include <gtk/gtk.h>
37
38 #include "rb-property-view.h"
39 #include "rb-dialog.h"
40 #include "rb-debug.h"
41 #include "rhythmdb.h"
42 #include "rhythmdb-property-model.h"
43 #include "rb-util.h"
44
45 static void rb_property_view_class_init (RBPropertyViewClass *klass);
46 static void rb_property_view_init (RBPropertyView *view);
47 static void rb_property_view_dispose (GObject *object);
48 static void rb_property_view_finalize (GObject *object);
49 static void rb_property_view_set_property (GObject *object,
50 guint prop_id,
51 const GValue *value,
52 GParamSpec *pspec);
53 static void rb_property_view_get_property (GObject *object,
54 guint prop_id,
55 GValue *value,
56 GParamSpec *pspec);
57 static void rb_property_view_constructed (GObject *object);
58 static void rb_property_view_row_activated_cb (GtkTreeView *treeview,
59 GtkTreePath *path,
60 GtkTreeViewColumn *column,
61 RBPropertyView *view);
62 static void rb_property_view_selection_changed_cb (GtkTreeSelection *selection,
63 RBPropertyView *view);
64 static void rb_property_view_pre_row_deleted_cb (RhythmDBPropertyModel *model,
65 RBPropertyView *view);
66 static void rb_property_view_post_row_deleted_cb (GtkTreeModel *model,
67 GtkTreePath *path,
68 RBPropertyView *view);
69 static gboolean rb_property_view_popup_menu_cb (GtkTreeView *treeview,
70 RBPropertyView *view);
71 static gboolean rb_property_view_button_press_cb (GtkTreeView *tree,
72 GdkEventButton *event,
impl_finalize(GObject * object)73 RBPropertyView *view);
74
75 struct RBPropertyViewPrivate
76 {
77 RhythmDB *db;
78
79 RhythmDBPropType propid;
80
81 RhythmDBPropertyModel *prop_model;
82
impl_dispose(GObject * object)83 char *title;
84
85 GtkWidget *treeview;
86 GtkTreeViewColumn *column;
87 GtkTreeSelection *selection;
88
89 gboolean draggable;
90 gboolean handling_row_deletion;
91 guint reset_selection_id;
92 };
93
94 #define RB_PROPERTY_VIEW_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_PROPERTY_VIEW, RBPropertyViewPrivate))
95
96 /**
97 * SECTION:rb-property-view
98 * @short_description: a #GtkTreeView backed by a #RhythmDBPropertyModel
99 *
100 * A simple #GtkTreeView that displays the contents of a #RhythmDBPropertyModel.
notify_cb(GObject * object,GParamSpec * pspec,RBObjectPropertyEditor * editor)101 * The first row in the tree view displays the total number of properties and entries,
102 * in the form "All 473 artists (6241)". Each subsequent row in the tree view
103 * displays a property value and the number of entries from the #RhythmDBQueryModel
104 * with that value.
105 *
106 * The property view itself creates a single column, but additional columns can be
107 * added.
108 */
109
110 enum
111 {
112 PROPERTY_SELECTED,
113 PROPERTIES_SELECTED,
114 PROPERTY_ACTIVATED,
115 SELECTION_RESET,
116 SHOW_POPUP,
117 LAST_SIGNAL
118 };
create_boolean_editor(RBObjectPropertyEditor * editor,const char * property,GParamSpec * pspec)119
120 enum
121 {
122 PROP_0,
123 PROP_DB,
124 PROP_PROP,
125 PROP_TITLE,
126 PROP_MODEL,
127 PROP_DRAGGABLE,
128 };
129
130 static guint rb_property_view_signals[LAST_SIGNAL] = { 0 };
131
132 G_DEFINE_TYPE (RBPropertyView, rb_property_view, GTK_TYPE_SCROLLED_WINDOW)
create_enum_editor(RBObjectPropertyEditor * editor,const char * property,GParamSpec * pspec)133
134 static void
135 rb_property_view_class_init (RBPropertyViewClass *klass)
136 {
137 GObjectClass *object_class = G_OBJECT_CLASS (klass);
138
139 object_class->dispose = rb_property_view_dispose;
140 object_class->finalize = rb_property_view_finalize;
141 object_class->constructed = rb_property_view_constructed;
142
143 object_class->set_property = rb_property_view_set_property;
144 object_class->get_property = rb_property_view_get_property;
145
146 /**
147 * RBPropertyView:db:
148 *
149 * #RhythmDB instance
150 */
151 g_object_class_install_property (object_class,
152 PROP_DB,
153 g_param_spec_object ("db",
154 "RhythmDB",
155 "RhythmDB database",
156 RHYTHMDB_TYPE,
157 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
158
159 /**
160 * RBPropertyView:prop:
161 *
162 * The property that is displayed in this view
163 */
164 g_object_class_install_property (object_class,
165 PROP_PROP,
166 g_param_spec_enum ("prop",
167 "PropertyId",
168 "RhythmDBPropType",
169 RHYTHMDB_TYPE_PROP_TYPE,
170 RHYTHMDB_PROP_TYPE,
171 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
172
173 /**
174 * RBPropertyView:title:
175 *
176 * The title displayed in the header of the property view
177 */
178 g_object_class_install_property (object_class,
179 PROP_TITLE,
180 g_param_spec_string ("title",
181 "title",
182 "title",
183 "",
184 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
185 /**
186 * RBPropertyView:property-model:
187 *
188 * The #RhythmDBPropertyModel backing the view.
189 */
190 g_object_class_install_property (object_class,
191 PROP_MODEL,
192 g_param_spec_object ("property-model",
193 "property model",
194 "RhythmDBPropertyModel",
195 RHYTHMDB_TYPE_PROPERTY_MODEL,
196 G_PARAM_READWRITE));
197 /**
198 * RBPropertyView:draggable:
199 *
200 * Whether the property view acts as a data source for drag and drop operations.
201 */
202 g_object_class_install_property (object_class,
203 PROP_DRAGGABLE,
204 g_param_spec_boolean ("draggable",
205 "draggable",
206 "is a drag source",
207 TRUE,
208 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
209
210 /**
211 * RBPropertyView::property-activated:
212 * @view: the #RBPropertyView
213 * @name: the property value that was activated
214 *
215 * Emitted when a row in a property view is activated by double clicking.
216 */
217 rb_property_view_signals[PROPERTY_ACTIVATED] =
218 g_signal_new ("property-activated",
219 G_OBJECT_CLASS_TYPE (object_class),
220 G_SIGNAL_RUN_LAST,
221 G_STRUCT_OFFSET (RBPropertyViewClass, property_activated),
222 NULL, NULL,
223 NULL,
224 G_TYPE_NONE,
225 1,
226 G_TYPE_STRING);
227
228 /**
229 * RBPropertyView::property-selected:
230 * @view: the #RBPropertyView
231 * @name: the property value that has been selected
232 *
233 * Emitted when an individual property value becomes selected. This is only
234 * emitted for single-selection property views. For multiple-selection views,
235 * use the properties-selected signal.
236 */
237 rb_property_view_signals[PROPERTY_SELECTED] =
238 g_signal_new ("property-selected",
239 G_OBJECT_CLASS_TYPE (object_class),
240 G_SIGNAL_RUN_LAST,
241 G_STRUCT_OFFSET (RBPropertyViewClass, property_selected),
242 NULL, NULL,
243 NULL,
244 G_TYPE_NONE,
245 1,
246 G_TYPE_STRING);
247
248 /**
249 * RBPropertyView::properties-selected:
250 * @view: the #RBPropertyView
251 * @properties: (element-type utf8): a list containing the selected property values
252 *
253 * Emitted when the set of selected property values changes. This is only
254 * emitted for multiple selection property views. For single-selection views,
255 * use the property-selected signal.
256 */
257 rb_property_view_signals[PROPERTIES_SELECTED] =
258 g_signal_new ("properties-selected",
259 G_OBJECT_CLASS_TYPE (object_class),
260 G_SIGNAL_RUN_LAST,
261 G_STRUCT_OFFSET (RBPropertyViewClass, properties_selected),
262 NULL, NULL,
263 NULL,
264 G_TYPE_NONE,
265 1,
266 G_TYPE_POINTER);
267
268 /**
269 * RBPropertyView::property-selection-reset:
270 * @view: the #RBPropertyView
271 *
272 * Emitted when the selection is reset. At this point, no property values
273 * are selected.
274 */
275 rb_property_view_signals[SELECTION_RESET] =
276 g_signal_new ("property-selection-reset",
277 G_OBJECT_CLASS_TYPE (object_class),
278 G_SIGNAL_RUN_LAST,
279 G_STRUCT_OFFSET (RBPropertyViewClass, selection_reset),
280 NULL, NULL,
281 NULL,
282 G_TYPE_NONE,
283 0);
284
285 /**
286 * RBPropertyView::show-popup:
287 * @view: the #RBPropertyView
288 *
289 * Emitted when a popup menu should be displayed for the property view.
290 * The source containing the property view should connect a handler to
291 * this signal that * displays an appropriate popup.
292 */
293 rb_property_view_signals[SHOW_POPUP] =
294 g_signal_new ("show_popup",
295 G_OBJECT_CLASS_TYPE (object_class),
296 G_SIGNAL_RUN_LAST,
297 G_STRUCT_OFFSET (RBPropertyViewClass, show_popup),
298 NULL, NULL,
299 NULL,
300 G_TYPE_NONE,
301 0);
302
303 g_type_class_add_private (klass, sizeof (RBPropertyViewPrivate));
304 }
305
306 static void
307 rb_property_view_init (RBPropertyView *view)
308 {
309 view->priv = RB_PROPERTY_VIEW_GET_PRIVATE (view);
310 }
311
312 static void
313 rb_property_view_set_model_internal (RBPropertyView *view,
314 RhythmDBPropertyModel *model)
315 {
impl_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)316 if (view->priv->prop_model != NULL) {
317 g_signal_handlers_disconnect_by_func (view->priv->prop_model,
318 G_CALLBACK (rb_property_view_pre_row_deleted_cb),
319 view);
320 g_signal_handlers_disconnect_by_func (view->priv->prop_model,
321 G_CALLBACK (rb_property_view_post_row_deleted_cb),
322 view);
323 g_object_unref (view->priv->prop_model);
324 }
325
326 view->priv->prop_model = model;
327
328 if (view->priv->prop_model != NULL) {
329 GtkTreeIter iter;
330
331 g_object_ref (view->priv->prop_model);
impl_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)332
333 gtk_tree_view_set_model (GTK_TREE_VIEW (view->priv->treeview),
334 GTK_TREE_MODEL (view->priv->prop_model));
335
336 g_signal_connect_object (view->priv->prop_model,
337 "pre-row-deletion",
338 G_CALLBACK (rb_property_view_pre_row_deleted_cb),
339 view,
340 0);
341 g_signal_connect_object (view->priv->prop_model,
342 "row_deleted",
343 G_CALLBACK (rb_property_view_post_row_deleted_cb),
344 view,
345 G_CONNECT_AFTER);
346
347 g_signal_handlers_block_by_func (view->priv->selection,
rb_object_property_editor_init(RBObjectPropertyEditor * editor)348 G_CALLBACK (rb_property_view_selection_changed_cb),
349 view);
350
351 gtk_tree_selection_unselect_all (view->priv->selection);
352
353 if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (view->priv->prop_model), &iter))
354 gtk_tree_selection_select_iter (view->priv->selection, &iter);
355
356 g_signal_handlers_unblock_by_func (view->priv->selection,
rb_object_property_editor_class_init(RBObjectPropertyEditorClass * klass)357 G_CALLBACK (rb_property_view_selection_changed_cb),
358 view);
359 }
360 }
361
362 static void
363 rb_property_view_dispose (GObject *object)
364 {
365 RBPropertyView *view;
366
367 g_return_if_fail (object != NULL);
368 g_return_if_fail (RB_IS_PROPERTY_VIEW (object));
369
370 view = RB_PROPERTY_VIEW (object);
371
372 if (view->priv->reset_selection_id != 0) {
373 g_source_remove (view->priv->reset_selection_id);
374 view->priv->reset_selection_id = 0;
375 }
376
377 rb_property_view_set_model_internal (view, NULL);
378
379 G_OBJECT_CLASS (rb_property_view_parent_class)->dispose (object);
380 }
381
382 static void
383 rb_property_view_finalize (GObject *object)
384 {
385 RBPropertyView *view;
386
387 g_return_if_fail (object != NULL);
388 g_return_if_fail (RB_IS_PROPERTY_VIEW (object));
389
390 view = RB_PROPERTY_VIEW (object);
391
392 g_free (view->priv->title);
393
394 G_OBJECT_CLASS (rb_property_view_parent_class)->finalize (object);
395 }
396
397 static void
398 rb_property_view_set_property (GObject *object,
399 guint prop_id,
400 const GValue *value,
401 GParamSpec *pspec)
402 {
403 RBPropertyView *view = RB_PROPERTY_VIEW (object);
404
405 switch (prop_id) {
406 case PROP_DB:
407 view->priv->db = g_value_get_object (value);
408 break;
409 case PROP_PROP:
410 view->priv->propid = g_value_get_enum (value);
411 break;
412 case PROP_TITLE:
413 view->priv->title = g_value_dup_string (value);
414 break;
415 case PROP_MODEL:
416 rb_property_view_set_model_internal (view, g_value_get_object (value));
417 break;
418 case PROP_DRAGGABLE:
419 view->priv->draggable = g_value_get_boolean (value);
420 break;
421 default:
422 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
423 break;
424 }
rb_object_property_editor_new(GObject * object,char ** properties)425 }
426
427 static void
428 rb_property_view_get_property (GObject *object,
429 guint prop_id,
430 GValue *value,
431 GParamSpec *pspec)
432 {
433 RBPropertyView *view = RB_PROPERTY_VIEW (object);
434
435 switch (prop_id) {
436 case PROP_DB:
437 g_value_set_object (value, view->priv->db);
438 break;
439 case PROP_PROP:
440 g_value_set_enum (value, view->priv->propid);
441 break;
442 case PROP_TITLE:
443 g_value_set_string (value, view->priv->title);
444 break;
445 case PROP_MODEL:
446 g_value_set_object (value, view->priv->prop_model);
447 break;
448 case PROP_DRAGGABLE:
449 g_value_set_boolean (value, view->priv->draggable);
450 break;
451 default:
452 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
453 break;
454 }
455 }
456
457 /**
458 * rb_property_view_new:
459 * @db: #RhythmDB instance
460 * @propid: property ID to be displayed in the property view
461 * @title: title of the property view
462 *
463 * Creates a new #RBPropertyView displaying the specified property.
464 *
465 * Return value: new property view instance
466 */
467 RBPropertyView *
468 rb_property_view_new (RhythmDB *db,
469 guint propid,
470 const char *title)
471 {
472 RBPropertyView *view;
473
474 view = RB_PROPERTY_VIEW (g_object_new (RB_TYPE_PROPERTY_VIEW,
475 "hadjustment", NULL,
476 "vadjustment", NULL,
477 "hscrollbar_policy", GTK_POLICY_AUTOMATIC,
478 "vscrollbar_policy", GTK_POLICY_AUTOMATIC,
479 "hexpand", TRUE,
480 "vexpand", TRUE,
481 "shadow_type", GTK_SHADOW_NONE,
482 "db", db,
483 "prop", propid,
484 "title", title,
485 "draggable", TRUE,
486 NULL));
487
488 g_return_val_if_fail (view->priv != NULL, NULL);
489
490 return view;
491 }
492
493 /**
494 * rb_property_view_set_selection_mode:
495 * @view: a #RBPropertyView
496 * @mode: the new #GtkSelectionMode for the property view
497 *
498 * Sets the selection mode (single or multiple) for the property view>
499 * The default selection mode is single.
500 */
501 void
502 rb_property_view_set_selection_mode (RBPropertyView *view,
503 GtkSelectionMode mode)
504 {
505 g_return_if_fail (RB_IS_PROPERTY_VIEW (view));
506 g_return_if_fail (mode == GTK_SELECTION_SINGLE || mode == GTK_SELECTION_MULTIPLE);
507
508 gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW (view->priv->treeview)),
509 mode);
510 }
511
512 /**
513 * rb_property_view_reset:
514 * @view: a #RBPropertyView
515 *
516 * Clears the selection in the property view.
517 */
518 void
519 rb_property_view_reset (RBPropertyView *view)
520 {
521 RhythmDBPropertyModel *model;
522
523 g_return_if_fail (RB_IS_PROPERTY_VIEW (view));
524
525 model = rhythmdb_property_model_new (view->priv->db, view->priv->propid);
526
527 rb_property_view_set_model_internal (view, model);
528 g_object_unref (model);
529 }
530
531 /**
532 * rb_property_view_get_model:
533 * @view: a #RBPropertyView
534 *
535 * Returns the #RhythmDBPropertyModel backing the view; no reference is taken
536 *
537 * Return value: (transfer none): property model
538 */
539 RhythmDBPropertyModel *
540 rb_property_view_get_model (RBPropertyView *view)
541 {
542 RhythmDBPropertyModel *model;
543
544 g_return_val_if_fail (RB_IS_PROPERTY_VIEW (view), NULL);
545
546 model = view->priv->prop_model;
547
548 return model;
549 }
550
551 /**
552 * rb_property_view_set_model:
553 * @view: a #RBPropertyView
554 * @model: the new #RhythmDBPropertyModel for the property view
555 *
556 * Replaces the model backing the property view.
557 */
558 void
559 rb_property_view_set_model (RBPropertyView *view,
560 RhythmDBPropertyModel *model)
561 {
562 g_return_if_fail (RB_IS_PROPERTY_VIEW (view));
563
564 rb_property_view_set_model_internal (view, model);
565 }
566
567 static void
568 rb_property_view_pre_row_deleted_cb (RhythmDBPropertyModel *model,
569 RBPropertyView *view)
570 {
571 view->priv->handling_row_deletion = TRUE;
572 rb_debug ("pre row deleted");
573 }
574
575 static gboolean
576 reset_selection_cb (RBPropertyView *view)
577 {
578 GtkTreeIter first_iter;
579 if ((gtk_tree_selection_count_selected_rows (view->priv->selection) == 0) &&
580 gtk_tree_model_get_iter_first (GTK_TREE_MODEL (view->priv->prop_model), &first_iter)) {
581 rb_debug ("no rows selected, signalling reset");
582 g_signal_handlers_block_by_func (G_OBJECT (view->priv->selection),
583 G_CALLBACK (rb_property_view_selection_changed_cb),
584 view);
585 gtk_tree_selection_select_iter (view->priv->selection, &first_iter);
586 g_signal_emit (G_OBJECT (view), rb_property_view_signals[SELECTION_RESET], 0);
587 g_signal_handlers_unblock_by_func (G_OBJECT (view->priv->selection),
588 G_CALLBACK (rb_property_view_selection_changed_cb),
589 view);
590 }
591
592 view->priv->reset_selection_id = 0;
593 return FALSE;
594 }
595
596 static void
597 rb_property_view_post_row_deleted_cb (GtkTreeModel *model,
598 GtkTreePath *path,
599 RBPropertyView *view)
600 {
601 view->priv->handling_row_deletion = FALSE;
602 rb_debug ("post row deleted");
603 if (gtk_tree_selection_count_selected_rows (view->priv->selection) == 0) {
604 if (view->priv->reset_selection_id == 0) {
605 /* we could be in the middle of renaming a property.
606 * we've seen it being removed, but we haven't seen the
607 * new name being added yet, which means the property model
608 * is out of sync with the query model, so we can't
609 * reset the selection here. wait until we've finished
610 * processing the change.
611 */
612 view->priv->reset_selection_id = g_idle_add ((GSourceFunc) reset_selection_cb, view);
613 }
614 }
615 }
616
617 /**
618 * rb_property_view_get_num_properties:
619 * @view: a #RBPropertyView
620 *
621 * Returns the number of property values present in the view.
622 *
623 * Return value: number of properties
624 */
625 guint
626 rb_property_view_get_num_properties (RBPropertyView *view)
627 {
628 g_return_val_if_fail (RB_IS_PROPERTY_VIEW (view), 0);
629
630 return gtk_tree_model_iter_n_children (GTK_TREE_MODEL (view->priv->prop_model),
631 NULL)-1;
632 }
633
634 static void
635 rb_property_view_cell_data_func (GtkTreeViewColumn *column,
636 GtkCellRenderer *renderer,
637 GtkTreeModel *tree_model,
638 GtkTreeIter *iter,
639 RBPropertyView *view)
640 {
641 char *title;
642 char *str;
643 guint number;
644 gboolean is_all;
645
646 gtk_tree_model_get (GTK_TREE_MODEL (tree_model), iter,
647 RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &title,
648 RHYTHMDB_PROPERTY_MODEL_COLUMN_PRIORITY, &is_all,
649 RHYTHMDB_PROPERTY_MODEL_COLUMN_NUMBER, &number, -1);
650
651 if (is_all) {
652 int nodes;
653 const char *fmt;
654
655 nodes = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (tree_model), NULL);
656 /* Subtract one for the All node */
657 nodes--;
658
659 switch (view->priv->propid) {
660 case RHYTHMDB_PROP_ARTIST:
661 fmt = ngettext ("%d artist (%d)", "All %d artists (%d)", nodes);
662 break;
663 case RHYTHMDB_PROP_ALBUM:
664 fmt = ngettext ("%d album (%d)", "All %d albums (%d)", nodes);
665 break;
666 case RHYTHMDB_PROP_GENRE:
667 fmt = ngettext ("%d genre (%d)", "All %d genres (%d)", nodes);
668 break;
669 default:
670 fmt = ngettext ("%d (%d)", "All %d (%d)", nodes);
671 break;
672 }
673
674 str = g_strdup_printf (fmt, nodes, number);
675 } else {
676 str = g_strdup_printf (_("%s (%d)"), title, number);
677 }
678
679 g_object_set (G_OBJECT (renderer), "text", str,
680 "weight", G_UNLIKELY (is_all) ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
681 "ellipsize", PANGO_ELLIPSIZE_MIDDLE,
682 NULL);
683 g_free (str);
684 g_free (title);
685 }
686
687 static void
688 rb_property_view_constructed (GObject *object)
689 {
690 GtkCellRenderer *renderer;
691 RBPropertyView *view;
692
693 RB_CHAIN_GOBJECT_METHOD (rb_property_view_parent_class, constructed, object);
694
695 view = RB_PROPERTY_VIEW (object);
696
697 view->priv->treeview = GTK_WIDGET (gtk_tree_view_new_with_model (GTK_TREE_MODEL (view->priv->prop_model)));
698
699
700 g_signal_connect_object (G_OBJECT (view->priv->treeview),
701 "row_activated",
702 G_CALLBACK (rb_property_view_row_activated_cb),
703 view,
704 0);
705
706 view->priv->selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view->priv->treeview));
707 g_signal_connect_object (G_OBJECT (view->priv->selection),
708 "changed",
709 G_CALLBACK (rb_property_view_selection_changed_cb),
710 view,
711 0);
712 g_signal_connect_object (G_OBJECT (view->priv->treeview),
713 "popup_menu",
714 G_CALLBACK (rb_property_view_popup_menu_cb),
715 view,
716 0);
717
718 g_signal_connect_object (G_OBJECT (view->priv->treeview),
719 "button_press_event",
720 G_CALLBACK (rb_property_view_button_press_cb),
721 view,
722 0);
723
724 gtk_container_add (GTK_CONTAINER (view), view->priv->treeview);
725
726 rb_property_view_set_model_internal (view, rhythmdb_property_model_new (view->priv->db, view->priv->propid));
727 if (view->priv->draggable)
728 rhythmdb_property_model_enable_drag (view->priv->prop_model,
729 GTK_TREE_VIEW (view->priv->treeview));
730
731 gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (view->priv->treeview), TRUE);
732 gtk_tree_selection_set_mode (view->priv->selection, GTK_SELECTION_SINGLE);
733
734 view->priv->column = gtk_tree_view_column_new ();
735 renderer = gtk_cell_renderer_text_new ();
736 gtk_tree_view_column_pack_start (view->priv->column, renderer, TRUE);
737 gtk_tree_view_column_set_cell_data_func (view->priv->column, renderer,
738 (GtkTreeCellDataFunc) rb_property_view_cell_data_func,
739 view, NULL);
740 gtk_tree_view_column_set_title (view->priv->column, view->priv->title);
741 gtk_tree_view_column_set_sizing (view->priv->column, GTK_TREE_VIEW_COLUMN_FIXED);
742 gtk_tree_view_append_column (GTK_TREE_VIEW (view->priv->treeview),
743 view->priv->column);
744 g_object_set (renderer, "single-paragraph-mode", TRUE, NULL);
745 }
746
747 static void
748 rb_property_view_row_activated_cb (GtkTreeView *treeview,
749 GtkTreePath *path,
750 GtkTreeViewColumn *column,
751 RBPropertyView *view)
752 {
753 GtkTreeIter iter;
754 char *val;
755 gboolean is_all;
756
757 rb_debug ("row activated");
758 g_return_if_fail (gtk_tree_model_get_iter (GTK_TREE_MODEL (view->priv->prop_model),
759 &iter, path));
760
761 gtk_tree_model_get (GTK_TREE_MODEL (view->priv->prop_model), &iter,
762 RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &val,
763 RHYTHMDB_PROPERTY_MODEL_COLUMN_PRIORITY, &is_all, -1);
764
765 rb_debug ("emitting property activated");
766 g_signal_emit (G_OBJECT (view), rb_property_view_signals[PROPERTY_ACTIVATED], 0,
767 is_all ? NULL : val);
768
769 g_free (val);
770 }
771
772 /**
773 * rb_property_view_set_selection:
774 * @view: a #RBPropertyView
775 * @vals: (element-type utf8): the values to be selected
776 *
777 * Replaces the selection in the property view. All values in the list
778 * that are present in the view will be selected, and the view will be
779 * scrolled to show the last value selected.
780 */
781 void
782 rb_property_view_set_selection (RBPropertyView *view,
783 const GList *vals)
784 {
785 g_return_if_fail (RB_IS_PROPERTY_VIEW (view));
786
787 view->priv->handling_row_deletion = TRUE;
788
789 gtk_tree_selection_unselect_all (view->priv->selection);
790
791 for (; vals ; vals = vals->next) {
792 GtkTreeIter iter;
793
794 if (rhythmdb_property_model_iter_from_string (view->priv->prop_model, vals->data, &iter)) {
795 GtkTreePath *path;
796
797 gtk_tree_selection_select_iter (view->priv->selection, &iter);
798 path = gtk_tree_model_get_path (GTK_TREE_MODEL (view->priv->prop_model), &iter);
799 if (path != NULL) {
800 gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (view->priv->treeview),
801 path, NULL, TRUE,
802 0.5, 0.0);
803 gtk_tree_path_free (path);
804 }
805
806 }
807 }
808
809 view->priv->handling_row_deletion = FALSE;
810 rb_property_view_selection_changed_cb (view->priv->selection, view);
811 }
812
813 /**
814 * rb_property_view_get_selection:
815 * @view: a #RBPropertyView
816 *
817 * Returns a #GList containing the selected property values. The list must
818 * be freed by the caller.
819 *
820 * Return value: (element-type utf8) (transfer full): list of selected values
821 */
822 GList *
823 rb_property_view_get_selection (RBPropertyView *view)
824 {
825 gboolean is_all = TRUE;
826 GtkTreeModel *model;
827 GtkTreeIter iter;
828 GList *selected_rows, *tem;
829 GList *selected_properties = NULL;
830
831 selected_rows = gtk_tree_selection_get_selected_rows (view->priv->selection, &model);
832 for (tem = selected_rows; tem; tem = tem->next) {
833 char *selected_prop = NULL;
834
835 g_assert (gtk_tree_model_get_iter (model, &iter, tem->data));
836 gtk_tree_model_get (model, &iter,
837 RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &selected_prop,
838 RHYTHMDB_PROPERTY_MODEL_COLUMN_PRIORITY, &is_all, -1);
839 if (is_all) {
840 rb_list_deep_free (selected_properties);
841 selected_properties = NULL;
842 break;
843 }
844 selected_properties = g_list_prepend (selected_properties,
845 selected_prop);
846 }
847
848 g_list_foreach (selected_rows, (GFunc) gtk_tree_path_free, NULL);
849 g_list_free (selected_rows);
850
851 return selected_properties;
852 }
853
854 static void
855 select_all (RBPropertyView *view, GtkTreeSelection *selection, GtkTreeModel *model)
856 {
857 GtkTreeIter iter;
858
859 g_signal_handlers_block_by_func (selection,
860 G_CALLBACK (rb_property_view_selection_changed_cb),
861 view);
862 gtk_tree_selection_unselect_all (selection);
863 if (gtk_tree_model_get_iter_first (model, &iter))
864 gtk_tree_selection_select_iter (selection, &iter);
865 g_signal_handlers_unblock_by_func (selection,
866 G_CALLBACK (rb_property_view_selection_changed_cb),
867 view);
868 }
869
870 static void
871 rb_property_view_selection_changed_cb (GtkTreeSelection *selection,
872 RBPropertyView *view)
873 {
874 char *selected_prop = NULL;
875 gboolean is_all = TRUE;
876 GtkTreeModel *model;
877 GtkTreeIter iter;
878
879 if (view->priv->handling_row_deletion)
880 return;
881
882 rb_debug ("selection changed");
883 if (gtk_tree_selection_get_mode (selection) == GTK_SELECTION_MULTIPLE) {
884 GList *selected_rows, *tem;
885 GList *selected_properties = NULL;
886
887 selected_rows = gtk_tree_selection_get_selected_rows (view->priv->selection, &model);
888 for (tem = selected_rows; tem; tem = tem->next) {
889 g_assert (gtk_tree_model_get_iter (model, &iter, tem->data));
890 gtk_tree_model_get (model, &iter,
891 RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &selected_prop,
892 RHYTHMDB_PROPERTY_MODEL_COLUMN_PRIORITY, &is_all, -1);
893 if (is_all) {
894 g_list_free (selected_properties);
895 selected_properties = NULL;
896 break;
897 }
898 selected_properties = g_list_prepend (selected_properties,
899 g_strdup (selected_prop));
900 }
901
902 g_list_foreach (selected_rows, (GFunc) gtk_tree_path_free, NULL);
903 g_list_free (selected_rows);
904
905 if (is_all)
906 select_all (view, selection, model);
907
908 g_signal_emit (view, rb_property_view_signals[PROPERTIES_SELECTED], 0,
909 selected_properties);
910 rb_list_deep_free (selected_properties);
911 } else {
912 if (gtk_tree_selection_get_selected (view->priv->selection, &model, &iter)) {
913 gtk_tree_model_get (model, &iter,
914 RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &selected_prop,
915 RHYTHMDB_PROPERTY_MODEL_COLUMN_PRIORITY, &is_all, -1);
916 g_signal_emit (view, rb_property_view_signals[PROPERTY_SELECTED], 0,
917 is_all ? NULL : selected_prop);
918 } else {
919 select_all (view, selection, model);
920 g_signal_emit (view, rb_property_view_signals[PROPERTY_SELECTED], 0, NULL);
921 }
922 }
923
924 g_free (selected_prop);
925 }
926
927 static gboolean
928 rb_property_view_popup_menu_cb (GtkTreeView *treeview,
929 RBPropertyView *view)
930 {
931 g_signal_emit (G_OBJECT (view), rb_property_view_signals[SHOW_POPUP], 0);
932 return TRUE;
933 }
934
935 /**
936 * rb_property_view_append_column_custom:
937 * @view: a #RBPropertyView
938 * @column: a #GtkTreeViewColumn to append to the view
939 *
940 * Appends a custom created column to the view.
941 */
942 void
943 rb_property_view_append_column_custom (RBPropertyView *view,
944 GtkTreeViewColumn *column)
945 {
946 g_return_if_fail (RB_IS_PROPERTY_VIEW (view));
947
948 gtk_tree_view_append_column (GTK_TREE_VIEW (view->priv->treeview), column);
949 }
950
951 /**
952 * rb_property_view_set_column_visible:
953 * @view: a #RBPropertyView
954 * @visible: whether the property column should be visible
955 *
956 * Sets the visibility of the property column.
957 */
958 void
959 rb_property_view_set_column_visible (RBPropertyView *view, gboolean visible)
960 {
961 g_return_if_fail (RB_IS_PROPERTY_VIEW (view));
962 gtk_tree_view_column_set_visible (view->priv->column, visible);
963 }
964
965 static gboolean
966 rb_property_view_button_press_cb (GtkTreeView *tree,
967 GdkEventButton *event,
968 RBPropertyView *view)
969 {
970
971 if (event->button == 3) {
972 GtkTreeSelection *selection;
973 GtkTreePath *path;
974
975 selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view->priv->treeview));
976
977 gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (view->priv->treeview), event->x, event->y, &path, NULL, NULL, NULL);
978 if (path == NULL) {
979 gtk_tree_selection_unselect_all (selection);
980 } else {
981 GtkTreeModel *model;
982 GtkTreeIter iter;
983 char *val;
984 GList *lst = NULL;
985
986 model = gtk_tree_view_get_model (GTK_TREE_VIEW (view->priv->treeview));
987 if (gtk_tree_model_get_iter (model, &iter, path)) {
988 gtk_tree_model_get (model, &iter,
989 RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &val, -1);
990 lst = g_list_prepend (lst, (gpointer) val);
991 rb_property_view_set_selection (view, lst);
992 g_free (val);
993 }
994 }
995 g_signal_emit (G_OBJECT (view), rb_property_view_signals[SHOW_POPUP], 0);
996 return TRUE;
997 }
998
999 return FALSE;
1000 }
1001
1002 /**
1003 * rb_property_view_set_search_func:
1004 * @view: a #RBPropertyView
1005 * @func: tree view search function to use for this view
1006 * @func_data: data to pass to the search function
1007 * @notify: function to call to dispose of the data
1008 *
1009 * Sets the compare function for the interactive search capabilities.
1010 * The function must return FALSE when the search key string matches
1011 * the row it is passed.
1012 */
1013 void
1014 rb_property_view_set_search_func (RBPropertyView *view,
1015 GtkTreeViewSearchEqualFunc func,
1016 gpointer func_data,
1017 GDestroyNotify notify)
1018 {
1019 g_return_if_fail (RB_IS_PROPERTY_VIEW (view));
1020
1021 gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW (view->priv->treeview),
1022 func, func_data,
1023 notify);
1024 }
1025
1026