rb-header.c 33.8 KB
Newer Older
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 7 8 9 10
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
11
 *  The Rhythmbox authors hereby grant permission for non-GPL compatible
James Livingston's avatar
James Livingston committed
12 13 14 15 16 17 18
 *  GStreamer plugins to be used and distributed together with GStreamer
 *  and Rhythmbox. This permission is above and beyond the permissions granted
 *  by the GPL license by which Rhythmbox is covered. If you modify this code
 *  you may extend this exception to your version of the code, but you are not
 *  obligated to do so. If you do not wish to do so, delete this exception
 *  statement from your version.
 *
19 20 21 22 23 24 25
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
Ryan P. Skadberg's avatar
Ryan P. Skadberg committed
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 34
#include <string.h>

35 36 37
#include <glib/gi18n.h>
#include <gtk/gtk.h>

38
#include "rb-stock-icons.h"
39
#include "rb-header.h"
40
#include "rb-debug.h"
41 42
#include "rb-shell-player.h"
#include "rb-util.h"
43
#include "rhythmdb.h"
44
#include "rb-player.h"
45
#include "rb-text-helpers.h"
46 47 48
#include "rb-fading-image.h"
#include "rb-file-helpers.h"
#include "rb-ext-db.h"
49

50 51 52 53 54 55 56 57 58 59 60 61 62 63
/**
 * SECTION:rb-header
 * @short_description: playback area widgetry
 *
 * The RBHeader widget displays information about the current playing track
 * (title, album, artist), the elapsed or remaining playback time, and a
 * position slider indicating the playback position.  It translates slider
 * move and drag events into seek requests for the player backend.
 *
 * For shoutcast-style streams, the title/artist/album display is supplemented
 * by metadata extracted from the stream.  See #RBStreamingSource for more information
 * on how the metadata is reported.
 */

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 68
static void rb_header_finalize (GObject *object);
static void rb_header_set_property (GObject *object,
69 70 71
				    guint prop_id,
				    const GValue *value,
				    GParamSpec *pspec);
72
static void rb_header_get_property (GObject *object,
73 74 75
				    guint prop_id,
				    GValue *value,
				    GParamSpec *pspec);
