Index: gtk/gtk.h
===================================================================
--- gtk/gtk.h	(revision 17411)
+++ gtk/gtk.h	(working copy)
@@ -195,6 +195,7 @@
 #include <gtk/gtkvbox.h>
 #include <gtk/gtkversion.h>
 #include <gtk/gtkviewport.h>
+#include <gtk/gtkscalebutton.h>
 #include <gtk/gtkvpaned.h>
 #include <gtk/gtkvruler.h>
 #include <gtk/gtkvscale.h>
Index: gtk/gtk.symbols
===================================================================
--- gtk/gtk.symbols	(revision 17411)
+++ gtk/gtk.symbols	(working copy)
@@ -4430,6 +4430,16 @@
 #endif
 #endif
 
+#if IN_HEADER(__GTK_SCALE_BUTTON_H__)
+#if IN_FILE(__GTK_SCALE_BUTTON_C__)
+gtk_scale_button_get_type G_GNUC_CONST
+gtk_scale_button_get_value
+gtk_scale_button_new
+gtk_scale_button_set_icons
+gtk_scale_button_set_value
+#endif
+#endif
+
 #if IN_HEADER(__GTK_VPANED_H__)
 #if IN_FILE(__GTK_VPANED_C__)
 gtk_vpaned_get_type G_GNUC_CONST
Index: gtk/Makefile.am
===================================================================
--- gtk/Makefile.am	(revision 17411)
+++ gtk/Makefile.am	(working copy)
@@ -310,6 +310,7 @@
 	gtkvbbox.h		\
 	gtkvbox.h		\
 	gtkviewport.h		\
+	gtkscalebutton.h	\
 	gtkvpaned.h		\
 	gtkvruler.h		\
 	gtkvscale.h		\
@@ -580,6 +581,7 @@
 	gtkvbbox.c		\
 	gtkvbox.c		\
 	gtkviewport.c		\
+	gtkscalebutton.c	\
 	gtkvpaned.c		\
 	gtkvruler.c		\
 	gtkvscale.c		\
