hythmbox-2.98/widgets/rb-header.c

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 }