76 77 78 79
static GtkSizeRequestMode rb_header_get_request_mode (GtkWidget *widget);
static void rb_header_get_preferred_width (GtkWidget *widget,
					   int *minimum_size,
					   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 84 85 86
static gboolean slider_press_callback (GtkWidget *widget, GdkEventButton *event, RBHeader *header);
static gboolean slider_moved_callback (GtkWidget *widget, GdkEventMotion *event, RBHeader *header);
static gboolean slider_release_callback (GtkWidget *widget, GdkEventButton *event, RBHeader *header);
static void slider_changed_callback (GtkWidget *widget, RBHeader *header);
87
static gboolean slider_scroll_callback (GtkWidget *widget, GdkEventScroll *event, RBHeader *header);
88
static void time_button_clicked_cb (GtkWidget *button, RBHeader *header);
89

90
static void rb_header_elapsed_changed_cb (RBShellPlayer *player, gint64 elapsed, RBHeader *header);
91
static void rb_header_extra_metadata_cb (RhythmDB *db, RhythmDBEntry *entry, const char *property_name, const GValue *metadata, RBHeader *header);
92 93 94 95 96 97
static void rb_header_sync (RBHeader *header);
static void rb_header_sync_time (RBHeader *header);

static void uri_dropped_cb (RBFadingImage *image, const char *uri, RBHeader *header);
static void pixbuf_dropped_cb (RBFadingImage *image, GdkPixbuf *pixbuf, RBHeader *header);
static void image_button_press_cb (GtkWidget *widget, GdkEvent *event, RBHeader *header);
98
static void art_added_cb (RBExtDB *db, RBExtDBKey *key, const char *filename, GValue *data, RBHeader *header);
99

100
struct RBHeaderPrivate
101 102 103
{
	RhythmDB *db;
	RhythmDBEntry *entry;
104
	RBExtDB *art_store;
105

106
	RBShellPlayer *shell_player;
107

108
	GtkWidget *songbox;
109
	GtkWidget *song;
110 111
	GtkWidget *details;
	GtkWidget *image;
112

113 114 115 116 117 118
	GtkWidget *scale;
	GtkAdjustment *adjustment;
	gboolean slider_dragging;
	gboolean slider_locked;
	guint slider_moved_timeout;
	long latest_set_time;
119 120 121

	GtkWidget *timebutton;
	GtkWidget *timelabel;
122

123
	gint64 elapsed_time;		/* nanoseconds */
124
	gboolean show_remaining;
125
	long duration;
126
	gboolean seekable;
127
	char *image_path;
128 129
	gboolean show_album_art;
	gboolean show_slider;
130 131 132 133 134 135
};

enum
{
	PROP_0,
	PROP_DB,
136
	PROP_SHELL_PLAYER,
137
	PROP_SEEKABLE,
138 139
	PROP_SLIDER_DRAGGING,
	PROP_SHOW_REMAINING,
140 141
	PROP_SHOW_POSITION_SLIDER,
	PROP_SHOW_ALBUM_ART
142 143
};

144 145 146
#define TITLE_FORMAT  "<big><b>%s</b></big>"
#define ALBUM_FORMAT  "<i>%s</i>"
#define ARTIST_FORMAT "<i>%s</i>"
147 148 149 150
#define STREAM_FORMAT "%s"

/* unicode graphic characters, encoded in UTF-8 */
static const char const *UNICODE_MIDDLE_DOT = "\xC2\xB7";
151

152 153 154
#define SCROLL_UP_SEEK_OFFSET	5
#define SCROLL_DOWN_SEEK_OFFSET -5

155
G_DEFINE_TYPE (RBHeader, rb_header, GTK_TYPE_GRID)
156 157

static void
158
rb_header_class_init (RBHeaderClass *klass)
159 160
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
161
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
162

163
	object_class->dispose = rb_header_dispose;
164
	object_class->finalize = rb_header_finalize;
165

166 167
	object_class->set_property = rb_header_set_property;
	object_class->get_property = rb_header_get_property;
168

169 170
	widget_class->get_request_mode = rb_header_get_request_mode;
	widget_class->get_preferred_width = rb_header_get_preferred_width;
171
	widget_class->size_allocate = rb_header_size_allocate;
172
	/* GtkGrid's get_preferred_height_for_width does all we need here */
173

174 175 176 177 178
	/**
	 * RBHeader:db:
	 *
	 * #RhythmDB instance
	 */
179 180 181 182 183 184
	g_object_class_install_property (object_class,
					 PROP_DB,
					 g_param_spec_object ("db",
							      "RhythmDB",
							      "RhythmDB object",
							      RHYTHMDB_TYPE,
185
							      G_PARAM_READWRITE));
186

187 188 189 190 191
	/**
	 * RBHeader:shell-player:
	 *
	 * The #RBShellPlayer instance
	 */
192
	g_object_class_install_property (object_class,
193 194 195 196 197 198
					 PROP_SHELL_PLAYER,
					 g_param_spec_object ("shell-player",
							      "shell player",
							      "RBShellPlayer object",
							      RB_TYPE_SHELL_PLAYER,
							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
199 200 201 202 203
	/**
	 * RBHeader:seekable:
	 *
	 * If TRUE, the header should allow seeking by dragging the playback position slider
	 */
204 205 206 207 208 209 210 211
	g_object_class_install_property (object_class,
					 PROP_SEEKABLE,
					 g_param_spec_boolean ("seekable",
						 	       "seekable",
							       "seekable",
							       TRUE,
							       G_PARAM_READWRITE));

212 213 214 215 216 217 218 219 220 221 222 223
	/**
	 * RBHeader:slider-dragging:
	 *
	 * Whether the song position slider is currently being dragged.
	 */
	g_object_class_install_property (object_class,
					 PROP_SLIDER_DRAGGING,
					 g_param_spec_boolean ("slider-dragging",
						 	       "slider dragging",
							       "slider dragging",
							       FALSE,
							       G_PARAM_READABLE));
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
	/**
	 * RBHeader:show-remaining:
	 *
	 * Whether to show remaining time (as opposed to elapsed time) in the numeric
	 * time display.
	 */
	g_object_class_install_property (object_class,
					 PROP_SHOW_REMAINING,
					 g_param_spec_boolean ("show-remaining",
							       "show remaining",
							       "whether to show remaining or elapsed time",
							       FALSE,
							       G_PARAM_READWRITE));

	/**
	 * RBHeader:show-position-slider:
	 *
	 * Whether to show the playback position slider.
	 */
	g_object_class_install_property (object_class,
					 PROP_SHOW_POSITION_SLIDER,
					 g_param_spec_boolean ("show-position-slider",
							       "show position slider",
							       "whether to show the playback position slider",
							       TRUE,
							       G_PARAM_READWRITE));
250 251 252 253 254 255 256 257 258 259 260 261
	/**
	 * RBHeader:show-album-art:
	 *
	 * Whether to show the album art display widget.
	 */
	g_object_class_install_property (object_class,
					 PROP_SHOW_ALBUM_ART,
					 g_param_spec_boolean ("show-album-art",
							       "show album art",
							       "whether to show album art",
							       TRUE,
							       G_PARAM_READWRITE));
262

263
	g_type_class_add_private (klass, sizeof (RBHeaderPrivate));
264 265 266
}

static void
267
rb_header_init (RBHeader *header)
268
{
269
	header->priv = G_TYPE_INSTANCE_GET_PRIVATE (header, RB_TYPE_HEADER, RBHeaderPrivate);
270

271 272 273
	gtk_grid_set_column_spacing (GTK_GRID (header), 6);
	gtk_grid_set_column_homogeneous (GTK_GRID (header), TRUE);
	gtk_container_set_border_width (GTK_CONTAINER (header), 3);
274

275
	/* set up position slider */
276
	header->priv->adjustment = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 10.0, 1.0, 10.0, 0.0));
277
	header->priv->scale = gtk_scale_new (GTK_ORIENTATION_HORIZONTAL, header->priv->adjustment);
278
	gtk_widget_set_hexpand (header->priv->scale, TRUE);
279
	g_signal_connect_object (G_OBJECT (header->priv->scale),
280 281
				 "button_press_event",
				 G_CALLBACK (slider_press_callback),
282 283
				 header, 0);
	g_signal_connect_object (G_OBJECT (header->priv->scale),
284 285
				 "button_release_event",
				 G_CALLBACK (slider_release_callback),
286 287
				 header, 0);
	g_signal_connect_object (G_OBJECT (header->priv->scale),
288 289
				 "motion_notify_event",
				 G_CALLBACK (slider_moved_callback),
290 291
				 header, 0);
	g_signal_connect_object (G_OBJECT (header->priv->scale),
292 293
				 "value_changed",
				 G_CALLBACK (slider_changed_callback),
294
				 header, 0);
295 296 297 298
	g_signal_connect_object (G_OBJECT (header->priv->scale),
				 "scroll_event",
				 G_CALLBACK (slider_scroll_callback),
				 header, 0);
299 300
	gtk_scale_set_draw_value (GTK_SCALE (header->priv->scale), FALSE);
	gtk_widget_set_size_request (header->priv->scale, 150, -1);
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334

	/* set up song information labels */
	header->priv->songbox = gtk_grid_new ();
	gtk_widget_set_hexpand (header->priv->songbox, TRUE);
	gtk_widget_set_valign (header->priv->songbox, GTK_ALIGN_CENTER);

	header->priv->song = gtk_label_new (" ");
	gtk_label_set_use_markup (GTK_LABEL (header->priv->song), TRUE);
	gtk_label_set_selectable (GTK_LABEL (header->priv->song), TRUE);
	gtk_label_set_ellipsize (GTK_LABEL (header->priv->song), PANGO_ELLIPSIZE_END);
	gtk_misc_set_alignment (GTK_MISC (header->priv->song), 0.0, 0.5);
	gtk_grid_attach (GTK_GRID (header->priv->songbox), header->priv->song, 0, 0, 1, 1);

	header->priv->details = gtk_label_new ("");
	gtk_label_set_use_markup (GTK_LABEL (header->priv->details), TRUE);
	gtk_label_set_selectable (GTK_LABEL (header->priv->details), TRUE);
	gtk_label_set_ellipsize (GTK_LABEL (header->priv->details), PANGO_ELLIPSIZE_END);
	gtk_widget_set_hexpand (header->priv->details, TRUE);
	gtk_misc_set_alignment (GTK_MISC (header->priv->details), 0.0, 0.5);
	gtk_grid_attach (GTK_GRID (header->priv->songbox), header->priv->details, 0, 1, 1, 2);

	/* elapsed time / duration display */
	header->priv->timelabel = gtk_label_new ("");
	gtk_widget_set_halign (header->priv->timelabel, GTK_ALIGN_END);

	header->priv->timebutton = gtk_button_new ();
	gtk_container_add (GTK_CONTAINER (header->priv->timebutton), header->priv->timelabel);
	g_signal_connect_object (header->priv->timebutton,
				 "clicked",
				 G_CALLBACK (time_button_clicked_cb),
				 header, 0);

	/* image display */
	header->priv->art_store = rb_ext_db_new ("album-art");
335 336 337 338
	g_signal_connect (header->priv->art_store,
			  "added",
			  G_CALLBACK (art_added_cb),
			  header);
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
	header->priv->image = GTK_WIDGET (g_object_new (RB_TYPE_FADING_IMAGE,
							"fallback", RB_STOCK_MISSING_ARTWORK,
							NULL));
	g_signal_connect (header->priv->image,
			  "pixbuf-dropped",
			  G_CALLBACK (pixbuf_dropped_cb),
			  header);
	g_signal_connect (header->priv->image,
			  "uri-dropped",
			  G_CALLBACK (uri_dropped_cb),
			  header);
	g_signal_connect (header->priv->image,
			  "button-press-event",
			  G_CALLBACK (image_button_press_cb),
			  header);

	gtk_grid_attach (GTK_GRID (header), header->priv->image, 0, 0, 1, 1);
	gtk_grid_attach (GTK_GRID (header), header->priv->songbox, 2, 0, 1, 1);
	gtk_grid_attach (GTK_GRID (header), header->priv->timebutton, 3, 0, 1, 1);
	gtk_grid_attach (GTK_GRID (header), header->priv->scale, 4, 0, 1, 1);
359 360 361 362 363

	/* currently, nothing sets this.  it should be set on track changes. */
	header->priv->seekable = TRUE;

	rb_header_sync (header);
364 365
}

