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