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