366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
static void
rb_header_dispose (GObject *object)
{
	RBHeader *header = RB_HEADER (object);

	if (header->priv->db != NULL) {
		g_object_unref (header->priv->db);
		header->priv->db = NULL;
	}

	if (header->priv->shell_player != NULL) {
		g_object_unref (header->priv->shell_player);
		header->priv->shell_player = NULL;
	}

	if (header->priv->art_store != NULL) {
		g_object_unref (header->priv->art_store);
		header->priv->art_store = NULL;
	}

	G_OBJECT_CLASS (rb_header_parent_class)->dispose (object);
}

389
static void
390
rb_header_finalize (GObject *object)
391
{
392
	RBHeader *header;
393 394

	g_return_if_fail (object != NULL);
395
	g_return_if_fail (RB_IS_HEADER (object));
396

397 398
	header = RB_HEADER (object);
	g_return_if_fail (header->priv != NULL);
399

400 401
	g_free (header->priv->image_path);

402
	G_OBJECT_CLASS (rb_header_parent_class)->finalize (object);
403 404
}

405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
static void
art_cb (RBExtDBKey *key, const char *filename, GValue *data, RBHeader *header)
{
	RhythmDBEntry *entry;

	entry = rb_shell_player_get_playing_entry (header->priv->shell_player);
	if (entry == NULL) {
		return;
	}

	if (rhythmdb_entry_matches_ext_db_key (header->priv->db, entry, key)) {
		GdkPixbuf *pixbuf = NULL;

		if (data != NULL && G_VALUE_HOLDS (data, GDK_TYPE_PIXBUF)) {
			pixbuf = GDK_PIXBUF (g_value_get_object (data));
		}

		rb_fading_image_set_pixbuf (RB_FADING_IMAGE (header->priv->image), pixbuf);

		g_free (header->priv->image_path);
		header->priv->image_path = g_strdup (filename);
	}

	rhythmdb_entry_unref (entry);
}

