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