No issues found
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2002, 2003 Jorn Baayen <jorn@nl.linux.org>
4 * Copyright (C) 2003 Colin Walters <walters@gnome.org>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * The Rhythmbox authors hereby grant permission for non-GPL compatible
12 * GStreamer plugins to be used and distributed together with GStreamer
13 * and Rhythmbox. This permission is above and beyond the permissions granted
14 * by the GPL license by which Rhythmbox is covered. If you modify this code
15 * you may extend this exception to your version of the code, but you are not
16 * obligated to do so. If you do not wish to do so, delete this exception
17 * statement from your version.
18 *
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with this program; if not, write to the Free Software
26 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
27 *
28 */
29
30 #include <config.h>
31
32 #include <math.h>
33 #include <string.h>
34
35 #include <glib/gi18n.h>
36 #include <gtk/gtk.h>
37
38 #include "rb-stock-icons.h"
39 #include "rb-header.h"
40 #include "rb-debug.h"
41 #include "rb-shell-player.h"
42 #include "rb-util.h"
43 #include "rhythmdb.h"
44 #include "rb-player.h"
45 #include "rb-text-helpers.h"
46 #include "rb-fading-image.h"
47 #include "rb-file-helpers.h"
48 #include "rb-ext-db.h"
49
50 /**
51 * SECTION:rb-header
52 * @short_description: playback area widgetry
53 *
54 * The RBHeader widget displays information about the current playing track
55 * (title, album, artist), the elapsed or remaining playback time, and a
56 * position slider indicating the playback position. It translates slider
57 * move and drag events into seek requests for the player backend.
58 *
59 * For shoutcast-style streams, the title/artist/album display is supplemented
60 * by metadata extracted from the stream. See #RBStreamingSource for more information
61 * on how the metadata is reported.
62 */
63
64 static void rb_header_class_init (RBHeaderClass *klass);
65 static void rb_header_init (RBHeader *header);
66 static void rb_header_dispose (GObject *object);
67 static void rb_header_finalize (GObject *object);
68 static void rb_header_set_property (GObject *object,
69 guint prop_id,
70 const GValue *value,
71 GParamSpec *pspec);
72 static void rb_header_get_property (GObject *object,
73 guint prop_id,
74 GValue *value,
75 GParamSpec *pspec);
76 static GtkSizeRequestMode rb_header_get_request_mode (GtkWidget *widget);
77 static void rb_header_get_preferred_width (GtkWidget *widget,
78 int *minimum_size,
79 int *natural_size);
80 static void rb_header_size_allocate (GtkWidget *widget, GtkAllocation *allocation);
81 static void rb_header_update_elapsed (RBHeader *header);
82 static void apply_slider_position (RBHeader *header);
83 static gboolean slider_press_callback (GtkWidget *widget, GdkEventButton *event, RBHeader *header);
84 static gboolean slider_moved_callback (GtkWidget *widget, GdkEventMotion *event, RBHeader *header);
85 static gboolean slider_release_callback (GtkWidget *widget, GdkEventButton *event, RBHeader *header);
86 static void slider_changed_callback (GtkWidget *widget, RBHeader *header);
87 static gboolean slider_scroll_callback (GtkWidget *widget, GdkEventScroll *event, RBHeader *header);
88 static gboolean slider_focus_out_callback (GtkWidget *widget, GdkEvent *event, RBHeader *header);
89 static void time_button_clicked_cb (GtkWidget *button, RBHeader *header);
90
91 static void rb_header_elapsed_changed_cb (RBShellPlayer *player, gint64 elapsed, RBHeader *header);
92 static void rb_header_extra_metadata_cb (RhythmDB *db, RhythmDBEntry *entry, const char *property_name, const GValue *metadata, RBHeader *header);
93 static void rb_header_sync (RBHeader *header);
94 static void rb_header_sync_time (RBHeader *header);
95
96 static void uri_dropped_cb (RBFadingImage *image, const char *uri, RBHeader *header);
97 static void pixbuf_dropped_cb (RBFadingImage *image, GdkPixbuf *pixbuf, RBHeader *header);
98 static void image_button_press_cb (GtkWidget *widget, GdkEvent *event, RBHeader *header);
99 static void art_added_cb (RBExtDB *db, RBExtDBKey *key, const char *filename, GValue *data, RBHeader *header);
100
101 struct RBHeaderPrivate
102 {
103 RhythmDB *db;
104 RhythmDBEntry *entry;
105 RBExtDB *art_store;
106
107 RBShellPlayer *shell_player;
108
109 GtkWidget *songbox;
110 GtkWidget *song;
111 GtkWidget *details;
112 GtkWidget *image;
113
114 GtkWidget *scale;
115 GtkAdjustment *adjustment;
116 gboolean slider_dragging;
117 gboolean slider_locked;
118 gboolean slider_drag_moved;
119 guint slider_moved_timeout;
120 long latest_set_time;
121
122 GtkWidget *timebutton;
123 GtkWidget *timelabel;
124
125 gint64 elapsed_time; /* nanoseconds */
126 gboolean show_remaining;
127 long duration;
128 gboolean seekable;
129 char *image_path;
130 gboolean show_album_art;
131 gboolean show_slider;
132 };
133
134 enum
135 {
136 PROP_0,
137 PROP_DB,
138 PROP_SHELL_PLAYER,
139 PROP_SEEKABLE,
140 PROP_SLIDER_DRAGGING,
141 PROP_SHOW_REMAINING,
142 PROP_SHOW_POSITION_SLIDER,
143 PROP_SHOW_ALBUM_ART
144 };
145
146 #define TITLE_FORMAT "<big><b>%s</b></big>"
147 #define ALBUM_FORMAT "<i>%s</i>"
148 #define ARTIST_FORMAT "<i>%s</i>"
149 #define STREAM_FORMAT "%s"
150
151 /* unicode graphic characters, encoded in UTF-8 */
152 static const char const *UNICODE_MIDDLE_DOT = "\xC2\xB7";
153
154 #define SCROLL_UP_SEEK_OFFSET 5
155 #define SCROLL_DOWN_SEEK_OFFSET -5
156
157 G_DEFINE_TYPE (RBHeader, rb_header, GTK_TYPE_GRID)
158
159 static void
160 rb_header_class_init (RBHeaderClass *klass)
161 {
162 GObjectClass *object_class = G_OBJECT_CLASS (klass);
163 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
164
165 object_class->dispose = rb_header_dispose;
166 object_class->finalize = rb_header_finalize;
167
168 object_class->set_property = rb_header_set_property;
169 object_class->get_property = rb_header_get_property;
170
171 widget_class->get_request_mode = rb_header_get_request_mode;
172 widget_class->get_preferred_width = rb_header_get_preferred_width;
173 widget_class->size_allocate = rb_header_size_allocate;
174 /* GtkGrid's get_preferred_height_for_width does all we need here */
175
176 /**
177 * RBHeader:db:
178 *
179 * #RhythmDB instance
180 */
181 g_object_class_install_property (object_class,
182 PROP_DB,
183 g_param_spec_object ("db",
184 "RhythmDB",
185 "RhythmDB object",
186 RHYTHMDB_TYPE,
187 G_PARAM_READWRITE));
188
189 /**
190 * RBHeader:shell-player:
191 *
192 * The #RBShellPlayer instance
193 */
194 g_object_class_install_property (object_class,
195 PROP_SHELL_PLAYER,
196 g_param_spec_object ("shell-player",
197 "shell player",
198 "RBShellPlayer object",
199 RB_TYPE_SHELL_PLAYER,
200 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
201 /**
202 * RBHeader:seekable:
203 *
204 * If TRUE, the header should allow seeking by dragging the playback position slider
205 */
206 g_object_class_install_property (object_class,
207 PROP_SEEKABLE,
208 g_param_spec_boolean ("seekable",
209 "seekable",
210 "seekable",
211 TRUE,
212 G_PARAM_READWRITE));
213
214 /**
215 * RBHeader:slider-dragging:
216 *
217 * Whether the song position slider is currently being dragged.
218 */
219 g_object_class_install_property (object_class,
220 PROP_SLIDER_DRAGGING,
221 g_param_spec_boolean ("slider-dragging",
222 "slider dragging",
223 "slider dragging",
224 FALSE,
225 G_PARAM_READABLE));
226 /**
227 * RBHeader:show-remaining:
228 *
229 * Whether to show remaining time (as opposed to elapsed time) in the numeric
230 * time display.
231 */
232 g_object_class_install_property (object_class,
233 PROP_SHOW_REMAINING,
234 g_param_spec_boolean ("show-remaining",
235 "show remaining",
236 "whether to show remaining or elapsed time",
237 FALSE,
238 G_PARAM_READWRITE));
239
240 /**
241 * RBHeader:show-position-slider:
242 *
243 * Whether to show the playback position slider.
244 */
245 g_object_class_install_property (object_class,
246 PROP_SHOW_POSITION_SLIDER,
247 g_param_spec_boolean ("show-position-slider",
248 "show position slider",
249 "whether to show the playback position slider",
250 TRUE,
251 G_PARAM_READWRITE));
252 /**
253 * RBHeader:show-album-art:
254 *
255 * Whether to show the album art display widget.
256 */
257 g_object_class_install_property (object_class,
258 PROP_SHOW_ALBUM_ART,
259 g_param_spec_boolean ("show-album-art",
260 "show album art",
261 "whether to show album art",
262 TRUE,
263 G_PARAM_READWRITE));
264
265 g_type_class_add_private (klass, sizeof (RBHeaderPrivate));
266 }
267
268 static void
269 rb_header_init (RBHeader *header)
270 {
271 header->priv = G_TYPE_INSTANCE_GET_PRIVATE (header, RB_TYPE_HEADER, RBHeaderPrivate);
272
273 gtk_grid_set_column_spacing (GTK_GRID (header), 6);
274 gtk_grid_set_column_homogeneous (GTK_GRID (header), TRUE);
275 gtk_container_set_border_width (GTK_CONTAINER (header), 3);
276
277 /* set up position slider */
278 header->priv->adjustment = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 10.0, 1.0, 10.0, 0.0));
279 header->priv->scale = gtk_scale_new (GTK_ORIENTATION_HORIZONTAL, header->priv->adjustment);
280 gtk_widget_set_hexpand (header->priv->scale, TRUE);
281 g_signal_connect_object (G_OBJECT (header->priv->scale),
282 "button_press_event",
283 G_CALLBACK (slider_press_callback),
284 header, 0);
285 g_signal_connect_object (G_OBJECT (header->priv->scale),
286 "button_release_event",
287 G_CALLBACK (slider_release_callback),
288 header, 0);
289 g_signal_connect_object (G_OBJECT (header->priv->scale),
290 "motion_notify_event",
291 G_CALLBACK (slider_moved_callback),
292 header, 0);
293 g_signal_connect_object (G_OBJECT (header->priv->scale),
294 "value_changed",
295 G_CALLBACK (slider_changed_callback),
296 header, 0);
297 g_signal_connect_object (G_OBJECT (header->priv->scale),
298 "scroll_event",
299 G_CALLBACK (slider_scroll_callback),
300 header, 0);
301 g_signal_connect_object (G_OBJECT (header->priv->scale),
302 "focus-out-event",
303 G_CALLBACK (slider_focus_out_callback),
304 header, 0);
305 gtk_scale_set_draw_value (GTK_SCALE (header->priv->scale), FALSE);
306 gtk_widget_set_size_request (header->priv->scale, 150, -1);
307
308 /* set up song information labels */
309 header->priv->songbox = gtk_grid_new ();
310 gtk_widget_set_hexpand (header->priv->songbox, TRUE);
311 gtk_widget_set_valign (header->priv->songbox, GTK_ALIGN_CENTER);
312
313 header->priv->song = gtk_label_new (" ");
314 gtk_label_set_use_markup (GTK_LABEL (header->priv->song), TRUE);
315 gtk_label_set_selectable (GTK_LABEL (header->priv->song), TRUE);
316 gtk_label_set_ellipsize (GTK_LABEL (header->priv->song), PANGO_ELLIPSIZE_END);
317 gtk_misc_set_alignment (GTK_MISC (header->priv->song), 0.0, 0.5);
318 gtk_grid_attach (GTK_GRID (header->priv->songbox), header->priv->song, 0, 0, 1, 1);
319
320 header->priv->details = gtk_label_new ("");
321 gtk_label_set_use_markup (GTK_LABEL (header->priv->details), TRUE);
322 gtk_label_set_selectable (GTK_LABEL (header->priv->details), TRUE);
323 gtk_label_set_ellipsize (GTK_LABEL (header->priv->details), PANGO_ELLIPSIZE_END);
324 gtk_widget_set_hexpand (header->priv->details, TRUE);
325 gtk_misc_set_alignment (GTK_MISC (header->priv->details), 0.0, 0.5);
326 gtk_grid_attach (GTK_GRID (header->priv->songbox), header->priv->details, 0, 1, 1, 2);
327
328 /* elapsed time / duration display */
329 header->priv->timelabel = gtk_label_new ("");
330 gtk_widget_set_halign (header->priv->timelabel, GTK_ALIGN_END);
331 gtk_widget_set_no_show_all (header->priv->timelabel, TRUE);
332
333 header->priv->timebutton = gtk_button_new ();
334 gtk_button_set_relief (GTK_BUTTON (header->priv->timebutton), GTK_RELIEF_NONE);
335 gtk_container_add (GTK_CONTAINER (header->priv->timebutton), header->priv->timelabel);
336 g_signal_connect_object (header->priv->timebutton,
337 "clicked",
338 G_CALLBACK (time_button_clicked_cb),
339 header, 0);
340
341 /* image display */
342 header->priv->art_store = rb_ext_db_new ("album-art");
343 g_signal_connect (header->priv->art_store,
344 "added",
345 G_CALLBACK (art_added_cb),
346 header);
347 header->priv->image = GTK_WIDGET (g_object_new (RB_TYPE_FADING_IMAGE,
348 "fallback", RB_STOCK_MISSING_ARTWORK,
349 NULL));
350 g_signal_connect (header->priv->image,
351 "pixbuf-dropped",
352 G_CALLBACK (pixbuf_dropped_cb),
353 header);
354 g_signal_connect (header->priv->image,
355 "uri-dropped",
356 G_CALLBACK (uri_dropped_cb),
357 header);
358 g_signal_connect (header->priv->image,
359 "button-press-event",
360 G_CALLBACK (image_button_press_cb),
361 header);
362
363 gtk_grid_attach (GTK_GRID (header), header->priv->image, 0, 0, 1, 1);
364 gtk_grid_attach (GTK_GRID (header), header->priv->songbox, 2, 0, 1, 1);
365 gtk_grid_attach (GTK_GRID (header), header->priv->timebutton, 3, 0, 1, 1);
366 gtk_grid_attach (GTK_GRID (header), header->priv->scale, 4, 0, 1, 1);
367
368 /* currently, nothing sets this. it should be set on track changes. */
369 header->priv->seekable = TRUE;
370
371 rb_header_sync (header);
372 }
373
374 static void
375 rb_header_dispose (GObject *object)
376 {
377 RBHeader *header = RB_HEADER (object);
378
379 if (header->priv->db != NULL) {
380 g_object_unref (header->priv->db);
381 header->priv->db = NULL;
382 }
383
384 if (header->priv->shell_player != NULL) {
385 g_object_unref (header->priv->shell_player);
386 header->priv->shell_player = NULL;
387 }
388
389 if (header->priv->art_store != NULL) {
390 g_object_unref (header->priv->art_store);
391 header->priv->art_store = NULL;
392 }
393
394 G_OBJECT_CLASS (rb_header_parent_class)->dispose (object);
395 }
396
397 static void
398 rb_header_finalize (GObject *object)
399 {
400 RBHeader *header;
401
402 g_return_if_fail (object != NULL);
403 g_return_if_fail (RB_IS_HEADER (object));
404
405 header = RB_HEADER (object);
406 g_return_if_fail (header->priv != NULL);
407
408 g_free (header->priv->image_path);
409
410 G_OBJECT_CLASS (rb_header_parent_class)->finalize (object);
411 }
412
413 static void
414 art_cb (RBExtDBKey *key, const char *filename, GValue *data, RBHeader *header)
415 {
416 RhythmDBEntry *entry;
417
418 entry = rb_shell_player_get_playing_entry (header->priv->shell_player);
419 if (entry == NULL) {
420 return;
421 }
422
423 if (rhythmdb_entry_matches_ext_db_key (header->priv->db, entry, key)) {
424 GdkPixbuf *pixbuf = NULL;
425
426 if (data != NULL && G_VALUE_HOLDS (data, GDK_TYPE_PIXBUF)) {
427 pixbuf = GDK_PIXBUF (g_value_get_object (data));
428 }
429
430 rb_fading_image_set_pixbuf (RB_FADING_IMAGE (header->priv->image), pixbuf);
431
432 g_free (header->priv->image_path);
433 header->priv->image_path = g_strdup (filename);
434 }
435
436 rhythmdb_entry_unref (entry);
437 }
438
439 static void
440 art_added_cb (RBExtDB *db, RBExtDBKey *key, const char *filename, GValue *data, RBHeader *header)
441 {
442 art_cb (key, filename, data, header);
443 }
444
445
446 static void
447 rb_header_playing_song_changed_cb (RBShellPlayer *player, RhythmDBEntry *entry, RBHeader *header)
448 {
449 if (header->priv->entry == entry)
450 return;
451
452 rb_fading_image_start (RB_FADING_IMAGE (header->priv->image), 2000);
453
454 header->priv->entry = entry;
455 if (header->priv->entry) {
456 RBExtDBKey *key;
457
458 header->priv->duration = rhythmdb_entry_get_ulong (header->priv->entry,
459 RHYTHMDB_PROP_DURATION);
460
461 key = rhythmdb_entry_create_ext_db_key (entry, RHYTHMDB_PROP_ALBUM);
462 rb_ext_db_request (header->priv->art_store,
463 key,
464 (RBExtDBRequestCallback) art_cb,
465 g_object_ref (header),
466 g_object_unref);
467 rb_ext_db_key_free (key);
468 } else {
469 header->priv->duration = 0;
470 }
471
472 rb_header_sync (header);
473
474 g_free (header->priv->image_path);
475 header->priv->image_path = NULL;
476 }
477
478 static GtkSizeRequestMode
479 rb_header_get_request_mode (GtkWidget *widget)
480 {
481 return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
482 }
483
484 static void
485 rb_header_get_preferred_width (GtkWidget *widget,
486 int *minimum_width,
487 int *natural_width)
488 {
489 *minimum_width = 0;
490 *natural_width = 0;
491 }
492
493 static void
494 rb_header_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
495 {
496 int spacing;
497 int scale_width;
498 int info_width;
499 int time_width;
500 int image_width;
501 GtkAllocation child_alloc;
502 gboolean rtl;
503
504 gtk_widget_set_allocation (widget, allocation);
505 spacing = gtk_grid_get_column_spacing (GTK_GRID (widget));
506 rtl = (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL);
507
508 /* take some leading space for the image, which we always make square */
509 if (RB_HEADER (widget)->priv->show_album_art) {
510 image_width = allocation->height;
511 if (rtl) {
512 child_alloc.x = allocation->x + allocation->width - image_width;
513 allocation->x -= image_width + spacing;
514 } else {
515 child_alloc.x = allocation->x;
516 allocation->x += image_width + spacing;
517 }
518 allocation->width -= image_width + spacing;
519 child_alloc.y = allocation->y;
520 child_alloc.width = image_width;
521 child_alloc.height = allocation->height;
522 gtk_widget_size_allocate (RB_HEADER (widget)->priv->image, &child_alloc);
523 } else {
524 image_width = 0;
525 }
526
527 /* figure out how much space to allocate to the scale.
528 * it gets at least its minimum size, at most 1/3 of the
529 * space we have.
530 */
531 if (RB_HEADER (widget)->priv->show_slider) {
532 gtk_widget_get_preferred_width (RB_HEADER (widget)->priv->scale, &scale_width, NULL);
533 if (scale_width < allocation->width / 3)
534 scale_width = allocation->width / 3;
535
536 if (scale_width + image_width > allocation->width)
537 scale_width = allocation->width - image_width;
538
539 if (scale_width > 0) {
540 if (rtl) {
541 child_alloc.x = allocation->x;
542 } else {
543 child_alloc.x = allocation->x + (allocation->width - scale_width) + spacing;
544 }
545 child_alloc.y = allocation->y;
546 child_alloc.width = scale_width - spacing;
547 child_alloc.height = allocation->height;
548 gtk_widget_show (RB_HEADER (widget)->priv->scale);
549 gtk_widget_size_allocate (RB_HEADER (widget)->priv->scale, &child_alloc);
550 } else {
551 gtk_widget_hide (RB_HEADER (widget)->priv->scale);
552 }
553 } else {
554 scale_width = 0;
555 }
556
557 /* time button gets its minimum size */
558 gtk_widget_get_preferred_width (RB_HEADER (widget)->priv->songbox, NULL, &info_width);
559 if (gtk_widget_get_visible (RB_HEADER (widget)->priv->timelabel)) {
560 gtk_widget_get_preferred_width (RB_HEADER (widget)->priv->timebutton, &time_width, NULL);
561 } else {
562 time_width = 0;
563 }
564
565 info_width = allocation->width - (scale_width + time_width) - (2 * spacing);
566
567 if (rtl) {
568 child_alloc.x = allocation->x + allocation->width - info_width;
569 } else {
570 child_alloc.x = allocation->x;
571 }
572
573 if (info_width > 0) {
574 child_alloc.y = allocation->y;
575 child_alloc.width = info_width;
576 child_alloc.height = allocation->height;
577 gtk_widget_show (RB_HEADER (widget)->priv->songbox);
578 gtk_widget_size_allocate (RB_HEADER (widget)->priv->songbox, &child_alloc);
579 } else {
580 gtk_widget_hide (RB_HEADER (widget)->priv->songbox);
581 info_width = 0;
582 }
583
584 if (time_width == 0) {
585 gtk_widget_hide (RB_HEADER (widget)->priv->timebutton);
586 } else if (info_width + scale_width + (2 * spacing) + time_width > allocation->width) {
587 gtk_widget_hide (RB_HEADER (widget)->priv->timebutton);
588 } else {
589 if (rtl) {
590 child_alloc.x = allocation->x + scale_width + spacing;
591 } else {
592 child_alloc.x = allocation->x + info_width + spacing;
593 }
594 child_alloc.y = allocation->y;
595 child_alloc.width = time_width;
596 child_alloc.height = allocation->height;
597 gtk_widget_show (RB_HEADER (widget)->priv->timebutton);
598 gtk_widget_size_allocate (RB_HEADER (widget)->priv->timebutton, &child_alloc);
599 }
600 }
601
602 static void
603 rb_header_set_property (GObject *object,
604 guint prop_id,
605 const GValue *value,
606 GParamSpec *pspec)
607 {
608 RBHeader *header = RB_HEADER (object);
609
610 switch (prop_id) {
611 case PROP_DB:
612 header->priv->db = g_value_get_object (value);
613 g_signal_connect_object (header->priv->db,
614 "entry-extra-metadata-notify",
615 G_CALLBACK (rb_header_extra_metadata_cb),
616 header, 0);
617 break;
618 case PROP_SHELL_PLAYER:
619 header->priv->shell_player = g_value_get_object (value);
620 g_signal_connect_object (header->priv->shell_player,
621 "elapsed-nano-changed",
622 G_CALLBACK (rb_header_elapsed_changed_cb),
623 header, 0);
624 g_signal_connect_object (header->priv->shell_player,
625 "playing-song-changed",
626 G_CALLBACK (rb_header_playing_song_changed_cb),
627 header, 0);
628 break;
629 case PROP_SEEKABLE:
630 header->priv->seekable = g_value_get_boolean (value);
631 break;
632 case PROP_SHOW_REMAINING:
633 header->priv->show_remaining = g_value_get_boolean (value);
634 rb_header_update_elapsed (header);
635 break;
636 case PROP_SHOW_POSITION_SLIDER:
637 header->priv->show_slider = g_value_get_boolean (value);
638 gtk_widget_set_visible (header->priv->scale, header->priv->show_slider);
639 break;
640 case PROP_SHOW_ALBUM_ART:
641 header->priv->show_album_art = g_value_get_boolean (value);
642 gtk_widget_set_visible (header->priv->image, header->priv->show_album_art);
643 break;
644 default:
645 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
646 break;
647 }
648 }
649
650 static void
651 rb_header_get_property (GObject *object,
652 guint prop_id,
653 GValue *value,
654 GParamSpec *pspec)
655 {
656 RBHeader *header = RB_HEADER (object);
657
658 switch (prop_id) {
659 case PROP_DB:
660 g_value_set_object (value, header->priv->db);
661 break;
662 case PROP_SHELL_PLAYER:
663 g_value_set_object (value, header->priv->shell_player);
664 break;
665 case PROP_SEEKABLE:
666 g_value_set_boolean (value, header->priv->seekable);
667 break;
668 case PROP_SLIDER_DRAGGING:
669 g_value_set_boolean (value, header->priv->slider_dragging);
670 break;
671 case PROP_SHOW_REMAINING:
672 g_value_set_boolean (value, header->priv->show_remaining);
673 break;
674 case PROP_SHOW_POSITION_SLIDER:
675 g_value_set_boolean (value, header->priv->show_slider);
676 break;
677 case PROP_SHOW_ALBUM_ART:
678 g_value_set_boolean (value, header->priv->show_album_art);
679 break;
680 default:
681 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
682 break;
683 }
684 }
685
686 /**
687 * rb_header_new:
688 * @shell_player: the #RBShellPlayer instance
689 * @db: the #RhythmDB instance
690 *
691 * Creates a new header widget.
692 *
693 * Return value: the header widget
694 */
695 RBHeader *
696 rb_header_new (RBShellPlayer *shell_player, RhythmDB *db)
697 {
698 RBHeader *header;
699
700 header = RB_HEADER (g_object_new (RB_TYPE_HEADER,
701 "shell-player", shell_player,
702 "db", db,
703 NULL));
704
705 g_return_val_if_fail (header->priv != NULL, NULL);
706
707 return header;
708 }
709
710 static void
711 get_extra_metadata (RhythmDB *db, RhythmDBEntry *entry, const char *field, char **value)
712 {
713 GValue *v;
714
715 v = rhythmdb_entry_request_extra_metadata (db, entry, field);
716 if (v != NULL) {
717 *value = g_value_dup_string (v);
718 g_value_unset (v);
719 g_free (v);
720 } else {
721 *value = NULL;
722 }
723 }
724
725 static void
726 rb_header_sync (RBHeader *header)
727 {
728 char *label_text;
729 const char *location = "<null>";
730
731 if (header->priv->entry != NULL) {
732 location = rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_LOCATION);
733 }
734 rb_debug ("syncing with entry = %s", location);
735
736 if (header->priv->entry != NULL) {
737 const char *title;
738 const char *album;
739 const char *artist;
740 const char *stream_name = NULL;
741 char *streaming_title;
742 char *streaming_artist;
743 char *streaming_album;
744 PangoDirection widget_dir;
745
746 gboolean have_duration = (header->priv->duration > 0);
747
748 title = rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_TITLE);
749 album = rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_ALBUM);
750 artist = rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_ARTIST);
751
752 get_extra_metadata (header->priv->db,
753 header->priv->entry,
754 RHYTHMDB_PROP_STREAM_SONG_TITLE,
755 &streaming_title);
756 if (streaming_title) {
757 /* use entry title as stream name */
758 stream_name = title;
759 title = streaming_title;
760 }
761
762 get_extra_metadata (header->priv->db,
763 header->priv->entry,
764 RHYTHMDB_PROP_STREAM_SONG_ARTIST,
765 &streaming_artist);
766 if (streaming_artist) {
767 /* override artist from entry */
768 artist = streaming_artist;
769 }
770
771 get_extra_metadata (header->priv->db,
772 header->priv->entry,
773 RHYTHMDB_PROP_STREAM_SONG_ALBUM,
774 &streaming_album);
775 if (streaming_album) {
776 /* override album from entry */
777 album = streaming_album;
778 }
779
780 widget_dir = (gtk_widget_get_direction (GTK_WIDGET (header->priv->song)) == GTK_TEXT_DIR_LTR) ?
781 PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL;
782
783 char *t;
784 t = rb_text_cat (widget_dir, title, TITLE_FORMAT, NULL);
785 gtk_label_set_markup (GTK_LABEL (header->priv->song), t);
786 g_free (t);
787
788 if (artist == NULL || artist[0] == '\0') { /* this is crap; should be up to the entry type */
789 if (stream_name != NULL) {
790 t = rb_text_cat (widget_dir, stream_name, STREAM_FORMAT, NULL);
791 gtk_label_set_markup (GTK_LABEL (header->priv->details), t);
792 g_free (t);
793 } else {
794 gtk_label_set_markup (GTK_LABEL (header->priv->details), "");
795 }
796 } else {
797 const char *by;
798 const char *from;
799 PangoDirection dir;
800 PangoDirection native;
801
802 native = PANGO_DIRECTION_LTR;
803 if (gtk_widget_get_direction (GTK_WIDGET (header->priv->details)) != GTK_TEXT_DIR_LTR) {
804 native = PANGO_DIRECTION_RTL;
805 }
806
807 dir = rb_text_common_direction (artist, album, NULL);
808 if (!rb_text_direction_conflict (dir, native)) {
809 dir = native;
810 by = _("by");
811 from = _("from");
812 } else {
813 by = UNICODE_MIDDLE_DOT;
814 from = UNICODE_MIDDLE_DOT;
815 }
816
817 t = rb_text_cat (dir,
818 by, "%s",
819 artist, ARTIST_FORMAT,
820 from, "%s",
821 album, ALBUM_FORMAT,
822 NULL);
823 gtk_label_set_markup (GTK_LABEL (header->priv->details), t);
824 g_free (t);
825 }
826
827 gtk_widget_set_sensitive (header->priv->scale, have_duration && header->priv->seekable);
828 rb_header_sync_time (header);
829
830 g_free (streaming_artist);
831 g_free (streaming_album);
832 g_free (streaming_title);
833 } else {
834 rb_debug ("not playing");
835 label_text = g_markup_printf_escaped (TITLE_FORMAT, _("Not Playing"));
836 gtk_label_set_markup (GTK_LABEL (header->priv->song), label_text);
837 g_free (label_text);
838
839 gtk_label_set_text (GTK_LABEL (header->priv->details), "");
840
841 rb_header_sync_time (header);
842 }
843 }
844
845 static void
846 rb_header_sync_time (RBHeader *header)
847 {
848 if (header->priv->shell_player == NULL)
849 return;
850
851 if (header->priv->slider_dragging == TRUE) {
852 rb_debug ("slider is dragging, not syncing");
853 return;
854 }
855
856 if (header->priv->duration > 0) {
857 double progress = ((double) header->priv->elapsed_time) / RB_PLAYER_SECOND;
858
859 header->priv->slider_locked = TRUE;
860
861 g_object_freeze_notify (G_OBJECT (header->priv->adjustment));
862 gtk_adjustment_set_value (header->priv->adjustment, progress);
863 gtk_adjustment_set_upper (header->priv->adjustment, header->priv->duration);
864 g_object_thaw_notify (G_OBJECT (header->priv->adjustment));
865
866 header->priv->slider_locked = FALSE;
867 gtk_widget_set_sensitive (header->priv->scale, header->priv->seekable);
868 } else {
869 header->priv->slider_locked = TRUE;
870
871 g_object_freeze_notify (G_OBJECT (header->priv->adjustment));
872 gtk_adjustment_set_value (header->priv->adjustment, 0.0);
873 gtk_adjustment_set_upper (header->priv->adjustment, 0.0);
874 g_object_thaw_notify (G_OBJECT (header->priv->adjustment));
875
876 header->priv->slider_locked = FALSE;
877 gtk_widget_set_sensitive (header->priv->scale, FALSE);
878 }
879
880 rb_header_update_elapsed (header);
881 }
882
883 static gboolean
884 slider_press_callback (GtkWidget *widget,
885 GdkEventButton *event,
886 RBHeader *header)
887 {
888 header->priv->slider_dragging = TRUE;
889 header->priv->slider_drag_moved = FALSE;
890 header->priv->latest_set_time = -1;
891 g_object_notify (G_OBJECT (header), "slider-dragging");
892
893 #if !GTK_CHECK_VERSION(3,5,0)
894 /* HACK: we want the behaviour you get with the middle button, so we
895 * mangle the event. clicking with other buttons moves the slider in
896 * step increments, clicking with the middle button moves the slider to
897 * the location of the click.
898 */
899 event->button = 2;
900 #endif
901
902
903 return FALSE;
904 }
905
906 static gboolean
907 slider_moved_timeout (RBHeader *header)
908 {
909 GDK_THREADS_ENTER ();
910
911 apply_slider_position (header);
912 header->priv->slider_moved_timeout = 0;
913 header->priv->slider_drag_moved = FALSE;
914
915 GDK_THREADS_LEAVE ();
916
917 return FALSE;
918 }
919
920 static gboolean
921 slider_moved_callback (GtkWidget *widget,
922 GdkEventMotion *event,
923 RBHeader *header)
924 {
925 double progress;
926
927 if (header->priv->slider_dragging == FALSE) {
928 rb_debug ("slider is not dragging");
929 return FALSE;
930 }
931 header->priv->slider_drag_moved = TRUE;
932
933 progress = gtk_adjustment_get_value (header->priv->adjustment);
934 header->priv->elapsed_time = (gint64) ((progress+0.5) * RB_PLAYER_SECOND);
935
936 rb_header_update_elapsed (header);
937
938 if (header->priv->slider_moved_timeout != 0) {
939 rb_debug ("removing old timer");
940 g_source_remove (header->priv->slider_moved_timeout);
941 header->priv->slider_moved_timeout = 0;
942 }
943 header->priv->slider_moved_timeout =
944 g_timeout_add (40, (GSourceFunc) slider_moved_timeout, header);
945
946 return FALSE;
947 }
948
949 static void
950 apply_slider_position (RBHeader *header)
951 {
952 double progress;
953 long new;
954
955 progress = gtk_adjustment_get_value (header->priv->adjustment);
956 new = (long) (progress+0.5);
957
958 if (new != header->priv->latest_set_time) {
959 rb_debug ("setting time to %ld", new);
960 rb_shell_player_set_playing_time (header->priv->shell_player, new, NULL);
961 header->priv->latest_set_time = new;
962 }
963 }
964
965 static gboolean
966 slider_release_callback (GtkWidget *widget,
967 GdkEventButton *event,
968 RBHeader *header)
969 {
970 #if !GTK_CHECK_VERSION(3,5,0)
971 /* HACK: see slider_press_callback */
972 event->button = 2;
973 #endif
974
975 if (header->priv->slider_dragging == FALSE) {
976 rb_debug ("slider is not dragging");
977 return FALSE;
978 }
979
980 if (header->priv->slider_moved_timeout != 0) {
981 g_source_remove (header->priv->slider_moved_timeout);
982 header->priv->slider_moved_timeout = 0;
983 }
984
985 if (header->priv->slider_drag_moved)
986 apply_slider_position (header);
987
988 header->priv->slider_dragging = FALSE;
989 header->priv->slider_drag_moved = FALSE;
990 g_object_notify (G_OBJECT (header), "slider-dragging");
991 return FALSE;
992 }
993
994 static void
995 slider_changed_callback (GtkWidget *widget,
996 RBHeader *header)
997 {
998 /* if the slider isn't being dragged, and nothing else is happening,
999 * this indicates the position was adjusted with a keypress (page up/page down etc.),
1000 * so we should directly apply the change.
1001 */
1002 if (header->priv->slider_dragging == FALSE &&
1003 header->priv->slider_locked == FALSE) {
1004 apply_slider_position (header);
1005 } else if (header->priv->slider_dragging) {
1006 header->priv->slider_drag_moved = TRUE;
1007 }
1008 }
1009
1010 static gboolean
1011 slider_scroll_callback (GtkWidget *widget, GdkEventScroll *event, RBHeader *header)
1012 {
1013 gboolean retval = TRUE;
1014 gdouble adj = gtk_adjustment_get_value (header->priv->adjustment);
1015
1016 switch (event->direction) {
1017 case GDK_SCROLL_UP:
1018 rb_debug ("slider scrolling up");
1019 gtk_adjustment_set_value (header->priv->adjustment, adj + SCROLL_UP_SEEK_OFFSET);
1020 break;
1021
1022 case GDK_SCROLL_DOWN:
1023 rb_debug ("slider scrolling down");
1024 gtk_adjustment_set_value (header->priv->adjustment, adj + SCROLL_DOWN_SEEK_OFFSET);
1025 break;
1026
1027 default:
1028 retval = FALSE;
1029 break;
1030 }
1031
1032 return retval;
1033 }
1034
1035 static gboolean
1036 slider_focus_out_callback (GtkWidget *widget, GdkEvent *event, RBHeader *header)
1037 {
1038 if (header->priv->slider_dragging) {
1039 if (header->priv->slider_drag_moved)
1040 apply_slider_position (header);
1041
1042 header->priv->slider_dragging = FALSE;
1043 header->priv->slider_drag_moved = FALSE;
1044 g_object_notify (G_OBJECT (header), "slider-dragging");
1045 }
1046 return FALSE;
1047 }
1048
1049 static void
1050 rb_header_update_elapsed (RBHeader *header)
1051 {
1052 long seconds;
1053 char *elapsed;
1054 char *duration;
1055 char *label;
1056
1057 if (header->priv->entry == NULL) {
1058 gtk_label_set_text (GTK_LABEL (header->priv->timelabel), "");
1059 gtk_widget_hide (header->priv->timelabel);
1060 return;
1061 }
1062 gtk_widget_show (header->priv->timelabel);
1063
1064 seconds = header->priv->elapsed_time / RB_PLAYER_SECOND;
1065 if (header->priv->duration == 0) {
1066 label = rb_make_time_string (seconds);
1067 gtk_label_set_text (GTK_LABEL (header->priv->timelabel), label);
1068 g_free (label);
1069 } else if (header->priv->show_remaining) {
1070
1071 duration = rb_make_time_string (header->priv->duration);
1072
1073 if (seconds > header->priv->duration) {
1074 elapsed = rb_make_time_string (0);
1075 } else {
1076 elapsed = rb_make_time_string (header->priv->duration - seconds);
1077 }
1078
1079 /* Translators: remaining time / total time */
1080 label = g_strdup_printf (_("-%s / %s"), elapsed, duration);
1081 gtk_widget_show (header->priv->timebutton);
1082 gtk_label_set_text (GTK_LABEL (header->priv->timelabel), label);
1083
1084 g_free (elapsed);
1085 g_free (duration);
1086 g_free (label);
1087 } else {
1088 elapsed = rb_make_time_string (seconds);
1089 duration = rb_make_time_string (header->priv->duration);
1090
1091 /* Translators: elapsed time / total time */
1092 label = g_strdup_printf (_("%s / %s"), elapsed, duration);
1093 gtk_label_set_text (GTK_LABEL (header->priv->timelabel), label);
1094
1095 g_free (elapsed);
1096 g_free (duration);
1097 g_free (label);
1098 }
1099 }
1100
1101 static void
1102 rb_header_elapsed_changed_cb (RBShellPlayer *player,
1103 gint64 elapsed,
1104 RBHeader *header)
1105 {
1106 header->priv->elapsed_time = elapsed;
1107 rb_header_sync_time (header);
1108 }
1109
1110 static void
1111 rb_header_extra_metadata_cb (RhythmDB *db,
1112 RhythmDBEntry *entry,
1113 const char *property_name,
1114 const GValue *metadata,
1115 RBHeader *header)
1116 {
1117 if (entry != header->priv->entry)
1118 return;
1119
1120 if (g_str_equal (property_name, RHYTHMDB_PROP_STREAM_SONG_TITLE) ||
1121 g_str_equal (property_name, RHYTHMDB_PROP_STREAM_SONG_ARTIST) ||
1122 g_str_equal (property_name, RHYTHMDB_PROP_STREAM_SONG_ALBUM)) {
1123 rb_header_sync (header);
1124 }
1125 }
1126
1127 static void
1128 pixbuf_dropped_cb (RBFadingImage *image, GdkPixbuf *pixbuf, RBHeader *header)
1129 {
1130 RBExtDBKey *key;
1131 const char *artist;
1132 GValue v = G_VALUE_INIT;
1133
1134 if (header->priv->entry == NULL || pixbuf == NULL)
1135 return;
1136
1137 /* maybe ignore tiny pixbufs? */
1138
1139 key = rb_ext_db_key_create_storage ("album", rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_ALBUM));
1140 artist = rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_ALBUM_ARTIST);
1141 if (artist == NULL || artist[0] == '\0' || strcmp (artist, _("Unknown")) == 0) {
1142 artist = rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_ARTIST);
1143 }
1144 rb_ext_db_key_add_field (key, "artist", artist);
1145
1146 g_value_init (&v, GDK_TYPE_PIXBUF);
1147 g_value_set_object (&v, image);
1148 rb_ext_db_store (header->priv->art_store, key, RB_EXT_DB_SOURCE_USER_EXPLICIT, &v);
1149 g_value_unset (&v);
1150
1151 rb_ext_db_key_free (key);
1152 }
1153
1154 static void
1155 uri_dropped_cb (RBFadingImage *image, const char *uri, RBHeader *header)
1156 {
1157 RBExtDBKey *key;
1158 const char *artist;
1159
1160 if (header->priv->entry == NULL || uri == NULL)
1161 return;
1162
1163 /* maybe ignore tiny pixbufs? */
1164
1165 key = rb_ext_db_key_create_storage ("album", rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_ALBUM));
1166 artist = rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_ALBUM_ARTIST);
1167 if (artist == NULL || artist[0] == '\0' || strcmp (artist, _("Unknown")) == 0) {
1168 artist = rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_ARTIST);
1169 }
1170 rb_ext_db_key_add_field (key, "artist", artist);
1171
1172 rb_ext_db_store_uri (header->priv->art_store, key, RB_EXT_DB_SOURCE_USER_EXPLICIT, uri);
1173
1174 rb_ext_db_key_free (key);
1175 }
1176
1177 static void
1178 image_button_press_cb (GtkWidget *widget, GdkEvent *event, RBHeader *header)
1179 {
1180 if (event->button.type != GDK_2BUTTON_PRESS ||
1181 event->button.button != 1)
1182 return;
1183
1184 if (header->priv->image_path != NULL) {
1185 GAppInfo *app;
1186 GAppLaunchContext *context;
1187 GList *files = NULL;
1188
1189 app = g_app_info_get_default_for_type ("image/jpeg", FALSE);
1190 if (app == NULL) {
1191 return;
1192 }
1193
1194 files = g_list_append (NULL, g_file_new_for_path (header->priv->image_path));
1195
1196 context = G_APP_LAUNCH_CONTEXT (gdk_display_get_app_launch_context (gtk_widget_get_display (widget)));
1197 g_app_info_launch (app, files, context, NULL);
1198 g_object_unref (context);
1199 g_object_unref (app);
1200 g_list_free_full (files, g_object_unref);
1201 }
1202 }
1203
1204 static void
1205 time_button_clicked_cb (GtkWidget *widget, RBHeader *header)
1206 {
1207 g_object_set (header, "show-remaining", !header->priv->show_remaining, NULL);
1208 }