431 432 433 434 435 436 437
static void
art_added_cb (RBExtDB *db, RBExtDBKey *key, const char *filename, GValue *data, RBHeader *header)
{
	art_cb (key, filename, data, header);
}


438
static void
439
rb_header_playing_song_changed_cb (RBShellPlayer *player, RhythmDBEntry *entry, RBHeader *header)
440 441 442 443 444 445
{
	if (header->priv->entry == entry)
		return;

	header->priv->entry = entry;
	if (header->priv->entry) {
446 447
		RBExtDBKey *key;

448 449
		header->priv->duration = rhythmdb_entry_get_ulong (header->priv->entry,
								   RHYTHMDB_PROP_DURATION);
450 451 452 453 454 455 456 457

		key = rhythmdb_entry_create_ext_db_key (entry, RHYTHMDB_PROP_ALBUM);
		rb_ext_db_request (header->priv->art_store,
				   key,
				   (RBExtDBRequestCallback) art_cb,
				   g_object_ref (header),
				   g_object_unref);
		rb_ext_db_key_free (key);
458 459 460 461
	} else {
		header->priv->duration = 0;
	}

462
	rb_header_sync (header);
463 464 465 466 467 468 469

	g_free (header->priv->image_path);
	header->priv->image_path = NULL;

	rb_fading_image_start (RB_FADING_IMAGE (header->priv->image), 2000);
}

470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
static GtkSizeRequestMode
rb_header_get_request_mode (GtkWidget *widget)
{
	return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
}

static void
rb_header_get_preferred_width (GtkWidget *widget,
			       int *minimum_width,
			       int *natural_width)
{
	*minimum_width = 0;
	*natural_width = 0;
}

