2008-05-20 Alejandro G. Castro <alex@igalia.com>
[hildon] / src / hildon-seekbar.c
1 /*
2  * This file is a part of hildon
3  *
4  * Copyright (C) 2005, 2006 Nokia Corporation, all rights reserved.
5  *
6  * Contact: Michael Dominic Kostrzewa <michael.kostrzewa@nokia.com>
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public License
10  * as published by the Free Software Foundation; version 2.1 of
11  * the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful, but
14  * WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
21  * 02110-1301 USA
22  *
23  */
24
25 /**
26  * SECTION:hildon-seekbar
27  * @short_description: A widget used to identify a place from a content.
28  *
29  * HildonSeekbar allows seeking in media with a range widget.  It
30  * supports for setting or getting the length (total time) of the media,
31  * the position within it and the fraction (maximum position in a
32  * stream/the amount currently downloaded).  The position is clamped
33  * between zero and the total time, or zero and the fraction in case of
34  * a stream.
35  */
36
37 #ifdef                                          HAVE_CONFIG_H
38 #include                                        <config.h>
39 #endif
40
41 #include                                        "hildon-seekbar.h"
42 #include                                        <libintl.h>
43 #include                                        <stdio.h>
44 #include                                        <math.h>
45 #include                                        <gtk/gtklabel.h>
46 #include                                        <gtk/gtkframe.h>
47 #include                                        <gtk/gtkalignment.h>
48 #include                                        <gtk/gtkadjustment.h>
49 #include                                        <gtk/gtktoolbar.h>
50 #include                                        <gtk/gtkversion.h>
51 #include                                        <gdk/gdkkeysyms.h>
52 #include                                        "hildon-seekbar-private.h"
53
54 static GtkScaleClass*                           parent_class = NULL;
55
56 static void 
57 hildon_seekbar_class_init                       (HildonSeekbarClass *seekbar_class);
58
59 static void 
60 hildon_seekbar_init                             (HildonSeekbar *seekbar);
61
62 static void
63 hildon_seekbar_set_property                     (GObject *object, 
64                                                  guint prop_id,
65                                                  const GValue *value,
66                                                  GParamSpec *pspec);
67
68 static void 
69 hildon_seekbar_get_property                     (GObject *object, 
70                                                  guint prop_id,
71                                                  GValue *value,
72                                                  GParamSpec *pspec);
73
74 static void
75 hildon_seekbar_size_request                     (GtkWidget *widget,
76                                                  GtkRequisition *event);
77
78 static void 
79 hildon_seekbar_size_allocate                    (GtkWidget *widget,
80                                                  GtkAllocation *allocation);
81
82 static gboolean 
83 hildon_seekbar_expose                           (GtkWidget *widget,
84                                                  GdkEventExpose *event);
85
86 static gboolean 
87 hildon_seekbar_button_press_event               (GtkWidget *widget,
88                                                  GdkEventButton *event);
89
90 static gboolean
91 hildon_seekbar_button_release_event             (GtkWidget *widget,
92                                                  GdkEventButton *event);
93
94 static gboolean 
95 hildon_seekbar_keypress                         (GtkWidget *widget,
96                                                  GdkEventKey *event);
97
98 #define                                         MINIMUM_WIDTH 115
99
100 #define                                         DEFAULT_HEIGHT 58
101
102 #define                                         TOOL_MINIMUM_WIDTH 75
103
104 #define                                         TOOL_DEFAULT_HEIGHT 40
105
106 #define                                         DEFAULT_DISPLAYC_BORDER 10
107
108 #define                                         BUFFER_SIZE 32
109
110 #define                                         EXTRA_SIDE_BORDER 20
111
112 #define                                         TOOL_EXTRA_SIDE_BORDER 0
113
114 #define                                         NUM_STEPS 20
115
116 #define                                         SECONDS_PER_MINUTE 60
117
118 /* the number of digits precision for the internal range.
119  * note, this needs to be enough so that the step size for
120  * small total_times doesn't get rounded off. Currently set to 3
121  * this is because for the smallest total time ( i.e 1 ) and the current
122  * num steps ( 20 ) is: 1/20 = 0.05.  0.05 is 2 digits, and we
123  * add one for safety */
124 #define                                         MAX_ROUND_DIGITS 3
125
126 enum 
127 {
128     PROP_0,
129     PROP_TOTAL_TIME,
130     PROP_POSITION,
131     PROP_FRACTION
132 };
133
134 /**
135  * hildon_seekbar_get_type:
136  * 
137  * Initializes, and returns the type of a hildon seekbar.
138  * 
139  * @Returns : GType of #HildonSeekbar
140  * 
141  */
142 GType G_GNUC_CONST 
143 hildon_seekbar_get_type                         (void)
144 {
145     static GType seekbar_type = 0;
146
147     if (!seekbar_type) {
148         static const GTypeInfo seekbar_info = {
149             sizeof (HildonSeekbarClass),
150             NULL,       /* base_init */
151             NULL,       /* base_finalize */
152             (GClassInitFunc) hildon_seekbar_class_init,
153             NULL,       /* class_finalize */
154             NULL,       /* class_data */
155             sizeof (HildonSeekbar),
156             0,  /* n_preallocs */
157             (GInstanceInitFunc) hildon_seekbar_init,
158         };
159         seekbar_type = g_type_register_static(GTK_TYPE_SCALE,
160                 "HildonSeekbar",
161                 &seekbar_info, 0);
162     }
163
164     return seekbar_type;
165 }
166
167 /**
168  * Initialises the seekbar class.
169  */
170 static void 
171 hildon_seekbar_class_init                       (HildonSeekbarClass *seekbar_class)
172 {
173     GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (seekbar_class);
174     GObjectClass *object_class = G_OBJECT_CLASS (seekbar_class);
175
176     parent_class = g_type_class_peek_parent (seekbar_class);
177
178     g_type_class_add_private (seekbar_class, sizeof (HildonSeekbarPrivate));
179
180     widget_class->size_request          = hildon_seekbar_size_request;
181     widget_class->size_allocate         = hildon_seekbar_size_allocate;
182     widget_class->expose_event          = hildon_seekbar_expose;
183     widget_class->button_press_event    = hildon_seekbar_button_press_event;
184     widget_class->button_release_event  = hildon_seekbar_button_release_event;
185     widget_class->key_press_event       = hildon_seekbar_keypress;
186
187     object_class->set_property          = hildon_seekbar_set_property;
188     object_class->get_property          = hildon_seekbar_get_property;
189
190     /**
191      * HildonSeekbar:total-time:
192      *
193      * Total playing time of this media file.
194      */
195     g_object_class_install_property (object_class, PROP_TOTAL_TIME,
196             g_param_spec_double ("total-time",
197                 "total time",
198                 "Total playing time of this media file",
199                 0,           /* min value */
200                 G_MAXDOUBLE, /* max value */
201                 0,           /* default */
202                 G_PARAM_READWRITE));
203
204     /**
205      * HildonSeekbar:position:
206      *
207      * Current position in this media file.
208      */
209     g_object_class_install_property (object_class, PROP_POSITION,
210             g_param_spec_double ("position",
211                 "position",
212                 "Current position in this media file",
213                 0,           /* min value */
214                 G_MAXDOUBLE, /* max value */
215                 0,           /* default */
216                 G_PARAM_READWRITE));
217
218     /**
219      * HildonSeekbar:fraction:
220      *
221      * Current fraction related to the progress indicator.
222      */
223     g_object_class_install_property (object_class, PROP_FRACTION,
224             g_param_spec_double ("fraction",
225                 "Fraction",
226                 "current fraction related to the"
227                 "progress indicator",
228                 0,           /* min value */
229                 G_MAXDOUBLE, /* max value */
230                 0,           /* default */
231                 G_PARAM_READWRITE));
232 }
233
234
235 static void
236 hildon_seekbar_init                             (HildonSeekbar *seekbar)
237 {
238     HildonSeekbarPrivate *priv;
239     GtkRange *range = GTK_RANGE(seekbar);
240
241     priv = HILDON_SEEKBAR_GET_PRIVATE (seekbar);
242     g_assert (priv);
243
244     /* Initialize range widget */
245     range->orientation = GTK_ORIENTATION_HORIZONTAL;
246     range->flippable = TRUE;
247     range->has_stepper_a = TRUE;
248     range->has_stepper_d = TRUE;
249     range->round_digits = MAX_ROUND_DIGITS;
250
251     gtk_scale_set_draw_value (GTK_SCALE (seekbar), FALSE);
252 }
253
254 /*
255  * Purpose of this function is to prevent Up and Down keys from
256  * changing the widget's value (like Left and Right). Instead they
257  * are used for changing focus to other widgtes.
258  */
259 static gboolean
260 hildon_seekbar_keypress                         (GtkWidget *widget,
261                                                  GdkEventKey *event)
262 {
263     if (event->keyval == GDK_Up || event->keyval == GDK_Down)
264         return FALSE;
265
266     return ((GTK_WIDGET_CLASS (parent_class)->key_press_event) (widget, event));
267 }
268
269 static void
270 hildon_seekbar_set_property                     (GObject *object, 
271                                                  guint prop_id,
272                                                  const GValue *value, 
273                                                  GParamSpec *pspec)
274 {
275     HildonSeekbar *seekbar = HILDON_SEEKBAR (object);
276
277     switch (prop_id) {
278
279         case PROP_TOTAL_TIME:
280             hildon_seekbar_set_total_time (seekbar, g_value_get_double (value));
281             break;
282
283         case PROP_POSITION:
284             hildon_seekbar_set_position (seekbar, g_value_get_double (value));
285             break;
286
287         case PROP_FRACTION:
288             hildon_seekbar_set_fraction (seekbar, g_value_get_double (value));
289             break;
290
291         default:
292             G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
293             break;
294     }
295 }
296
297 /* handle getting of seekbar properties */
298 static void
299 hildon_seekbar_get_property                     (GObject *object, 
300                                                  guint prop_id,
301                                                  GValue *value, 
302                                                  GParamSpec *pspec)
303 {
304     GtkRange *range = GTK_RANGE (object);
305
306     switch (prop_id) {
307
308         case PROP_TOTAL_TIME:
309             g_value_set_double (value, range->adjustment->upper);
310             break;
311
312         case PROP_POSITION:
313             g_value_set_double (value, range->adjustment->value);
314             break;
315
316         case PROP_FRACTION:
317             g_value_set_double (value, 
318                     hildon_seekbar_get_fraction (HILDON_SEEKBAR(object)));
319             break;
320
321         default:
322             G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
323             break;
324     }
325 }
326
327 /**
328  * hildon_seekbar_new:
329  *
330  * Create a new #HildonSeekbar widget.
331  * 
332  * Returns: a #GtkWidget pointer of #HildonSeekbar widget
333  */
334 GtkWidget*
335 hildon_seekbar_new                              (void)
336 {
337     return g_object_new (HILDON_TYPE_SEEKBAR, NULL);
338 }
339
340 /**
341  * hildon_seekbar_get_total_time:
342  * @seekbar: pointer to #HildonSeekbar widget
343  *
344  * Returns: total playing time of media in seconds.
345  */
346 gint
347 hildon_seekbar_get_total_time                   (HildonSeekbar *seekbar)
348 {
349     GtkWidget *widget;
350     widget = GTK_WIDGET (seekbar);
351     g_return_val_if_fail (HILDON_IS_SEEKBAR (seekbar), 0);
352     g_return_val_if_fail (GTK_RANGE (widget)->adjustment, 0);
353     return GTK_RANGE (widget)->adjustment->upper;
354 }
355
356 /**
357  * hildon_seekbar_set_total_time:
358  * @seekbar: pointer to #HildonSeekbar widget
359  * @time: integer greater than zero
360  *
361  * Set total playing time of media in seconds.
362  */
363 void
364 hildon_seekbar_set_total_time                   (HildonSeekbar *seekbar, 
365                                                  gint time)
366 {
367     GtkAdjustment *adj;
368     GtkWidget *widget;
369     gboolean value_changed = FALSE;
370
371     g_return_if_fail (HILDON_IS_SEEKBAR (seekbar));
372     widget = GTK_WIDGET (seekbar);
373
374     if (time <= 0) {
375         return;
376     }
377
378     g_return_if_fail (GTK_RANGE (widget)->adjustment);
379
380     adj = GTK_RANGE (widget)->adjustment;
381     adj->upper = time;
382
383     /* Clamp position to total time */
384     if (adj->value > time) {
385         adj->value = time;
386         value_changed = TRUE;
387     }
388
389     /* Calculate new step value */
390     adj->step_increment = adj->upper / NUM_STEPS;
391     adj->page_increment = adj->step_increment;
392
393     gtk_adjustment_changed (adj);
394
395     /* Update range widget position/fraction */
396     if (value_changed) {
397         gtk_adjustment_value_changed (adj);
398         hildon_seekbar_set_fraction(seekbar,
399                 MIN (hildon_seekbar_get_fraction (seekbar),
400                     time));
401
402         g_object_freeze_notify (G_OBJECT (seekbar));
403
404         hildon_seekbar_set_position (seekbar,
405                 MIN (hildon_seekbar_get_position (seekbar),
406                     time));
407
408         g_object_notify(G_OBJECT (seekbar), "total-time");
409
410         g_object_thaw_notify (G_OBJECT (seekbar));
411     }
412 }
413
414 /**
415  * hildon_seekbar_get_fraction:
416  * @seekbar: pointer to #HildonSeekbar widget
417  *
418  * Get current fraction value of the rage.
419  *
420  * Returns: current fraction
421  */
422 guint 
423 hildon_seekbar_get_fraction                     (HildonSeekbar *seekbar)
424 {
425     g_return_val_if_fail (HILDON_IS_SEEKBAR (seekbar), 0);
426
427 #if defined(MAEMO_GTK) || GTK_CHECK_VERSION(2,11,0)
428     return gtk_range_get_fill_level (GTK_RANGE (seekbar));
429 #else
430     return 0;
431 #endif
432 }
433
434 /**
435  * hildon_seekbar_set_fraction:
436  * @seekbar: pointer to #HildonSeekbar widget
437  * @fraction: the new position of the progress indicator
438  *
439  * Set current fraction value of the range.
440  * It should be between the minimal and maximal values of the range in seekbar.
441  */
442 void 
443 hildon_seekbar_set_fraction                     (HildonSeekbar *seekbar, 
444                                                  guint fraction)
445 {
446     GtkRange *range = NULL;
447     g_return_if_fail (HILDON_IS_SEEKBAR (seekbar));
448
449     range = GTK_RANGE(GTK_WIDGET(seekbar));
450
451     g_return_if_fail (fraction <= range->adjustment->upper &&
452             fraction >= range->adjustment->lower);
453
454     /* Set to show stream indicator. */
455     g_object_set (G_OBJECT (seekbar), "show-fill-level", TRUE, NULL);
456
457     fraction = CLAMP (fraction, range->adjustment->lower,
458             range->adjustment->upper);
459
460 #if defined(MAEMO_GTK) || GTK_CHECK_VERSION(2,11,0)
461     /* Update stream position of range widget */
462     gtk_range_set_fill_level (range, fraction);
463 #endif
464
465     if (fraction < hildon_seekbar_get_position(seekbar))
466         hildon_seekbar_set_position(seekbar, fraction);
467
468     g_object_notify (G_OBJECT (seekbar), "fraction");
469 }
470
471 /**
472  * hildon_seekbar_get_position:
473  * @seekbar: pointer to #HildonSeekbar widget
474  *
475  * Get current position in stream in seconds.
476  *
477  * Returns: current position in stream in seconds
478  */
479 gint 
480 hildon_seekbar_get_position                     (HildonSeekbar *seekbar)
481 {
482     g_return_val_if_fail (HILDON_IS_SEEKBAR(seekbar), 0);
483     g_return_val_if_fail (GTK_RANGE(seekbar)->adjustment, 0);
484
485     return GTK_RANGE (seekbar)->adjustment->value;
486 }
487
488 /**
489  * hildon_seekbar_set_position:
490  * @seekbar: pointer to #HildonSeekbar widget
491  * @time: time within range of >= 0 && < G_MAXINT
492  *
493  * Set current position in stream in seconds.
494  */
495 void 
496 hildon_seekbar_set_position                     (HildonSeekbar *seekbar, 
497                                                  gint time)
498 {
499     GtkRange *range;
500     GtkAdjustment *adj;
501     gint value;
502
503     g_return_if_fail (time >= 0);
504     g_return_if_fail (HILDON_IS_SEEKBAR(seekbar));
505     range = GTK_RANGE (seekbar);
506     adj = range->adjustment;
507     g_return_if_fail (adj);
508
509     /* only change value if it is a different int. this allows us to have
510        smooth scrolls for small total_times */
511     value = floor (adj->value);
512     if (time != value) {
513         value = (time < adj->upper) ? time : adj->upper;
514
515 #if defined(MAEMO_GTK) || GTK_CHECK_VERSION(2,11,0)
516         if (value <= gtk_range_get_fill_level (range)) {
517 #else
518         if (value) {
519 #endif
520             adj->value = value;
521             gtk_adjustment_value_changed (adj);
522
523             g_object_notify (G_OBJECT (seekbar), "position");
524         }
525     }
526 }
527
528 static void 
529 hildon_seekbar_size_request                     (GtkWidget *widget,
530                                                  GtkRequisition *req)
531 {
532     HildonSeekbar *self = NULL;
533     HildonSeekbarPrivate *priv = NULL;
534     GtkWidget *parent = NULL;
535
536     self = HILDON_SEEKBAR (widget);
537     priv = HILDON_SEEKBAR_GET_PRIVATE (self);
538     g_assert (priv);
539
540     parent = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_TOOLBAR);
541
542     priv->is_toolbar = parent ? TRUE : FALSE;
543
544     if (GTK_WIDGET_CLASS (parent_class)->size_request)
545         GTK_WIDGET_CLASS (parent_class)->size_request (widget, req);
546
547     /* Request minimum size, depending on whether the widget is in a
548      * toolbar or not */
549     req->width = priv->is_toolbar ? TOOL_MINIMUM_WIDTH : MINIMUM_WIDTH;
550     req->height = priv->is_toolbar ? TOOL_DEFAULT_HEIGHT : DEFAULT_HEIGHT;
551 }
552
553 static void 
554 hildon_seekbar_size_allocate                    (GtkWidget *widget,
555                                                  GtkAllocation *allocation)
556 {
557     HildonSeekbarPrivate *priv;
558
559     priv = HILDON_SEEKBAR_GET_PRIVATE (widget);
560     g_assert (priv);
561
562     if (priv->is_toolbar == TRUE)
563     {
564         /* Center vertically */
565         if (allocation->height > TOOL_DEFAULT_HEIGHT)
566         {
567             allocation->y +=
568                 (allocation->height - TOOL_DEFAULT_HEIGHT) / 2;
569             allocation->height = TOOL_DEFAULT_HEIGHT;
570         }
571         /* Add space for border */
572         allocation->x += TOOL_EXTRA_SIDE_BORDER;
573         allocation->width -= 2 * TOOL_EXTRA_SIDE_BORDER;
574     }
575     else
576     {
577         /* Center vertically */
578         if (allocation->height > DEFAULT_HEIGHT)
579         {
580             allocation->y += (allocation->height - DEFAULT_HEIGHT) / 2;
581             allocation->height = DEFAULT_HEIGHT;
582         }
583
584         /* Add space for border */
585         allocation->x += EXTRA_SIDE_BORDER;
586         allocation->width -= 2 * EXTRA_SIDE_BORDER;
587     }
588
589     if (GTK_WIDGET_CLASS (parent_class)->size_allocate)
590         GTK_WIDGET_CLASS (parent_class)->size_allocate (widget, allocation);
591 }
592
593 static gboolean
594 hildon_seekbar_expose                           (GtkWidget *widget,
595                                                  GdkEventExpose *event)
596 {
597     HildonSeekbarPrivate *priv;
598     gint extra_side_borders = 0;
599
600     priv = HILDON_SEEKBAR_GET_PRIVATE(widget);
601     g_assert (priv);
602
603     extra_side_borders = priv->is_toolbar ? TOOL_EXTRA_SIDE_BORDER :
604         EXTRA_SIDE_BORDER;
605
606     if (GTK_WIDGET_DRAWABLE (widget)) {
607         /* Paint border */
608         gtk_paint_box(widget->style, widget->window,
609                 GTK_WIDGET_STATE(widget), GTK_SHADOW_OUT,
610                 NULL, widget, "seekbar",
611                 widget->allocation.x - extra_side_borders,
612                 widget->allocation.y,
613                 widget->allocation.width + 2 * extra_side_borders,
614                 widget->allocation.height);
615
616         (*GTK_WIDGET_CLASS (parent_class)->expose_event) (widget, event);
617     }
618
619     return FALSE;
620 }
621
622 /*
623  * Event handler for button press. Changes button1 to button2.
624  */
625 static gboolean
626 hildon_seekbar_button_press_event               (GtkWidget *widget,
627                                                  GdkEventButton *event)
628 {
629     gint result = FALSE;
630
631     /* We change here the button id because we want to use button2
632      * functionality for button1: jump to mouse position
633      * instead of slowly incrementing to it */
634     if (event->button == 1) event->button = 2;
635
636     /* call the parent handler */
637     if (GTK_WIDGET_CLASS (parent_class)->button_press_event)
638         result = GTK_WIDGET_CLASS (parent_class)->button_press_event (widget,
639                 event);
640
641     return result;
642 }
643 /*
644  * Event handler for button release. Changes button1 to button2.
645  */
646 static gboolean
647 hildon_seekbar_button_release_event             (GtkWidget *widget,
648                                                  GdkEventButton *event)
649 {
650     gboolean result = FALSE;
651
652     /* We change here the button id because we want to use button2
653      * functionality for button1: jump to mouse position
654      * instead of slowly incrementing to it */
655     event->button = event->button == 1 ? 2 : event->button;
656
657     /* call the parent handler */
658     if (GTK_WIDGET_CLASS (parent_class)->button_release_event)
659         result = GTK_WIDGET_CLASS (parent_class)->button_release_event (widget,
660                 event);
661
662     return result;
663 }