1 /* Volume Button / popup widget
2  * (c) copyright 2005 Ronald S. Bultje <rbultje@ronald.bitfreak.net>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Library General Public License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public
15  * License along with this library; if not, write to the
16  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17  * Boston, MA 02111-1307, USA.
18  */
19 
20 #define _GNU_SOURCE
21 #include <math.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <glib/gi18n.h>
25 #include <gtk/gtk.h>
26 #include "bacon-volume.h"
27 
28 #define SCALE_SIZE 100
29 #define CLICK_TIMEOUT 250
30 
31 enum {
32   SIGNAL_VALUE_CHANGED,
33   NUM_SIGNALS
34 };
35 
36 static void	bacon_volume_button_class_init	(BaconVolumeButtonClass * klass);
37 static void	bacon_volume_button_init	(BaconVolumeButton * button);
38 static void	bacon_volume_button_dispose	(GObject        * object);
39 
40 static gboolean	bacon_volume_button_scroll	(GtkWidget      * widget,
41 						 GdkEventScroll * event);
42 static gboolean	bacon_volume_button_press	(GtkWidget      * widget,
43 						 GdkEventButton * event);
44 static gboolean cb_dock_press			(GtkWidget      * widget,
45 						 GdkEventButton * event,
46 						 gpointer         data);
47 
48 static gboolean cb_button_press			(GtkWidget      * widget,
49 						 GdkEventButton * event,
50 						 gpointer         data);
51 static gboolean cb_button_release		(GtkWidget      * widget,
52 						 GdkEventButton * event,
53 						 gpointer         data);
54 static void	bacon_volume_scale_value_changed(GtkRange       * range);
55 
56 /* see below for scale definitions */
57 static GtkWidget *bacon_volume_scale_new	(BaconVolumeButton * button,
58 						 float min, float max,
59 						 float step);
60 
61 static GtkButtonClass *parent_class = NULL;
62 static guint signals[NUM_SIGNALS] = { 0 };
63 
64 GType
bacon_volume_button_get_type(void)65 bacon_volume_button_get_type (void)
66 {
67   static GType bacon_volume_button_type = 0;
68 
69   if (!bacon_volume_button_type) {
70     static const GTypeInfo bacon_volume_button_info = {
71       sizeof (BaconVolumeButtonClass),
72       NULL,
73       NULL,
74       (GClassInitFunc) bacon_volume_button_class_init,
75       NULL,
76       NULL,
77       sizeof (BaconVolumeButton),
78       0,
79       (GInstanceInitFunc) bacon_volume_button_init,
80       NULL
81     };
82 
83     bacon_volume_button_type =
84 	g_type_register_static (GTK_TYPE_BUTTON,
85 				"BaconVolumeButton",
86 				&bacon_volume_button_info, 0);
87   }
88 
89   return bacon_volume_button_type;
90 }
91 
92 static void
bacon_volume_button_class_init(BaconVolumeButtonClass * klass)93 bacon_volume_button_class_init (BaconVolumeButtonClass *klass)
94 {
95   GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
96   GtkWidgetClass *gtkwidget_class = GTK_WIDGET_CLASS (klass);
97 
98   parent_class = g_type_class_ref (GTK_TYPE_BUTTON);
99 
100   /* events */
101   gobject_class->dispose = bacon_volume_button_dispose;
102   gtkwidget_class->button_press_event = bacon_volume_button_press;
103   gtkwidget_class->scroll_event = bacon_volume_button_scroll;
104 
105   /* signals */
106   signals[SIGNAL_VALUE_CHANGED] = g_signal_new ("value-changed",
107       G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST,
108       G_STRUCT_OFFSET (BaconVolumeButtonClass, value_changed),
109       NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
110 }
111 
112 static void
bacon_volume_button_init(BaconVolumeButton * button)113 bacon_volume_button_init (BaconVolumeButton *button)
114 {
115   button->timeout = FALSE;
116   button->click_id = 0;
117   button->dock = button->scale = NULL;
118 #ifndef HAVE_GTK_ONLY
119   button->theme = gtk_icon_theme_get_default ();
120 #endif
121 }
122 
123 static void
bacon_volume_button_dispose(GObject * object)124 bacon_volume_button_dispose (GObject *object)
125 {
126   BaconVolumeButton *button = BACON_VOLUME_BUTTON (object);
127 
128   if (button->dock) {
129     gtk_widget_destroy (button->dock);
130     button->dock = NULL;
131   }
132 
133   if (button->theme) {
134     g_object_unref (G_OBJECT (button->theme));
135     button->theme = NULL;
136   }
137 
138   if (button->click_id != 0) {
139     g_source_remove (button->click_id);
140     button->click_id = 0;
141   }
142 
143   G_OBJECT_CLASS (parent_class)->dispose (object);
144 }
145 
146 /*
147  * public API.
148  */
149 
150 GtkWidget *
bacon_volume_button_new(GtkIconSize size,float min,float max,float step)151 bacon_volume_button_new (GtkIconSize size,
152 			 float min, float max,
153 			 float step)
154 {
155   BaconVolumeButton *button;
156   GtkWidget *frame, *box;
157 
158   button = g_object_new (BACON_TYPE_VOLUME_BUTTON, NULL);
159   button->size = size;
160   gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NORMAL);
161 
162 #ifndef HAVE_GTK_ONLY
163   /* image */
164   button->image = gtk_image_new ();
165   gtk_container_add (GTK_CONTAINER (button), button->image);
166   gtk_widget_show_all (button->image);
167 #endif
168 
169   /* window */
170   button->dock = gtk_window_new (GTK_WINDOW_POPUP);
171   g_signal_connect (button->dock, "button-press-event",
172 		    G_CALLBACK (cb_dock_press), button);
173   gtk_window_set_decorated (GTK_WINDOW (button->dock), FALSE);
174 
175   /* frame */
176   frame = gtk_frame_new (NULL);
177   gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_OUT);
178   gtk_container_add (GTK_CONTAINER (button->dock), frame);
179   box = gtk_vbox_new (FALSE, 0);
180   gtk_container_add (GTK_CONTAINER (frame), box);
181 
182   /* + */
183   button->plus = gtk_button_new_with_label (_("+"));
184   gtk_button_set_relief (GTK_BUTTON (button->plus), GTK_RELIEF_NONE);
185   g_signal_connect (button->plus, "button-press-event",
186 		    G_CALLBACK (cb_button_press), button);
187   g_signal_connect (button->plus, "button-release-event",
188 		    G_CALLBACK (cb_button_release), button);
189   gtk_box_pack_start (GTK_BOX (box), button->plus, TRUE, FALSE, 0);
190 
191   /* scale */
192   button->scale = bacon_volume_scale_new (button, min, max, step);
193   gtk_widget_set_size_request (button->scale, -1, SCALE_SIZE);
194   gtk_scale_set_draw_value (GTK_SCALE (button->scale), FALSE);
195   gtk_range_set_inverted (GTK_RANGE (button->scale), TRUE);
196   gtk_box_pack_start (GTK_BOX (box), button->scale, TRUE, FALSE, 0);
197 
198   /* - */
199   button->min = gtk_button_new_with_label (_("-"));
200   gtk_button_set_relief (GTK_BUTTON (button->min), GTK_RELIEF_NONE);
201   g_signal_connect (button->min, "button-press-event",
202 		   G_CALLBACK (cb_button_press), button);
203   g_signal_connect (button->min, "button-release-event",
204 		    G_CALLBACK (cb_button_release), button);
205   gtk_box_pack_start (GTK_BOX (box), button->min, TRUE, FALSE, 0);
206 
207   /* call callback once so original icon is drawn */
208   bacon_volume_scale_value_changed (GTK_RANGE (button->scale));
209 
210   return GTK_WIDGET (button);
211 }
212 
213 float
bacon_volume_button_get_value(BaconVolumeButton * button)214 bacon_volume_button_get_value (BaconVolumeButton * button)
215 {
216   g_return_val_if_fail (button != NULL, 0);
217 
218   return gtk_range_get_value (GTK_RANGE (button->scale));
219 }
220 
221 void
bacon_volume_button_set_value(BaconVolumeButton * button,float value)222 bacon_volume_button_set_value (BaconVolumeButton * button,
223 			       float value)
224 {
225   g_return_if_fail (button != NULL);
226 
227   gtk_range_set_value (GTK_RANGE (button->scale), value);
228 }
229 
230 /*
231  * button callbacks.
232  */
233 
234 static gboolean
bacon_volume_button_scroll(GtkWidget * widget,GdkEventScroll * event)235 bacon_volume_button_scroll (GtkWidget      * widget,
236 			    GdkEventScroll * event)
237 {
238   BaconVolumeButton *button = BACON_VOLUME_BUTTON (widget);
239   GtkAdjustment *adj = gtk_range_get_adjustment (GTK_RANGE (button->scale));
240   float d;
241 
242   if (event->type != GDK_SCROLL)
243     return FALSE;
244 
245   d = bacon_volume_button_get_value (button);
246   if (event->direction == GDK_SCROLL_UP) {
247     d += adj->step_increment;
248     if (d > adj->upper)
249       d = adj->upper;
250   } else {
251     d -= adj->step_increment;
252     if (d < adj->lower)
253       d = adj->lower;
254   }
255   bacon_volume_button_set_value (button, d);
256 
257   return TRUE;
258 }
259 
260 static gboolean
bacon_volume_button_press(GtkWidget * widget,GdkEventButton * event)261 bacon_volume_button_press (GtkWidget      * widget,
262 			   GdkEventButton * event)
263 {
264   BaconVolumeButton *button = BACON_VOLUME_BUTTON (widget);
265   GtkAdjustment *adj = gtk_range_get_adjustment (GTK_RANGE (button->scale));
266   gint x, y, m, dx, dy, sx, sy, ystartoff, mouse_y;
267   float v;
268   GdkEventButton *e;
269 
270   /* position roughly */
271   gdk_window_get_origin (widget->window, &x, &y);
272   x += widget->allocation.x;
273   y += widget->allocation.y;
274   gtk_window_move (GTK_WINDOW (button->dock), x, y - (SCALE_SIZE / 2));
275   gtk_widget_show_all (button->dock);
276   gdk_window_get_origin (button->dock->window, &dx, &dy);
277   dy += button->dock->allocation.y;
278   gdk_window_get_origin (button->scale->window, &sx, &sy);
279   sy += button->scale->allocation.y;
280   ystartoff = sy - dy;
281   mouse_y = event->y;
282   button->timeout = TRUE;
283 
284   /* position (needs widget to be shown already) */
285   v = bacon_volume_button_get_value (button) / (adj->upper - adj->lower);
286   x += (widget->allocation.width - button->dock->allocation.width) / 2;
287   y -= ystartoff;
288   y -= GTK_RANGE (button->scale)->min_slider_size / 2;
289   m = button->scale->allocation.height -
290       GTK_RANGE (button->scale)->min_slider_size;
291   y -= m * (1.0 - v);
292   y += mouse_y;
293   gtk_window_move (GTK_WINDOW (button->dock), x, y);
294   gdk_window_get_origin (button->scale->window, &sx, &sy);
295 
296   GTK_WIDGET_CLASS (parent_class)->button_press_event (widget, event);
297 
298   /* grab focus */
299   gtk_widget_grab_focus (button->dock);
300   gtk_grab_add (button->dock);
301   gdk_pointer_grab (button->dock->window, TRUE,
302       GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
303       GDK_POINTER_MOTION_MASK, NULL, NULL, GDK_CURRENT_TIME);
304   gdk_keyboard_grab (button->dock->window, TRUE, GDK_CURRENT_TIME);
305 
306   /* forward event to the slider */
307   e = (GdkEventButton *) gdk_event_copy ((GdkEvent *) event);
308   e->window = button->scale->window;
309 
310   /* position: the X position isn't relevant, halfway will work just fine.
311    * The vertical position should be *exactly* in the middle of the slider
312    * of the scale; if we don't do that correctly, it'll move from its current
313    * position, which means a position change on-click, which is bad. */
314   e->x = button->scale->allocation.width / 2;
315   m = button->scale->allocation.height -
316       GTK_RANGE (button->scale)->min_slider_size;
317   e->y = ((1.0 - v) * m) + GTK_RANGE (button->scale)->min_slider_size / 2;
318   gtk_widget_event (button->scale, (GdkEvent *) e);
319   e->window = event->window;
320   gdk_event_free ((GdkEvent *) e);
321 
322   button->pop_time = event->time;
323 
324   return TRUE;
325 }
326 
327 /*
328  * +/- button callbacks.
329  */
330 
331 static gboolean
cb_button_timeout(gpointer data)332 cb_button_timeout (gpointer data)
333 {
334   BaconVolumeButton *button = BACON_VOLUME_BUTTON (data);
335   GtkAdjustment *adj = gtk_range_get_adjustment (GTK_RANGE (button->scale));
336   float val;
337   gboolean res = TRUE;
338 
339   if (button->click_id == 0)
340     return FALSE;
341 
342   val = bacon_volume_button_get_value (button);
343   val += button->direction;
344   if (val <= adj->lower) {
345     res = FALSE;
346     val = adj->lower;
347   } else if (val > adj->upper) {
348     res = FALSE;
349     val = adj->upper;
350   }
351   bacon_volume_button_set_value (button, val);
352 
353   if (!res) {
354     g_source_remove (button->click_id);
355     button->click_id = 0;
356   }
357 
358   return res;
359 }
360 
361 static gboolean
cb_button_press(GtkWidget * widget,GdkEventButton * event,gpointer data)362 cb_button_press (GtkWidget      * widget,
363 		 GdkEventButton * event,
364 		 gpointer         data)
365 {
366   BaconVolumeButton *button = BACON_VOLUME_BUTTON (data);
367   GtkAdjustment *adj = gtk_range_get_adjustment (GTK_RANGE (button->scale));
368 
369   if (button->click_id != 0)
370     g_source_remove (button->click_id);
371   button->direction = (widget == button->plus) ?
372       fabs (adj->page_increment) : - fabs (adj->page_increment);
373   button->click_id = g_timeout_add (CLICK_TIMEOUT,
374 				    (GSourceFunc) cb_button_timeout, button);
375   cb_button_timeout (button);
376 
377   return TRUE;
378 }
379 
380 static gboolean
cb_button_release(GtkWidget * widget,GdkEventButton * event,gpointer data)381 cb_button_release (GtkWidget      * widget,
382 		   GdkEventButton * event,
383 		   gpointer         data)
384 {
385   BaconVolumeButton *button = BACON_VOLUME_BUTTON (data);
386 
387   if (button->click_id != 0) {
388     g_source_remove (button->click_id);
389     button->click_id = 0;
390   }
391 
392   return TRUE;
393 }
394 
395 /*
396  * Scale callbacks.
397  */
398 
399 static void
bacon_volume_release_grab(BaconVolumeButton * button,GdkEventButton * event)400 bacon_volume_release_grab (BaconVolumeButton *button,
401 			   GdkEventButton * event)
402 {
403   GdkEventButton *e;
404 
405   /* ungrab focus */
406   gdk_keyboard_ungrab (GDK_CURRENT_TIME);
407   gdk_pointer_ungrab (GDK_CURRENT_TIME);
408   gtk_grab_remove (button->dock);
409 
410   /* hide again */
411   gtk_widget_hide (button->dock);
412   button->timeout = FALSE;
413 
414   e = (GdkEventButton *) gdk_event_copy ((GdkEvent *) event);
415   e->window = GTK_WIDGET (button)->window;
416   e->type = GDK_BUTTON_RELEASE;
417   gtk_widget_event (GTK_WIDGET (button), (GdkEvent *) e);
418   e->window = event->window;
419   gdk_event_free ((GdkEvent *) e);
420 }
421 
422 static gboolean
cb_dock_press(GtkWidget * widget,GdkEventButton * event,gpointer data)423 cb_dock_press (GtkWidget      * widget,
424 	       GdkEventButton * event,
425 	       gpointer         data)
426 {
427 
428   BaconVolumeButton *button = BACON_VOLUME_BUTTON (data);
429 
430   if (/*ewidget == button->dock &&*/ event->type == GDK_BUTTON_PRESS) {
431     bacon_volume_release_grab (button, event);
432     return TRUE;
433   }
434 
435   return FALSE;
436 }
437 
438 /*
439  * Scale stuff.
440  */
441 
442 #define BACON_TYPE_VOLUME_SCALE \
443   (bacon_volume_scale_get_type ())
444 #define BACON_VOLUME_SCALE(obj) \
445   (G_TYPE_CHECK_INSTANCE_CAST ((obj), BACON_TYPE_VOLUME_SCALE, \
446 			       BaconVolumeScale))
447 
448 typedef struct _BaconVolumeScale {
449   GtkVScale parent;
450   BaconVolumeButton *button;
451 } BaconVolumeScale;
452 
453 static GType	bacon_volume_scale_get_type	 (void);
454 
455 static void	bacon_volume_scale_class_init    (GtkVScaleClass * klass);
456 
457 static gboolean	bacon_volume_scale_press	 (GtkWidget      * widget,
458 						  GdkEventButton * event);
459 static gboolean bacon_volume_scale_release	 (GtkWidget      * widget,
460 						  GdkEventButton * event);
461 
462 static GtkVScaleClass *scale_parent_class = NULL;
463 
464 static GType
bacon_volume_scale_get_type(void)465 bacon_volume_scale_get_type (void)
466 {
467   static GType bacon_volume_scale_type = 0;
468 
469   if (!bacon_volume_scale_type) {
470     static const GTypeInfo bacon_volume_scale_info = {
471       sizeof (GtkVScaleClass),
472       NULL,
473       NULL,
474       (GClassInitFunc) bacon_volume_scale_class_init,
475       NULL,
476       NULL,
477       sizeof (BaconVolumeScale),
478       0,
479       NULL,
480       NULL
481     };
482 
483     bacon_volume_scale_type =
484         g_type_register_static (GTK_TYPE_VSCALE,
485 				"BaconVolumeScale",
486 				&bacon_volume_scale_info, 0);
487   }
488 
489   return bacon_volume_scale_type;
490 }
491 
492 static void
bacon_volume_scale_class_init(GtkVScaleClass * klass)493 bacon_volume_scale_class_init (GtkVScaleClass * klass)
494 {
495   GtkWidgetClass *gtkwidget_class = GTK_WIDGET_CLASS (klass);
496   GtkRangeClass *gtkrange_class = GTK_RANGE_CLASS (klass);
497 
498   scale_parent_class = g_type_class_ref (GTK_TYPE_VSCALE);
499 
500   gtkwidget_class->button_press_event = bacon_volume_scale_press;
501   gtkwidget_class->button_release_event = bacon_volume_scale_release;
502   gtkrange_class->value_changed = bacon_volume_scale_value_changed;
503 }
504 
505 static GtkWidget *
bacon_volume_scale_new(BaconVolumeButton * button,float min,float max,float step)506 bacon_volume_scale_new (BaconVolumeButton * button,
507 			float min, float max,
508 			float step)
509 {
510   BaconVolumeScale *scale = g_object_new (BACON_TYPE_VOLUME_SCALE, NULL);
511   GtkObject *adj;
512 
513   adj = gtk_adjustment_new (min, min, max, step, 10 * step, 0);
514   gtk_range_set_adjustment (GTK_RANGE (scale), GTK_ADJUSTMENT (adj));
515   scale->button = button;
516 
517   return GTK_WIDGET (scale);
518 }
519 
520 static gboolean
bacon_volume_scale_press(GtkWidget * widget,GdkEventButton * event)521 bacon_volume_scale_press (GtkWidget      * widget,
522 			  GdkEventButton * event)
523 {
524   BaconVolumeScale *scale = BACON_VOLUME_SCALE (widget);
525   BaconVolumeButton *button = scale->button;
526 
527   /* the scale will grab input; if we have input grabbed, all goes
528    * horribly wrong, so let's not do that. */
529   gtk_grab_remove (button->dock);
530 
531   return GTK_WIDGET_CLASS (scale_parent_class)->button_press_event (widget, event);
532 }
533 
534 static gboolean
bacon_volume_scale_release(GtkWidget * widget,GdkEventButton * event)535 bacon_volume_scale_release (GtkWidget      * widget,
536 			    GdkEventButton * event)
537 {
538   BaconVolumeScale *scale = BACON_VOLUME_SCALE (widget);
539   BaconVolumeButton *button = scale->button;
540   gboolean res;
541 
542   if (button->timeout) {
543     /* if we did a quick click, leave the window open; else, hide it */
544     if (event->time > button->pop_time + CLICK_TIMEOUT) {
545       bacon_volume_release_grab (button, event);
546       GTK_WIDGET_CLASS (scale_parent_class)->button_release_event (widget, event);
547       return TRUE;
548     }
549     button->timeout = FALSE;
550   }
551 
552   res = GTK_WIDGET_CLASS (scale_parent_class)->button_release_event (widget, event);
553 
554   /* the scale will release input; right after that, we *have to* grab
555    * it back so we can catch out-of-scale clicks and hide the popup,
556    * so I basically want a g_signal_connect_after_always(), but I can't
557    * find that, so we do this complex 'first-call-parent-then-do-actual-
558    * action' thingy... */
559   gtk_grab_add (button->dock);
560 
561   return res;
562 }
563 
564 static void
bacon_volume_scale_value_changed(GtkRange * range)565 bacon_volume_scale_value_changed (GtkRange * range)
566 {
567   BaconVolumeScale *scale = BACON_VOLUME_SCALE (range);
568   BaconVolumeButton *button = scale->button;
569   GtkAdjustment *adj = gtk_range_get_adjustment (GTK_RANGE (button->scale));
570   float step = (adj->upper - adj->lower) / 4;
571   float val = gtk_range_get_value (range);
572   gint w, h;
573 #ifdef HAVE_GTK_ONLY
574   char *s;
575 
576   /* update label */
577   s = g_strdup_printf ("%d", lrintf (val));
578   gtk_button_set_label (GTK_BUTTON (button), s);
579   q_free (s);
580 #else
581   const char *s;
582   GdkPixbuf *buf=NULL;
583 
584   if (val == adj->lower)
585     s = "stock_volume-0";
586   else if (val > adj->lower && val <= adj->lower + step)
587     s = "stock_volume-min";
588   else if (val > adj->lower + step && val <= adj->lower + step * 2)
589     s = "stock_volume-med";
590   else
591     s = "stock_volume-max";
592 
593   /* update image */
594   gtk_icon_size_lookup (button->size, &w, &h);
595   buf = gtk_icon_theme_load_icon (button->theme, s, w, 0, NULL);
596   if(buf)
597   {
598 	  gtk_image_set_from_pixbuf (GTK_IMAGE (button->image), buf);
599 	  g_object_unref(buf);
600   }
601   else
602   {
603 	  gtk_image_set_from_stock(GTK_IMAGE (button->image), "gmpc-volume", button->size);
604   }
605 #endif
606 
607   /* signal */
608   g_signal_emit (button, signals[SIGNAL_VALUE_CHANGED], 0);
609 }
610