485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
static void
rb_header_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
{
	int spacing;
	int scale_width;
	int info_width;
	int time_width;
	int image_width;
	GtkAllocation child_alloc;
	gboolean rtl;

	gtk_widget_set_allocation (widget, allocation);
	spacing = gtk_grid_get_column_spacing (GTK_GRID (widget));
	rtl = (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL);

	/* take some leading space for the image, which we always make square */
501
	if (RB_HEADER (widget)->priv->show_album_art) {
502 503 504 505 506 507 508 509 510 511 512 513 514
		image_width = allocation->height;
		if (rtl) {
			child_alloc.x = allocation->x + allocation->width - image_width;
			allocation->x -= image_width + spacing;
		} else {
			child_alloc.x = allocation->x;
			allocation->x += image_width + spacing;
		}
		allocation->width -= image_width + spacing;
		child_alloc.y = allocation->y;
		child_alloc.width = image_width;
		child_alloc.height = allocation->height;
		gtk_widget_size_allocate (RB_HEADER (widget)->priv->image, &child_alloc);
515 516
	} else {
		image_width = 0;
517 518 519 520 521 522
	}

	/* figure out how much space to allocate to the scale.
	 * it gets at least its minimum size, at most 1/3 of the
	 * space we have.
	 */
523
	if (RB_HEADER (widget)->priv->show_slider) {
524 525 526 527
		gtk_widget_get_preferred_width (RB_HEADER (widget)->priv->scale, &scale_width, NULL);
		if (scale_width < allocation->width / 3)
			scale_width = allocation->width / 3;

528 529 530 531 532 533 534 535 536 537 538 539 540 541
		if (scale_width + image_width > allocation->width)
			scale_width = allocation->width - image_width;

		if (scale_width > 0) {
			if (rtl) {
				child_alloc.x = allocation->x;
			} else {
				child_alloc.x = allocation->x + (allocation->width - scale_width) + spacing;
			}
			child_alloc.y = allocation->y;
			child_alloc.width = scale_width - spacing;
			child_alloc.height = allocation->height;
			gtk_widget_show (RB_HEADER (widget)->priv->scale);
			gtk_widget_size_allocate (RB_HEADER (widget)->priv->scale, &child_alloc);
542
		} else {
543
			gtk_widget_hide (RB_HEADER (widget)->priv->scale);
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
		}
	} else {
		scale_width = 0;
	}

	/* time button gets its minimum size */
	gtk_widget_get_preferred_width (RB_HEADER (widget)->priv->songbox, NULL, &info_width);
	gtk_widget_get_preferred_width (RB_HEADER (widget)->priv->timebutton, &time_width, NULL);

	info_width = allocation->width - (scale_width + time_width) - (2 * spacing);

	if (rtl) {
		child_alloc.x = allocation->x + allocation->width - info_width;
	} else {
		child_alloc.x = allocation->x;
	}

561 562 563 564 565 566 567 568 569 570 571 572 573
	if (info_width > 0) {
		child_alloc.y = allocation->y;
		child_alloc.width = info_width;
		child_alloc.height = allocation->height;
		gtk_widget_show (RB_HEADER (widget)->priv->songbox);
		gtk_widget_size_allocate (RB_HEADER (widget)->priv->songbox, &child_alloc);
	} else {
		gtk_widget_hide (RB_HEADER (widget)->priv->songbox);
		info_width = 0;
	}

	if (info_width + scale_width + (2 * spacing) + time_width > allocation->width) {
		gtk_widget_hide (RB_HEADER (widget)->priv->timebutton);
574
	} else {
575 576 577 578 579 580 581 582 583 584
		if (rtl) {
			child_alloc.x = allocation->x + scale_width + spacing;
		} else {
			child_alloc.x = allocation->x + info_width + spacing;
		}
		child_alloc.y = allocation->y;
		child_alloc.width = time_width;
		child_alloc.height = allocation->height;
		gtk_widget_show (RB_HEADER (widget)->priv->timebutton);
		gtk_widget_size_allocate (RB_HEADER (widget)->priv->timebutton, &child_alloc);
585
	}
586 587
}

