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