Index: gtk/gtkscalebutton.c
===================================================================
--- gtk/gtkscalebutton.c	(revision 0)
+++ gtk/gtkscalebutton.c	(revision 0)
@@ -0,0 +1,916 @@
+/* Volume Button / popup widget
+ * (c) copyright 2005 Ronald S. Bultje <rbultje@ronald.bitfreak.net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+#define _GNU_SOURCE
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "gtkmain.h"
+#include "gtkintl.h"
+#include "gtkrange.h"
+#include "gtkscale.h"
+#include "gtkvscale.h"
+#include "gtkframe.h"
+#include "gtkvbox.h"
+#include "gtkwindow.h"
+#include "gtkmarshalers.h"
+#include "gtkstock.h"
+
+#include <gdk/gdkkeysyms.h>
+#include "gtkscalebutton.h"
+
+#define SCALE_SIZE 100
+#define CLICK_TIMEOUT 250
+
+enum {
+  VALUE_CHANGED,
+  LAST_SIGNAL
+};
+
+enum {
+  PROP_0,
+  PROP_VALUE,
+  PROP_SIZE,
+  PROP_ADJUSTMENT
+};
+
+struct GtkScaleButton {
+  GtkButton parent;
+
+  /* popup */
+  GtkWidget *dock, *scale, *image, *plus, *min;
+  GtkIconSize size;
+  guint click_id;
+  gdouble direction;
+  guint32 pop_time;
+  GList *icon_list;
+  guint timeout : 1;
+};
+
+static void	gtk_scale_button_class_init	(GtkScaleButtonClass * klass);
+static void	gtk_scale_button_init	(GtkScaleButton * button);
+static void	gtk_scale_button_dispose	(GObject        * object);
+static void	gtk_scale_button_set_property	(GObject      *object,
+						 guint         prop_id,
+						 const GValue *value,
+						 GParamSpec   *pspec);
+static void	gtk_scale_button_get_property	(GObject     *object,
+						 guint        prop_id,
+						 GValue      *value,
+						 GParamSpec  *pspec);
+static gboolean	gtk_scale_button_scroll	(GtkWidget      *widget,
+						 GdkEventScroll *event);
+static gboolean	gtk_scale_button_press		(GtkWidget      *widget,
+						 GdkEventButton *event);
+static gboolean gtk_scale_button_key_release	(GtkWidget      *widget,
+    						 GdkEventKey    *event);
+static gboolean cb_dock_button_press		(GtkWidget      *widget,
+						 GdkEventButton *event,
+						 gpointer         data);
+static gboolean cb_dock_key_release		(GtkWidget      *widget,
+						 GdkEventKey    *event,
+						 gpointer        data);
+static gboolean cb_dock_key_press		(GtkWidget      *widget,
+						 GdkEventKey    *event,
+						 gpointer        data);
+
+static gboolean cb_button_press			(GtkWidget      *widget,
+						 GdkEventButton *event,
+						 gpointer        data);
+static gboolean cb_button_release		(GtkWidget      *widget,
+						 GdkEventButton *event,
+						 gpointer        data);
+static void     gtk_scale_button_update_icon (GtkScaleButton *button);
+static void	gtk_scale_button_scale_value_changed(GtkRange *range);
+
+/* see below for scale definitions */
+static GtkWidget *gtk_scale_button_scale_new	(GtkScaleButton * button,
+						 gdouble min,
+						 gdouble max,
+						 gdouble step);
+
+static GtkButtonClass *parent_class = NULL;
+static guint signals[LAST_SIGNAL] = { 0 };
+
+G_DEFINE_TYPE (GtkScaleButton, gtk_scale_button, GTK_TYPE_BUTTON)
+
+static void
+gtk_scale_button_class_init (GtkScaleButtonClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  parent_class = g_type_class_peek_parent (klass);
+
+  object_class->dispose = gtk_scale_button_dispose;
+  object_class->set_property = gtk_scale_button_set_property;
+  object_class->get_property = gtk_scale_button_get_property;
+  widget_class->button_press_event = gtk_scale_button_press;
+  widget_class->key_release_event = gtk_scale_button_key_release;
+  widget_class->scroll_event = gtk_scale_button_scroll;
+
+  g_object_class_install_property (object_class,
+				   PROP_VALUE,
+				   g_param_spec_double ("value",
+							P_("Value"),
+							P_("The value of the scale"),
+							-G_MAXDOUBLE,
+							G_MAXDOUBLE,
+							0,
+							G_PARAM_READWRITE));
+
+  g_object_class_install_property (object_class,
+				   PROP_SIZE,
+				   g_param_spec_enum ("size",
+						      P_("Icon size"),
+						      P_("The icon size"),
+						      GTK_TYPE_ICON_SIZE,
+						      GTK_ICON_SIZE_SMALL_TOOLBAR,
+						      G_PARAM_READWRITE));
+
+  g_object_class_install_property (object_class,
+                                   PROP_ADJUSTMENT,
+                                   g_param_spec_object ("adjustment",
+							P_("Adjustment"),
+							P_("The GtkAdjustment that contains the current value of this scale button object"),
+                                                        GTK_TYPE_ADJUSTMENT,
+                                                        G_PARAM_READWRITE));
+
+  /**
+   * GtkScaleButton::value-changed:
+   * @button: the object that received the signal
+   *
+   * Emitted when the value field has changed.
+   */
+  signals[VALUE_CHANGED] =
+    g_signal_new (I_("value-changed"),
+		  G_TYPE_FROM_CLASS (klass),
+		  G_SIGNAL_RUN_LAST,
+		  G_STRUCT_OFFSET (GtkScaleButtonClass, value_changed),
+		  NULL, NULL,
+		  _gtk_marshal_VOID__DOUBLE,
+		  G_TYPE_NONE, 1, G_TYPE_DOUBLE);
+}
+
+static void
+gtk_scale_button_init (GtkScaleButton *button)
+{
+  GtkWidget *frame, *box;
+
+  button->timeout = FALSE;
+  button->click_id = 0;
+  button->dock = button->scale = NULL;
+
+  gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
+  gtk_button_set_focus_on_click (GTK_BUTTON (button), FALSE);
+
+  /* image */
+  button->image = gtk_image_new ();
+  gtk_container_add (GTK_CONTAINER (button), button->image);
+  gtk_widget_show_all (button->image);
+
+  /* window */
+  button->dock = gtk_window_new (GTK_WINDOW_POPUP);
+  g_signal_connect (button->dock, "button-press-event",
+		    G_CALLBACK (cb_dock_button_press), button);
+  g_signal_connect (button->dock, "key-release-event",
+		    G_CALLBACK (cb_dock_key_release), button);
+  g_signal_connect (button->dock, "key-press-event",
+		    G_CALLBACK (cb_dock_key_press), button);
+  gtk_window_set_decorated (GTK_WINDOW (button->dock), FALSE);
+
+  /* frame */
+  frame = gtk_frame_new (NULL);
+  gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_OUT);
+  gtk_container_add (GTK_CONTAINER (button->dock), frame);
+  box = gtk_vbox_new (FALSE, 0);
+  gtk_container_add (GTK_CONTAINER (frame), box);
+
+  /* + */
+  button->plus = gtk_button_new_with_label ("+");
+  gtk_button_set_relief (GTK_BUTTON (button->plus), GTK_RELIEF_NONE);
+  g_signal_connect (button->plus, "button-press-event",
+		    G_CALLBACK (cb_button_press), button);
+  g_signal_connect (button->plus, "button-release-event",
+		    G_CALLBACK (cb_button_release), button);
+  gtk_box_pack_start (GTK_BOX (box), button->plus, TRUE, FALSE, 0);
+
+  /* scale */
+  button->scale = gtk_scale_button_scale_new (button, 0., 100., 2.);
+  gtk_widget_set_size_request (button->scale, -1, SCALE_SIZE);
+  gtk_scale_set_draw_value (GTK_SCALE (button->scale), FALSE);
+  gtk_range_set_inverted (GTK_RANGE (button->scale), TRUE);
+  gtk_box_pack_start (GTK_BOX (box), button->scale, TRUE, FALSE, 0);
+
+  /* - */
+  button->min = gtk_button_new_with_label ("-");
+  gtk_button_set_relief (GTK_BUTTON (button->min), GTK_RELIEF_NONE);
+  g_signal_connect (button->min, "button-press-event",
+		   G_CALLBACK (cb_button_press), button);
+  g_signal_connect (button->min, "button-release-event",
+		    G_CALLBACK (cb_button_release), button);
+  gtk_box_pack_start (GTK_BOX (box), button->min, TRUE, FALSE, 0);
+
+  /* set button text and size */
+  button->size = GTK_ICON_SIZE_SMALL_TOOLBAR;
+  gtk_scale_button_update_icon (button);
+}
+
+static void
+gtk_scale_button_set_property (GObject      *object,
+				guint         prop_id,
+				const GValue *value,
+				GParamSpec   *pspec)
+{
+  GtkScaleButton *button;
+
+  button = GTK_SCALE_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_VALUE:
+      gtk_scale_button_set_value (button, g_value_get_double (value));
+      break;
+    case PROP_SIZE:
+      {
+	GtkIconSize size;
+	size = g_value_get_enum (value);
+	if (button->size != size)
+	  {
+	    button->size = size;
+	    gtk_scale_button_update_icon (button);
+	  }
+      }
+    case PROP_ADJUSTMENT:
+      gtk_range_set_adjustment (GTK_RANGE (button->scale), g_value_get_object (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void 
+gtk_scale_button_get_property (GObject     *object,
+				guint        prop_id,
+				GValue      *value,
+				GParamSpec  *pspec)
+{
+  GtkScaleButton *button;
+
+  button = GTK_SCALE_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_VALUE:
+      g_value_set_float (value, gtk_scale_button_get_value (button));
+      break;
+    case PROP_SIZE:
+      g_value_set_enum (value, button->size);
+      break;
+    case PROP_ADJUSTMENT:
+      g_value_set_object (value, GTK_RANGE (button->scale)->adjustment);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_scale_button_dispose (GObject *object)
+{
+  GtkScaleButton *button = GTK_SCALE_BUTTON (object);
+  GList *l;
+
+  if (button->dock) {
+    gtk_widget_destroy (button->dock);
+    button->dock = NULL;
+  }
+
+  if (button->click_id != 0) {
+    g_source_remove (button->click_id);
+    button->click_id = 0;
+  }
+  for (l = button->icon_list; l != NULL; l = l->next) {
+    g_free (l->data);
+  }
+  g_list_free (button->icon_list);
+  button->icon_list = NULL;
+
+  G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+/**
+ * gtk_scale_button_new
+ * @size: a stock icon size
+ * @lower: the minimum value of the scale (usually 0)
+ * @upper: the maximum value of the scale (usually 100)
+ * @step_increment: the stepping of value when a scroll-wheel event,
+ * or up/down arrow event occurs (usually 2)
+ * @icons: a NULL-terminated array of stock icon names, or NULL if
+ * you want to set the list later with #gtk_scale_button_set_icons. See
+ * #gtk_scale_button_set_icons for details.
+ *
+ * Creates a #GtkScaleButton, with a range between lower and upper, with
+ * a stepping of step_increment.
+ *
+ * Return value: a new #GtkScaleButton
+ */
+GtkWidget *
+gtk_scale_button_new (GtkIconSize size,
+		       gdouble lower,
+		       gdouble upper,
+		       gdouble step_increment,
+		       const char **icons)
+{
+  GtkScaleButton *button;
+
+  button = g_object_new (GTK_TYPE_SCALE_BUTTON, NULL);
+
+  if (icons != NULL)
+    gtk_scale_button_set_icons (button, icons);
+
+  gtk_range_set_range (GTK_RANGE (button->scale), lower, upper);
+  gtk_range_set_increments (GTK_RANGE (button->scale),
+			    step_increment,
+			    10 * step_increment);
+
+  if (button->size != size)
+    {
+      button->size = size;
+      gtk_scale_button_update_icon (button);
+    }
+
+  return GTK_WIDGET (button);
+}
+
+gdouble
+gtk_scale_button_get_value (GtkScaleButton * button)
+{
+  g_return_val_if_fail (button != NULL, 0);
+
+  return gtk_range_get_value (GTK_RANGE (button->scale));
+}
+
+void
+gtk_scale_button_set_value (GtkScaleButton * button,
+			       gdouble value)
+{
+  g_return_if_fail (button != NULL);
+
+  gtk_range_set_value (GTK_RANGE (button->scale), value);
+}
+
+void
+gtk_scale_button_set_icons (GtkScaleButton *button,
+			    const char **icons)
+{
+  GList *l;
+  guint i, num_icons;
+
+  /* Count the number of icons passed */
+  for (num_icons = 0; icons && icons[num_icons] != NULL; num_icons++) {}
+  g_return_if_fail (num_icons >= 4);
+
+  /* Empty the original list */
+  for (l = button->icon_list; l != NULL; l = l->next)
+    g_free (l->data);
+  g_list_free (button->icon_list);
+  button->icon_list = NULL;
+
+  for (i = 0; icons[i] != NULL; i++)
+    {
+      button->icon_list = g_list_prepend (button->icon_list,
+					  g_strdup (icons[i]));
+    }
+  button->icon_list = g_list_reverse (button->icon_list);
+  gtk_scale_button_update_icon (button);
+}
+
+/*
+ * button callbacks.
+ */
+
+static gboolean
+gtk_scale_button_scroll (GtkWidget      * widget,
+			    GdkEventScroll * event)
+{
+  GtkScaleButton *button = GTK_SCALE_BUTTON (widget);
+  GtkAdjustment *adj = gtk_range_get_adjustment (GTK_RANGE (button->scale));
+  gdouble d;
+
+  if (event->type != GDK_SCROLL)
+    return FALSE;
+
+  d = gtk_scale_button_get_value (button);
+  if (event->direction == GDK_SCROLL_UP) {
+    d += adj->step_increment;
+    if (d > adj->upper)
+      d = adj->upper;
+  } else {
+    d -= adj->step_increment;
+    if (d < adj->lower)
+      d = adj->lower;
+  }
+  gtk_scale_button_set_value (button, d);
+
+  return TRUE;
+}
+
+static gboolean
+gtk_scale_button_press (GtkWidget      * widget,
+			 GdkEventButton * event)
+{
+  GtkScaleButton *button = GTK_SCALE_BUTTON (widget);
+  GtkAdjustment *adj = gtk_range_get_adjustment (GTK_RANGE (button->scale));
+  gint x, y, m, dx, dy, sx, sy, ystartoff, mouse_y, height;
+  gdouble v;
+  GdkEventButton *e;
+  GdkDisplay *display;
+  GdkScreen *screen;
+
+  display = gtk_widget_get_display (widget);
+  screen = gtk_widget_get_screen (widget);
+
+  /* position roughly */
+  gtk_window_set_screen (GTK_WINDOW (button->dock), screen);
+
+  gdk_window_get_origin (widget->window, &x, &y);
+  x += widget->allocation.x;
+  y += widget->allocation.y;
+  gtk_window_move (GTK_WINDOW (button->dock), x, y - (SCALE_SIZE / 2));
+  gtk_widget_show_all (button->dock);
+  gdk_window_get_origin (button->dock->window, &dx, &dy);
+  dy += button->dock->allocation.y;
+  gdk_window_get_origin (button->scale->window, &sx, &sy);
+  sy += button->scale->allocation.y;
+  ystartoff = sy - dy;
+  mouse_y = event->y;
+  button->timeout = TRUE;
+
+  /* position (needs widget to be shown already) */
+  height = gdk_screen_get_height (screen);
+  v = gtk_scale_button_get_value (button) / (adj->upper - adj->lower);
+  x += (widget->allocation.width - button->dock->allocation.width) / 2;
+  y -= ystartoff;
+  y -= GTK_RANGE (button->scale)->min_slider_size / 2;
+  m = button->scale->allocation.height -
+      GTK_RANGE (button->scale)->min_slider_size;
+  y -= m * (1.0 - v);
+  y += mouse_y;
+  /* Make sure the widget stays in the screen */
+  if (y < 0)
+    y = 0;
+  if (y + widget->allocation.height > height)
+    y = height - widget->allocation.height;
+  gtk_window_move (GTK_WINDOW (button->dock), x, y);
+  gdk_window_get_origin (button->scale->window, &sx, &sy);
+
+  GTK_WIDGET_CLASS (parent_class)->button_press_event (widget, event);
+
+  /* grab focus */
+  gtk_grab_add (button->dock);
+
+  if (gdk_pointer_grab (button->dock->window, TRUE,
+			GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
+			GDK_POINTER_MOTION_MASK, NULL, NULL, event->time)
+      != GDK_GRAB_SUCCESS) {
+	  gtk_grab_remove (button->dock);
+	  gtk_widget_hide (button->dock);
+	  return FALSE;
+   }
+
+  if (gdk_keyboard_grab (button->dock->window, TRUE, event->time) != GDK_GRAB_SUCCESS) {
+	  gdk_display_pointer_ungrab (display, event->time);
+	  gtk_grab_remove (button->dock);
+	  gtk_widget_hide (button->dock);
+	  return FALSE;
+  }
+
+  gtk_widget_grab_focus (button->dock);
+
+  /* forward event to the slider */
+  e = (GdkEventButton *) gdk_event_copy ((GdkEvent *) event);
+  e->window = button->scale->window;
+
+  /* position: the X position isn't relevant, halfway will work just fine.
+   * The vertical position should be *exactly* in the middle of the slider
+   * of the scale; if we don't do that correctly, it'll move from its current
+   * position, which means a position change on-click, which is bad. */
+  e->x = button->scale->allocation.width / 2;
+  m = button->scale->allocation.height -
+      GTK_RANGE (button->scale)->min_slider_size;
+  e->y = ((1.0 - v) * m) + GTK_RANGE (button->scale)->min_slider_size / 2;
+  gtk_widget_event (button->scale, (GdkEvent *) e);
+  e->window = event->window;
+  gdk_event_free ((GdkEvent *) e);
+
+  button->pop_time = event->time;
+
+  return TRUE;
+}
+
+static gboolean
+gtk_scale_button_key_release (GtkWidget      * widget,
+    			  GdkEventKey    * event)
+{
+  GtkScaleButton *button = GTK_SCALE_BUTTON (widget);
+  GtkAdjustment *adj = gtk_range_get_adjustment (GTK_RANGE (button->scale));
+  gint x, y, m, dx, dy, sx, sy, ystartoff;
+  gdouble v;
+  GdkDisplay *display;
+  GdkScreen *screen;
+
+  if (event->keyval != GDK_space && event->keyval != GDK_Return)
+    return FALSE;
+
+  display = gtk_widget_get_display (widget);
+  screen = gtk_widget_get_screen (widget);
+
+  /* position roughly */
+  gtk_window_set_screen (GTK_WINDOW (button->dock), screen);
+
+  gdk_window_get_origin (widget->window, &x, &y);
+  x += widget->allocation.x;
+  y += widget->allocation.y;
+  gtk_window_move (GTK_WINDOW (button->dock), x, y - (SCALE_SIZE / 2));
+  gtk_widget_show_all (button->dock);
+  gdk_window_get_origin (button->dock->window, &dx, &dy);
+  dy += button->dock->allocation.y;
+  gdk_window_get_origin (button->scale->window, &sx, &sy);
+  sy += button->scale->allocation.y;
+  ystartoff = sy - dy;
+  button->timeout = TRUE;
+
+  /* position (needs widget to be shown already) */
+  v = gtk_scale_button_get_value (button) / (adj->upper - adj->lower);
+  x += (widget->allocation.width - button->dock->allocation.width) / 2;
+  y -= ystartoff;
+  y -= GTK_RANGE (button->scale)->min_slider_size / 2;
+  m = button->scale->allocation.height -
+      GTK_RANGE (button->scale)->min_slider_size;
+  y -= m * (1.0 - v);
+  gtk_window_move (GTK_WINDOW (button->dock), x, y);
+  gdk_window_get_origin (button->scale->window, &sx, &sy);
+
+  /* grab focus */
+  gtk_grab_add (button->dock);
+
+  if (gdk_pointer_grab (button->dock->window, TRUE,
+			GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK,
+			NULL, NULL, event->time)
+      != GDK_GRAB_SUCCESS) {
+	  gtk_grab_remove (button->dock);
+	  gtk_widget_hide (button->dock);
+	  return FALSE;
+   }
+
+  if (gdk_keyboard_grab (button->dock->window, TRUE, event->time) != GDK_GRAB_SUCCESS) {
+	  gdk_display_pointer_ungrab (display, event->time);
+	  gtk_grab_remove (button->dock);
+	  gtk_widget_hide (button->dock);
+	  return FALSE;
+  }
+
+  gtk_widget_grab_focus (button->scale);
+
+  button->pop_time = event->time;
+
+  return TRUE;
+}
+
+/*
+ * +/- button callbacks.
+ */
+
+static gboolean
+cb_button_timeout (gpointer data)
+{
+  GtkScaleButton *button = GTK_SCALE_BUTTON (data);
+  GtkAdjustment *adj = gtk_range_get_adjustment (GTK_RANGE (button->scale));
+  gdouble val;
+  gboolean res = TRUE;
+
+  if (button->click_id == 0)
+    return FALSE;
+
+  val = gtk_scale_button_get_value (button);
+  val += button->direction;
+  if (val <= adj->lower) {
+    res = FALSE;
+    val = adj->lower;
+  } else if (val > adj->upper) {
+    res = FALSE;
+    val = adj->upper;
+  }
+  gtk_scale_button_set_value (button, val);
+
+  if (!res) {
+    g_source_remove (button->click_id);
+    button->click_id = 0;
+  }
+
+  return res;
+}
+
+static gboolean
+cb_button_press (GtkWidget      * widget,
+		 GdkEventButton * event,
+		 gpointer         data)
+{
+  GtkScaleButton *button = GTK_SCALE_BUTTON (data);
+  GtkAdjustment *adj = gtk_range_get_adjustment (GTK_RANGE (button->scale));
+
+  if (button->click_id != 0)
+    g_source_remove (button->click_id);
+  button->direction = (widget == button->plus) ?
+      fabs (adj->page_increment) : - fabs (adj->page_increment);
+  button->click_id = g_timeout_add (CLICK_TIMEOUT,
+				    (GSourceFunc) cb_button_timeout, button);
+  cb_button_timeout (button);
+
+  return TRUE;
+}
+
+static gboolean
+cb_button_release (GtkWidget      * widget,
+		   GdkEventButton * event,
+		   gpointer         data)
+{
+  GtkScaleButton *button = GTK_SCALE_BUTTON (data);
+
+  if (button->click_id != 0) {
+    g_source_remove (button->click_id);
+    button->click_id = 0;
+  }
+
+  return TRUE;
+}
+
+/*
+ * Scale callbacks.
+ */
+
+static void
+gtk_scale_button_release_grab (GtkScaleButton *button,
+			   GdkEventButton * event)
+{
+  GdkEventButton *e;
+  GdkDisplay *display;
+
+  /* ungrab focus */
+  display = gtk_widget_get_display (GTK_WIDGET (button));
+  gdk_display_keyboard_ungrab (display, event->time);
+  gdk_display_pointer_ungrab (display, event->time);
+  gtk_grab_remove (button->dock);
+
+  /* hide again */
+  gtk_widget_hide (button->dock);
+  button->timeout = FALSE;
+
+  e = (GdkEventButton *) gdk_event_copy ((GdkEvent *) event);
+  e->window = GTK_WIDGET (button)->window;
+  e->type = GDK_BUTTON_RELEASE;
+  gtk_widget_event (GTK_WIDGET (button), (GdkEvent *) e);
+  e->window = event->window;
+  gdk_event_free ((GdkEvent *) e);
+}
+
+static gboolean
+cb_dock_button_press (GtkWidget      * widget,
+		      GdkEventButton * event,
+		      gpointer         data)
+{
+  GtkScaleButton *button = GTK_SCALE_BUTTON (data);
+
+  if (event->type == GDK_BUTTON_PRESS) {
+    gtk_scale_button_release_grab (button, event);
+    return TRUE;
+  }
+
+  return FALSE;
+}
+
+static gboolean
+cb_dock_key_release (GtkWidget      * widget,
+		     GdkEventKey    * event,
+		     gpointer         data)
+{
+  GtkScaleButton *button = GTK_SCALE_BUTTON (data);
+
+  if (event->keyval == GDK_Escape) {
+    GdkDisplay *display;
+
+    /* ungrab focus */
+    display = gtk_widget_get_display (widget);
+    gdk_display_keyboard_ungrab (display, event->time);
+    gdk_display_pointer_ungrab (display, event->time);
+    gtk_grab_remove (button->dock);
+
+    /* hide again */
+    gtk_widget_hide (button->dock);
+    button->timeout = FALSE;
+   
+    return TRUE;
+  }
+  return FALSE;
+}
+
+static gboolean
+cb_dock_key_press (GtkWidget      * widget,
+		   GdkEventKey    * event,
+		   gpointer         data)
+{
+  if (event->keyval == GDK_Escape) {
+    return TRUE;
+  }
+  return FALSE;
+}
+
+/*
+ * Scale stuff.
+ */
+
+#define GTK_TYPE_SCALE_BUTTON_SCALE \
+  (gtk_scale_button_scale_get_type ())
+#define GTK_SCALE_BUTTON_SCALE(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_SCALE_BUTTON_SCALE, \
+			       GtkScaleButtonScale))
+
+typedef struct _GtkScaleButtonScaleClass {
+  GtkVScaleClass parent_class;
+} GtkScaleButtonScaleClass;
+
+typedef struct _GtkScaleButtonScale {
+  GtkVScale parent;
+  GtkScaleButton *button;
+} GtkScaleButtonScale;
+
+static GType	gtk_scale_button_scale_get_type	 (void);
+
+static void	gtk_scale_button_scale_class_init    (GtkScaleButtonScaleClass * klass);
+
+static gboolean	gtk_scale_button_scale_press	 (GtkWidget      * widget,
+						  GdkEventButton * event);
+static gboolean gtk_scale_button_scale_release	 (GtkWidget      * widget,
+						  GdkEventButton * event);
+
+static GtkVScaleClass *scale_parent_class = NULL;
+
+G_DEFINE_TYPE (GtkScaleButtonScale, gtk_scale_button_scale, GTK_TYPE_VSCALE)
+
+static void
+gtk_scale_button_scale_class_init (GtkScaleButtonScaleClass * klass)
+{
+  GtkWidgetClass *gtkwidget_class = GTK_WIDGET_CLASS (klass);
+  GtkRangeClass *gtkrange_class = GTK_RANGE_CLASS (klass);
+
+  scale_parent_class = g_type_class_peek_parent (klass);
+
+  gtkwidget_class->button_press_event = gtk_scale_button_scale_press;
+  gtkwidget_class->button_release_event = gtk_scale_button_scale_release;
+  gtkrange_class->value_changed = gtk_scale_button_scale_value_changed;
+}
+
+static void
+gtk_scale_button_scale_init (GtkScaleButtonScale *scale)
+{
+}
+
+static GtkWidget *
+gtk_scale_button_scale_new (GtkScaleButton * button,
+			     gdouble min,
+			     gdouble max,
+			     gdouble step)
+{
+  GtkScaleButtonScale *scale = g_object_new (GTK_TYPE_SCALE_BUTTON_SCALE, NULL);
+  GtkObject *adj;
+
+  adj = gtk_adjustment_new (min, min, max, step, 10 * step, 0);
+  gtk_range_set_adjustment (GTK_RANGE (scale), GTK_ADJUSTMENT (adj));
+  scale->button = button;
+
+  return GTK_WIDGET (scale);
+}
+
+static gboolean
+gtk_scale_button_scale_press (GtkWidget      * widget,
+			  GdkEventButton * event)
+{
+  GtkScaleButtonScale *scale = GTK_SCALE_BUTTON_SCALE (widget);
+  GtkScaleButton *button = scale->button;
+
+  /* the scale will grab input; if we have input grabbed, all goes
+   * horribly wrong, so let's not do that. */
+  gtk_grab_remove (button->dock);
+
+  return GTK_WIDGET_CLASS (scale_parent_class)->button_press_event (widget, event);
+}
+
+static gboolean
+gtk_scale_button_scale_release (GtkWidget      * widget,
+			    GdkEventButton * event)
+{
+  GtkScaleButtonScale *scale = GTK_SCALE_BUTTON_SCALE (widget);
+  GtkScaleButton *button = scale->button;
+  gboolean res;
+
+  if (button->timeout) {
+    /* if we did a quick click, leave the window open; else, hide it */
+    if (event->time > button->pop_time + CLICK_TIMEOUT) {
+      gtk_scale_button_release_grab (button, event);
+      GTK_WIDGET_CLASS (scale_parent_class)->button_release_event (widget, event);
+      return TRUE;
+    }
+    button->timeout = FALSE;
+  }
+
+  res = GTK_WIDGET_CLASS (scale_parent_class)->button_release_event (widget, event);
+
+  /* the scale will release input; right after that, we *have to* grab
+   * it back so we can catch out-of-scale clicks and hide the popup,
+   * so I basically want a g_signal_connect_after_always(), but I can't
+   * find that, so we do this complex 'first-call-parent-then-do-actual-
+   * action' thingy... */
+  gtk_grab_add (button->dock);
+
+  return res;
+}
+
+static void
+gtk_scale_button_update_icon (GtkScaleButton *button)
+{
+  GtkRange *range;
+  GtkAdjustment *adj;
+  gdouble step, value;
+  const char *name;
+  guint num_icons;
+
+  if (button->icon_list == NULL)
+    {
+      gtk_image_set_from_stock (GTK_IMAGE (button->image),
+				GTK_STOCK_MISSING_IMAGE,
+				button->size);
+      return;
+    }
+
+  range = GTK_RANGE (button->scale);
+  adj = gtk_range_get_adjustment (range);
+  num_icons = g_list_length (button->icon_list);
+  step = (adj->upper - adj->lower) / (num_icons - 2);
+  value = gtk_scale_button_get_value (button);
+
+  if (value == adj->lower) {
+    name = g_list_nth_data (button->icon_list, 0);
+  } else if (value == adj->upper) {
+    GList *l;
+    l = g_list_last (button->icon_list);
+    name = l->data;
+  } else {
+    guint i;
+    i = (guint) (value - adj->lower) / step + 1;
+    name = g_list_nth_data (button->icon_list, i);
+  }
+
+  gtk_image_set_from_icon_name (GTK_IMAGE (button->image),
+				name,
+				button->size);
+}
+
+static void
+gtk_scale_button_scale_value_changed (GtkRange *range)
+{
+  GtkScaleButtonScale *scale = GTK_SCALE_BUTTON_SCALE (range);
+  GtkScaleButton *button = scale->button;
+  gdouble value;
+
+  value = gtk_range_get_value (range);
+
+  gtk_scale_button_update_icon (button);
+
+  g_signal_emit (button, signals[VALUE_CHANGED], 0, value);
+  g_object_notify (G_OBJECT (button), "value");
+}
+ 
+/*
+ * vim: sw=2 ts=8 cindent noai bs=2
+ */
Index: gtk/gtkscalebutton.h
===================================================================
--- gtk/gtkscalebutton.h	(revision 0)
+++ gtk/gtkscalebutton.h	(revision 0)
@@ -0,0 +1,66 @@
+/* Volume Button / popup widget
+ * (c) copyright 2005 Ronald S. Bultje <rbultje@ronald.bitfreak.net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __GTK_SCALE_BUTTON_H__
+#define __GTK_SCALE_BUTTON_H__
+
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gtk/gtkbutton.h>
+#include <gtk/gtkicontheme.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_SCALE_BUTTON                 (gtk_scale_button_get_type ())
+#define GTK_SCALE_BUTTON(obj)                 (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_SCALE_BUTTON, GtkScaleButton))
+#define GTK_SCALE_BUTTON_CLASS(klass)         (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_SCALE_BUTTON, GtkButtonClass))
+#define GTK_IS_SCALE_BUTTON(obj)              (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_SCALE_BUTTON))
+#define GTK_IS_SCALE_BUTTON_CLASS(klass)      (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_SCALE_BUTTON))
+#define GTK_SCALE_BUTTON_GET_CLASS(obj)       (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_SCALE_BUTTON, GtkButtonScaleClass))
+
+typedef struct GtkScaleButton GtkScaleButton;
+
+typedef struct _GtkScaleButtonClass {
+  GtkButtonClass parent_class;
+
+  /* signals */
+  void	(* value_changed) (GtkScaleButton *button, gdouble value);
+
+  /* Padding for future expansion */
+  void (*_gtk_reserved1) (void);
+  void (*_gtk_reserved2) (void);
+  void (*_gtk_reserved3) (void);
+  void (*_gtk_reserved4) (void);
+} GtkScaleButtonClass;
+
+GType		gtk_scale_button_get_type	(void);
+GtkWidget*	gtk_scale_button_new		(GtkIconSize size,
+						 gdouble min,
+						 gdouble max,
+						 gdouble step,
+						 const char **icons);
+void		gtk_scale_button_set_icons	(GtkScaleButton *button,
+						 const char **icons);
+gdouble		gtk_scale_button_get_value	(GtkScaleButton *button);
+void		gtk_scale_button_set_value	(GtkScaleButton *button,
+						 gdouble value);
+
+G_END_DECLS
+
+#endif /* __GTK_SCALE_BUTTON_H__ */
+
Index: tests/testvolumebutton.c
===================================================================
--- tests/testvolumebutton.c	(revision 0)
+++ tests/testvolumebutton.c	(revision 0)
@@ -0,0 +1,60 @@
+/* testvolumebutton.c
+ * Copyright (C) 2007  Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include <gtk/gtk.h>
+
+static void
+value_changed (GtkWidget *button,
+	       gdouble volume,
+	       gpointer user_data)
+{
+	g_message ("volume changed to %f", volume);
+}
+
+static const char *icons[] = {
+	"audio-volume-muted",
+	"audio-volume-low",
+	"audio-volume-medium",
+	"audio-volume-high",
+	NULL
+};
+
+int main (int argc, char **argv)
+{
+	GtkWidget *window;
+	GtkWidget *button;
+
+	gtk_init (&argc, &argv);
+
+	window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+	button = gtk_scale_button_new (GTK_ICON_SIZE_SMALL_TOOLBAR,
+					0, 100, 2, NULL);
+	gtk_scale_button_set_icons (GTK_SCALE_BUTTON (button), icons);
+	g_signal_connect (G_OBJECT (button),
+			  "value-changed",
+			  G_CALLBACK (value_changed), NULL);
+	gtk_container_add (GTK_CONTAINER (window), button);
+
+	gtk_widget_show_all (window);
+
+	gtk_main ();
+
+	return 0;
+}
+
Index: tests/Makefile.am
===================================================================
--- tests/Makefile.am	(revision 17411)
+++ tests/Makefile.am	(working copy)
@@ -85,7 +85,8 @@
 	testmerge			\
 	testactions			\
 	testgrouping			\
-	testtooltips
+	testtooltips			\
+	testvolumebutton
 
 autotestfilechooser_DEPENDENCIES = $(TEST_DEPS)
 simple_DEPENDENCIES = $(TEST_DEPS)
@@ -140,6 +141,7 @@
 testactions_DEPENDENCIES = $(TEST_DEPS)
 testgrouping_DEPENDENCIES = $(TEST_DEPS)
 testtooltips_DEPENDENCIES = $(TEST_DEPS)
+testvolumebutton_DEPENDENCIES = $(TEST_DEPS)
 
 autotestfilechooser_LDADD = $(LDADDS)
 simple_LDADD = $(LDADDS)
@@ -201,6 +203,7 @@
 testactions_LDADD = $(LDADDS)
 testgrouping_LDADD = $(LDADDS)
 testtooltips_LDADD = $(LDADDS)
+testvolumebutton_LDADD = $(LDADDS)
 
 autotestfilechooser_SOURCES =	\
 	autotestfilechooser.c
@@ -285,6 +288,9 @@
 testrecentchoosermenu_SOURCES =	\
 	testrecentchoosermenu.c
 
+testvolumebutton_SOURCES =	\
+	testvolumebutton.c
+
 EXTRA_DIST = 			\
 	prop-editor.h		\
 	testgtk.1 		\