588
static void
589
rb_header_set_property (GObject *object,
590 591 592 593
			guint prop_id,
			const GValue *value,
			GParamSpec *pspec)
{
594
	RBHeader *header = RB_HEADER (object);
595 596 597

	switch (prop_id) {
	case PROP_DB:
598
		header->priv->db = g_value_get_object (value);
599 600 601 602
		g_signal_connect_object (header->priv->db,
					 "entry-extra-metadata-notify",
					 G_CALLBACK (rb_header_extra_metadata_cb),
					 header, 0);
603
		break;
604 605
	case PROP_SHELL_PLAYER:
		header->priv->shell_player = g_value_get_object (value);
606 607 608 609 610 611 612 613
		g_signal_connect_object (header->priv->shell_player,
					 "elapsed-nano-changed",
					 G_CALLBACK (rb_header_elapsed_changed_cb),
					 header, 0);
		g_signal_connect_object (header->priv->shell_player,
					 "playing-song-changed",
					 G_CALLBACK (rb_header_playing_song_changed_cb),
					 header, 0);
614
		break;
615 616 617
	case PROP_SEEKABLE:
		header->priv->seekable = g_value_get_boolean (value);
		break;
618 619 620 621 622
	case PROP_SHOW_REMAINING:
		header->priv->show_remaining = g_value_get_boolean (value);
		rb_header_update_elapsed (header);
		break;
	case PROP_SHOW_POSITION_SLIDER:
623 624
		header->priv->show_slider = g_value_get_boolean (value);
		gtk_widget_set_visible (header->priv->scale, header->priv->show_slider);
625 626
		break;
	case PROP_SHOW_ALBUM_ART:
627 628
		header->priv->show_album_art = g_value_get_boolean (value);
		gtk_widget_set_visible (header->priv->image, header->priv->show_album_art);
629
		break;
630 631 632 633 634 635
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

636
static void
637
rb_header_get_property (GObject *object,
638 639 640 641
			guint prop_id,
			GValue *value,
			GParamSpec *pspec)
{
642
	RBHeader *header = RB_HEADER (object);
643 644 645

	switch (prop_id) {
	case PROP_DB:
646
		g_value_set_object (value, header->priv->db);
647
		break;
648 649
	case PROP_SHELL_PLAYER:
		g_value_set_object (value, header->priv->shell_player);
650
		break;
651 652 653
	case PROP_SEEKABLE:
		g_value_set_boolean (value, header->priv->seekable);
		break;
654 655 656
	case PROP_SLIDER_DRAGGING:
		g_value_set_boolean (value, header->priv->slider_dragging);
		break;
657 658 659 660
	case PROP_SHOW_REMAINING:
		g_value_set_boolean (value, header->priv->show_remaining);
		break;
	case PROP_SHOW_POSITION_SLIDER:
661
		g_value_set_boolean (value, header->priv->show_slider);
662 663
		break;
	case PROP_SHOW_ALBUM_ART:
664
		g_value_set_boolean (value, header->priv->show_album_art);
665
		break;
666 667 668 669 670 671
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

672 673 674 675 676
/**
 * rb_header_new:
 * @shell_player: the #RBShellPlayer instance
 * @db: the #RhythmDB instance
 *
Jonathan Matthew's avatar
Jonathan Matthew committed
677 678
 * Creates a new header widget.
 *
679 680
 * Return value: the header widget
 */
681
RBHeader *
682
rb_header_new (RBShellPlayer *shell_player, RhythmDB *db)
683
{
684
	RBHeader *header;
685

686 687
	header = RB_HEADER (g_object_new (RB_TYPE_HEADER,
					  "shell-player", shell_player,
688
					  "db", db,
689
					  NULL));
690

691
	g_return_val_if_fail (header->priv != NULL, NULL);
692

693
	return header;
694 695
}

696 697 698 699 700 701 702 703 704 705 706 707 708 709 710
static void
get_extra_metadata (RhythmDB *db, RhythmDBEntry *entry, const char *field, char **value)
{
	GValue *v;

	v = rhythmdb_entry_request_extra_metadata (db, entry, field);
	if (v != NULL) {
		*value = g_value_dup_string (v);
		g_value_unset (v);
		g_free (v);
	} else {
		*value = NULL;
	}
}

711
static void
712
rb_header_sync (RBHeader *header)
713
{
714
	char *label_text;
715
	const char *location = "<null>";
716

717 718 719 720
	if (header->priv->entry != NULL) {
		location = rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_LOCATION);
	}
	rb_debug ("syncing with entry = %s", location);
721

722
	if (header->priv->entry != NULL) {
723
		const char *title;
724
		const char *album;
725
		const char *artist;
726
		const char *stream_name = NULL;
727 728 729
		char *streaming_title;
		char *streaming_artist;
		char *streaming_album;
730
		PangoDirection widget_dir;
731 732

		gboolean have_duration = (header->priv->duration > 0);
733

734
		title = rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_TITLE);
735 736
		album = rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_ALBUM);
		artist = rhythmdb_entry_get_string (header->priv->entry, RHYTHMDB_PROP_ARTIST);
737

738 739 740 741 742
		get_extra_metadata (header->priv->db,
				    header->priv->entry,
				    RHYTHMDB_PROP_STREAM_SONG_TITLE,
				    &streaming_title);
		if (streaming_title) {
743 744 745
			/* use entry title as stream name */
			stream_name = title;
			title = streaming_title;
746 747
		}

748 749 750 751 752
		get_extra_metadata (header->priv->db,
				    header->priv->entry,
				    RHYTHMDB_PROP_STREAM_SONG_ARTIST,
				    &streaming_artist);
		if (streaming_artist) {
753 754 755 756
			/* override artist from entry */
			artist = streaming_artist;
		}

757 758 759 760 761 762 763 764 765
		get_extra_metadata (header->priv->db,
				    header->priv->entry,
				    RHYTHMDB_PROP_STREAM_SONG_ALBUM,
				    &streaming_album);
		if (streaming_album) {
			/* override album from entry */
			album = streaming_album;
		}

766 767
		widget_dir = (gtk_widget_get_direction (GTK_WIDGET (header->priv->song)) == GTK_TEXT_DIR_LTR) ?
			     PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL;
768

769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811
		char *t;
		t = rb_text_cat (widget_dir, title, TITLE_FORMAT, NULL);
		gtk_label_set_markup (GTK_LABEL (header->priv->song), t);
		g_free (t);

		if (artist == NULL || artist[0] == '\0') {	/* this is crap; should be up to the entry type */
			if (stream_name != NULL) {
				t = rb_text_cat (widget_dir, stream_name, STREAM_FORMAT, NULL);
				gtk_label_set_markup (GTK_LABEL (header->priv->details), t);
				g_free (t);
			} else {
				gtk_label_set_markup (GTK_LABEL (header->priv->details), "");
			}
		} else {
			const char *by;
			const char *from;
			PangoDirection dir;
			PangoDirection native;

			native = PANGO_DIRECTION_LTR;
			if (gtk_widget_get_direction (GTK_WIDGET (header->priv->details)) != GTK_TEXT_DIR_LTR) {
				native = PANGO_DIRECTION_RTL;
			}

			dir = rb_text_common_direction (artist, album, NULL);
			if (!rb_text_direction_conflict (dir, native)) {
				dir = native;
				by = _("by");
				from = _("from");
			} else {
				by = UNICODE_MIDDLE_DOT;
				from = UNICODE_MIDDLE_DOT;
			}

			t = rb_text_cat (dir,
					 by, "%s",
					 artist, ARTIST_FORMAT,
					 from, "%s",
					 album, ALBUM_FORMAT,
					 NULL);
			gtk_label_set_markup (GTK_LABEL (header->priv->details), t);
			g_free (t);
		}
812

813 814
		gtk_widget_set_sensitive (header->priv->scale, have_duration && header->priv->seekable);
		rb_header_sync_time (header);
815 816 817 818

		g_free (streaming_artist);
		g_free (streaming_album);
		g_free (streaming_title);
819 820
	} else {
		rb_debug ("not playing");
821
		label_text = g_markup_printf_escaped (TITLE_FORMAT, _("Not Playing"));
822
		gtk_label_set_markup (GTK_LABEL (header->priv->song), label_text);
823
		g_free (label_text);
824

825
		gtk_label_set_text (GTK_LABEL (header->priv->details), "");
826

827
		rb_header_sync_time (header);
828
	}
829 830
}

831
static void
832
rb_header_sync_time (RBHeader *header)
833
{
834
	if (header->priv->shell_player == NULL)
835
		return;
836

837
	if (header->priv->slider_dragging == TRUE) {
838
		rb_debug ("slider is dragging, not syncing");
839
		return;
840
	}
841

842
	if (header->priv->duration > 0) {
843
		double progress = ((double) header->priv->elapsed_time) / RB_PLAYER_SECOND;
844

845
		header->priv->slider_locked = TRUE;
846 847

		g_object_freeze_notify (G_OBJECT (header->priv->adjustment));
848
		gtk_adjustment_set_value (header->priv->adjustment, progress);
849 850 851
		gtk_adjustment_set_upper (header->priv->adjustment, header->priv->duration);
		g_object_thaw_notify (G_OBJECT (header->priv->adjustment));

852
		header->priv->slider_locked = FALSE;
853
		gtk_widget_set_sensitive (header->priv->scale, header->priv->seekable);
854
	} else {
855
		header->priv->slider_locked = TRUE;
856 857

		g_object_freeze_notify (G_OBJECT (header->priv->adjustment));
858
		gtk_adjustment_set_value (header->priv->adjustment, 0.0);
859 860 861
		gtk_adjustment_set_upper (header->priv->adjustment, 0.0);
		g_object_thaw_notify (G_OBJECT (header->priv->adjustment));

862 863
		header->priv->slider_locked = FALSE;
		gtk_widget_set_sensitive (header->priv->scale, FALSE);
864 865
	}

866
	rb_header_update_elapsed (header);
867 868 869 870 871
}

static gboolean
slider_press_callback (GtkWidget *widget,
		       GdkEventButton *event,
872
		       RBHeader *header)
873
{
874 875
	header->priv->slider_dragging = TRUE;
	header->priv->latest_set_time = -1;
876 877
	g_object_notify (G_OBJECT (header), "slider-dragging");

878
#if !GTK_CHECK_VERSION(3,5,0)
879 880 881 882 883 884
	/* HACK: we want the behaviour you get with the middle button, so we
	 * mangle the event.  clicking with other buttons moves the slider in
	 * step increments, clicking with the middle button moves the slider to
	 * the location of the click.
	 */
	event->button = 2;
885
#endif
886 887


888 889 890 891
	return FALSE;
}

static gboolean
892
slider_moved_timeout (RBHeader *header)
893
{
894
	GDK_THREADS_ENTER ();
895

896
	apply_slider_position (header);
897
	header->priv->slider_moved_timeout = 0;
898

899
	GDK_THREADS_LEAVE ();
900 901 902 903 904 905 906

	return FALSE;
}

static gboolean
slider_moved_callback (GtkWidget *widget,
		       GdkEventMotion *event,
907
		       RBHeader *header)
908 909 910
{
	double progress;

911
	if (header->priv->slider_dragging == FALSE) {
912
		rb_debug ("slider is not dragging");
913
		return FALSE;
914
	}
915

916
	progress = gtk_adjustment_get_value (header->priv->adjustment);
917
	header->priv->elapsed_time = (gint64) ((progress+0.5) * RB_PLAYER_SECOND);
918

919
	rb_header_update_elapsed (header);
920

921
	if (header->priv->slider_moved_timeout != 0) {
922
		rb_debug ("removing old timer");
923 924
		g_source_remove (header->priv->slider_moved_timeout);
		header->priv->slider_moved_timeout = 0;
925
	}
926 927
	header->priv->slider_moved_timeout =
		g_timeout_add (40, (GSourceFunc) slider_moved_timeout, header);
928

929 930 931
	return FALSE;
}

932 933
static void
apply_slider_position (RBHeader *header)
934 935
{
	double progress;
936
	long new;
937

938
	progress = gtk_adjustment_get_value (header->priv->adjustment);
939
	new = (long) (progress+0.5);
940

941
	if (new != header->priv->latest_set_time) {
942
		rb_debug ("setting time to %ld", new);
943
		rb_shell_player_set_playing_time (header->priv->shell_player, new, NULL);
944
		header->priv->latest_set_time = new;
945
	}
946 947 948
}

static gboolean
949 950 951
slider_release_callback (GtkWidget *widget,
			 GdkEventButton *event,
			 RBHeader *header)
952
{
953
#if !GTK_CHECK_VERSION(3,5,0)
954 955
	/* HACK: see slider_press_callback */
	event->button = 2;
956
#endif
957

958 959 960 961
	if (header->priv->slider_dragging == FALSE) {
		rb_debug ("slider is not dragging");
		return FALSE;
	}
962

963 964 965 966
	if (header->priv->slider_moved_timeout != 0) {
		g_source_remove (header->priv->slider_moved_timeout);
		header->priv->slider_moved_timeout = 0;
	}
967

968 969 970
	apply_slider_position (header);
	header->priv->slider_dragging = FALSE;
	g_object_notify (G_OBJECT (header), "slider-dragging");
971 972 973 974 975
	return FALSE;
}

static void
slider_changed_callback (GtkWidget *widget,
976
		         RBHeader *header)
977
{
978 979 980 981
	/* if the slider isn't being dragged, and nothing else is happening,
	 * this indicates the position was adjusted with a keypress (page up/page down etc.),
	 * so we should directly apply the change.
	 */
982
	if (header->priv->slider_dragging == FALSE &&
983 984
	    header->priv->slider_locked == FALSE) {
		apply_slider_position (header);
985 986 987
	}
}

988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012
static gboolean
slider_scroll_callback (GtkWidget *widget, GdkEventScroll *event, RBHeader *header)
{
	gboolean retval = TRUE;
	gdouble adj = gtk_adjustment_get_value (header->priv->adjustment);

	switch (event->direction) {
	case GDK_SCROLL_UP:
		rb_debug ("slider scrolling up");
		gtk_adjustment_set_value (header->priv->adjustment, adj + SCROLL_UP_SEEK_OFFSET);
		break;

	case GDK_SCROLL_DOWN:
		rb_debug ("slider scrolling down");
		gtk_adjustment_set_value (header->priv->adjustment, adj + SCROLL_DOWN_SEEK_OFFSET);
		break;

	default:
		retval = FALSE;
		break;
	}

	return retval;
}

1013
static void
1014
rb_header_update_elapsed (RBHeader *header)
1015
{
1016
	long seconds;
1017 1018 1019
	char *elapsed;
	char *duration;
	char *label;
1020

1021 1022
	if (header->priv->entry == NULL) {
		gtk_label_set_text (GTK_LABEL (header->priv->timelabel), "");
1023
		return;
1024
	}
1025

1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
	seconds = header->priv->elapsed_time / RB_PLAYER_SECOND;
	if (header->priv->duration == 0) {
		label = rb_make_time_string (seconds);
		gtk_label_set_text (GTK_LABEL (header->priv->timelabel), label);
		g_free (label);
	} else if (header->priv->show_remaining) {

		duration = rb_make_time_string (header->priv->duration);

		if (seconds > header->priv->duration) {
			elapsed = rb_make_time_string (0);
		} else {
			elapsed = rb_make_time_string (header->priv->duration - seconds);
		}
1040

1041 1042 1043 1044 1045 1046 1047
		/* Translators: remaining time / total time */
		label = g_strdup_printf (_("-%s / %s"), elapsed, duration);
		gtk_label_set_text (GTK_LABEL (header->priv->timelabel), label);

		g_free (elapsed);
		g_free (duration);
		g_free (label);
1048
	} else {
1049 1050
		elapsed = rb_make_time_string (seconds);
		duration = rb_make_time_string (header->priv->duration);