2008-03-03 Sven Herzberg <sven@imendio.com>
[hildon] / src / hildon-time-picker.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-time-picker
27  * @short_description: A dialog popup widget which lets the user set the time.
28  * @see_also: #HildonTimeEditor
29  * 
30  * #HildonTimePicker is a dialog popup widget which lets the user set the time,
31  * using up/down arrows on hours and minutes. There are two arrows for minutes,
32  * so that minutes can be added also in 10 min increments.This widget is mainly 
33  * used as a part of #HildonTimeEditor implementation.
34  * 
35  * <example>
36  * <title>HildonTimePicker example</title>
37  * <programlisting>
38  * <!-- -->
39  * parent = gtk_widget_get_ancestor (GTK_WIDGET (editor), GTK_TYPE_WINDOW);
40  * picker = hildon_time_picker_new (GTK_WINDOW (parent));
41  * <!-- -->
42  * hildon_time_editor_get_time (editor, &amp;h, &amp;m, &amp;s);
43  * hildon_time_picker_set_time( HILDON_TIME_PICKER( picker ), h, m );
44  * <!-- -->
45  * result = gtk_dialog_run (GTK_DIALOG (picker));
46  * switch (result)
47  * {
48  * case GTK_RESPONSE_OK:
49  * case GTK_RESPONSE_ACCEPT:
50  *      hildon_time_picker_get_time(HILDON_TIME_PICKER (picker), &amp;h, &amp;m );
51  *      foo_set_time(h,m);
52  *      break;
53  * default:
54  *      break;
55  * }
56  * <!-- -->
57  * gtk_widget_destroy( picker );
58  * <!-- -->
59  * </programlisting>
60  * </example>
61  */
62
63 #ifdef                                          HAVE_CONFIG_H
64 #include                                        <config.h>
65 #endif
66
67 #include                                        "hildon-time-picker.h"
68 #include                                        "hildon-defines.h"
69 #include                                        <gtk/gtk.h>
70 #include                                        <gdk/gdkkeysyms.h>
71 #include                                        <gdk/gdk.h>
72 #include                                        <time.h>
73 #include                                        <stdio.h>
74 #include                                        <stdlib.h>
75 #include                                        <string.h>
76 #include                                        <langinfo.h>
77 #include                                        <libintl.h>
78 #include                                        "hildon-time-picker-private.h"
79 #include                                        "hildon-time-editor.h"
80
81 #define                                         _(String) \
82                                                 dgettext("hildon-libs", String)
83
84 #define                                         DEFAULT_HOURS 1
85
86 #define                                         DEFAULT_MINUTES 1
87
88 #define                                         DEFAULT_ARROW_WIDTH 26
89
90 #define                                         DEFAULT_ARROW_HEIGHT 26
91
92 #define                                         MINS_IN_1H  (60)
93
94 #define                                         MINS_IN_24H (MINS_IN_1H * 24)
95
96 #define                                         MINS_IN_12H (MINS_IN_1H * 12)
97
98 #define                                         HILDON_TIME_PICKER_LABEL_X_PADDING 0
99
100 #define                                         HILDON_TIME_PICKER_LABEL_Y_PADDING 1
101
102 static void
103 hildon_time_picker_class_init                   (HildonTimePickerClass *klass);
104
105 static void
106 hildon_time_picker_init                         (HildonTimePicker *picker);
107
108 static gboolean
109 hildon_time_picker_key_repeat_timeout           (gpointer tpicker);
110
111 static void
112 hildon_time_picker_change_time                  (HildonTimePicker *picker, 
113                                                  guint minutes);
114
115 static gboolean
116 hildon_time_picker_ampm_release                 (GtkWidget *widget, 
117                                                  GdkEvent *event,
118                                                  HildonTimePicker *picker);
119
120 static gboolean
121 hildon_time_picker_arrow_press                  (GtkWidget *widget, 
122                                                  GdkEvent *event,
123                                                  HildonTimePicker *picker);
124
125 static gboolean
126 hildon_time_picker_arrow_release                (GtkWidget *widget, 
127                                                  GdkEvent *event,
128                                                  HildonTimePicker *picker);
129
130 static void
131 hildon_time_picker_finalize                     (GObject *object);
132
133 static void
134 hildon_time_picker_get_property                 (GObject *object, 
135                                                  guint param_id,
136                                                  GValue *value, 
137                                                  GParamSpec *pspec);
138
139 static void
140 hildon_time_picker_set_property                 (GObject *object, 
141                                                  guint param_id,
142                                                  const GValue *value, 
143                                                  GParamSpec *pspec);
144
145 static gboolean
146 hildon_time_picker_event_box_focus_in           (GtkWidget *widget, 
147                                                  GdkEvent *event,
148                                                  gpointer unused);
149
150 static gboolean
151 hildon_time_picker_event_box_focus_out          (GtkWidget *widget, 
152                                                  GdkEvent *event,
153                                                  gpointer unused);
154
155 static gboolean
156 hildon_time_picker_event_box_key_press          (GtkWidget *widget,  
157                                                  GdkEventKey *event,
158                                                  HildonTimePicker *picker);
159
160 static gboolean
161 hildon_time_picker_event_box_key_release        (GtkWidget *widget, 
162                                                  GdkEventKey *event,
163                                                  HildonTimePicker *picker);
164
165 static gboolean
166 hildon_time_picker_event_box_button_press       (GtkWidget *widget, 
167                                                  GdkEventKey *event,
168                                                  gpointer unused);
169
170 static void
171 hildon_time_picker_realize                      (GtkWidget *widget);
172
173 static void
174 hildon_time_picker_style_set                    (GtkWidget *widget,
175                                                  GtkStyle *previous_style);
176
177 static void
178 frame_size_request                              (GtkWidget *widget, 
179                                                  GtkRequisition *requistion);
180
181 static GtkDialogClass*                          parent_class;
182
183 enum
184 {
185     PROP_0,
186     PROP_MINUTES
187 };
188
189 static const gint                               button_multipliers[WIDGET_GROUP_COUNT][2] =
190 {
191     { MINS_IN_1H, -MINS_IN_1H },
192     { 10, -10 },
193     { 1, -1 },
194     { 0, 0 }
195 };
196
197 /**
198  * hildon_time_picker_get_type:
199  *
200  * Returns the type of HildonTimePicker.
201  *
202  * Returns: HildonTimePicker type
203  */
204 GType G_GNUC_CONST
205 hildon_time_picker_get_type                     (void)
206 {
207     static GType picker_type = 0;
208
209     if( !picker_type )
210     {
211         static const GTypeInfo picker_info =
212         {
213             sizeof (HildonTimePickerClass),
214             NULL,       /* base_init */
215             NULL,       /* base_finalize */
216             (GClassInitFunc)hildon_time_picker_class_init,
217             NULL,       /* class_finalize */
218             NULL,       /* class_data */
219             sizeof (HildonTimePicker),
220             0,          /* n_preallocs */
221             (GInstanceInitFunc)hildon_time_picker_init,
222         };
223         picker_type = g_type_register_static( GTK_TYPE_DIALOG, "HildonTimePicker",
224                 &picker_info, 0 );
225     }
226     return picker_type;
227 }
228
229
230 static void
231 hildon_time_picker_class_init                   (HildonTimePickerClass *klass)
232 {
233     GObjectClass *gobject_class     = G_OBJECT_CLASS (klass);
234     GtkWidgetClass *widget_class    = GTK_WIDGET_CLASS (klass);
235     parent_class = g_type_class_peek_parent (klass);
236
237     gobject_class->finalize = hildon_time_picker_finalize;
238     gobject_class->get_property = hildon_time_picker_get_property;
239     gobject_class->set_property = hildon_time_picker_set_property;
240     widget_class->realize = hildon_time_picker_realize;
241     widget_class->style_set = hildon_time_picker_style_set;
242
243     /**
244      * HildonTimePicker:minutes:
245      *
246      * Currently selected time in minutes since midnight.
247      */
248     g_object_class_install_property (gobject_class, PROP_MINUTES,
249             g_param_spec_uint ("minutes",
250                 "Current minutes",
251                 "The selected time in minutes "
252                 "since midnight",
253                 0, MINS_IN_24H, 0,
254                 G_PARAM_READABLE | G_PARAM_WRITABLE) );
255
256     gtk_widget_class_install_style_property (widget_class,
257             g_param_spec_uint ("arrow-width",
258                 "Arrow width",
259                 "Increase/decrease arrows width.",
260                 0, G_MAXUINT,
261                 DEFAULT_ARROW_WIDTH,
262                 G_PARAM_READABLE) );
263
264     gtk_widget_class_install_style_property (widget_class,
265             g_param_spec_uint ("arrow-height",
266                 "Arrow height",
267                 "Increase/decrease arrows height.",
268                 0, G_MAXUINT,
269                 DEFAULT_ARROW_HEIGHT,
270                 G_PARAM_READABLE) );
271
272     g_type_class_add_private (klass, sizeof (HildonTimePickerPrivate));
273 }
274
275 /* Okay, this is really bad. We make the requisition of the frames a bit larger 
276  * so that it doesn't "change" when digits are changed (see #37489). It's a 
277  * really bad solution to a problem, but the whole layout of the time picker is 
278  * on crack anyways */
279 static void 
280 frame_size_request                              (GtkWidget *widget, 
281                                                  GtkRequisition *requistion)
282 {
283     int framed = requistion->width / 10;
284     requistion->width = (framed + 1) * 10;
285 }
286
287 static void
288 hildon_time_picker_init                         (HildonTimePicker *picker)
289 {
290     HildonTimePickerPrivate *priv = HILDON_TIME_PICKER_GET_PRIVATE (picker);
291     gint widget_group_table_column_pos[WIDGET_GROUP_COUNT];
292     GtkDialog *dialog = GTK_DIALOG (picker);
293     GtkTable *table = NULL;
294     GtkWidget *maintocenter, *colon_label;
295     const struct tm *local = NULL;
296     time_t stamp;
297     gint i = 0;
298
299     g_assert (priv);
300
301     widget_group_table_column_pos[WIDGET_GROUP_HOURS] = 1;
302     widget_group_table_column_pos[WIDGET_GROUP_10_MINUTES] = 3;
303     widget_group_table_column_pos[WIDGET_GROUP_1_MINUTES] = 4;
304     widget_group_table_column_pos[WIDGET_GROUP_AMPM] = 5;
305
306     /* Get AM/PM strings from locale. If they're set, the time is wanted
307        in 12 hour mode. */
308     priv->am_symbol = g_strdup (nl_langinfo (AM_STR));
309     priv->pm_symbol = g_strdup (nl_langinfo (PM_STR));
310
311     priv->show_ampm = priv->am_symbol[0] != '\0';
312     if (priv->show_ampm)
313     {
314         /* Check if AM/PM should be before or after time.
315            %p is the AM/PM string, so we assume that if the format string
316            begins with %p it's in the beginning, and in any other case it's
317            in the end (although that's not necessarily the case). */
318         if (strncmp (nl_langinfo (T_FMT_AMPM), "%p", 2) == 0)
319         {
320             /* Before time. Update column position. */
321             priv->ampm_left = TRUE;
322             widget_group_table_column_pos[WIDGET_GROUP_AMPM] = 0;
323         }
324     }
325
326     gtk_widget_push_composite_child ();
327
328     /* Pack all our internal widgets into a table */
329     table = GTK_TABLE (gtk_table_new (3, 6, FALSE));
330
331     /* Put everything centered into window */
332     maintocenter = gtk_alignment_new (0.5, 0, 0, 0);
333
334     /* Create our internal widgets */
335     for (i = 0; i < WIDGET_GROUP_COUNT; i++)
336     {
337         HildonTimePickerWidgetGroup *group = &priv->widgets[i];
338         gint table_column = widget_group_table_column_pos[i];
339
340         /* Create frame and attach to table. With AM/PM label we're attaching
341            it later. */
342         group->frame = gtk_frame_new (NULL);
343         if (i != WIDGET_GROUP_AMPM)
344         {
345             gtk_table_attach (table, group->frame, table_column, table_column + 1,
346                     1, 2, GTK_EXPAND, GTK_EXPAND, 0, 0);
347
348
349         }
350         /* FIXME: is it needed to force it to 0 here? */
351         gtk_container_set_border_width (GTK_CONTAINER(group->frame), 0);
352
353         /* Create eventbox inside frame */
354         group->eventbox = gtk_event_box_new ();
355         gtk_container_add (GTK_CONTAINER (group->frame), group->eventbox);
356
357         g_object_set (group->eventbox, "can-focus", TRUE, NULL);
358         gtk_widget_set_events (group->eventbox,
359                 GDK_FOCUS_CHANGE_MASK | GDK_BUTTON_PRESS_MASK);
360
361         /* Connect signals to eventbox */
362         g_signal_connect (group->eventbox, "key-release-event",
363                 G_CALLBACK (hildon_time_picker_event_box_key_release),
364                 picker);
365         g_signal_connect (group->eventbox, "key-press-event",
366                 G_CALLBACK (hildon_time_picker_event_box_key_press),
367                 picker);
368         g_signal_connect (group->eventbox, "focus-in-event",
369                 G_CALLBACK (hildon_time_picker_event_box_focus_in),
370                 picker);
371         g_signal_connect (group->eventbox, "focus-out-event",
372                 G_CALLBACK (hildon_time_picker_event_box_focus_out),
373                 picker);
374         g_signal_connect (group->eventbox, "button-press-event",
375                 G_CALLBACK (hildon_time_picker_event_box_button_press),
376                 picker);
377
378         /* Create label inside eventbox */
379         group->label = GTK_LABEL (gtk_label_new (NULL));
380         g_signal_connect (group->frame, "size-request",
381                 G_CALLBACK (frame_size_request),
382                 NULL);
383         gtk_misc_set_alignment (GTK_MISC (group->label), 0.5, 0.5);
384         gtk_container_add (GTK_CONTAINER (group->eventbox), GTK_WIDGET (group->label));
385
386         if (i != WIDGET_GROUP_AMPM)
387         {
388             gint button;
389
390             /* Add some padding to hour and minute labels, and make them bigger */
391             gtk_misc_set_padding(GTK_MISC (group->label),
392                     HILDON_TIME_PICKER_LABEL_X_PADDING,
393                     HILDON_TIME_PICKER_LABEL_Y_PADDING);
394
395             gtk_widget_set_name (GTK_WIDGET(group->label), "osso-LargeFont");
396
397             /* Create up and down buttons for hours and mins */
398             for (button = 0; button < BUTTON_COUNT; button++)
399             {
400                 gint table_row = button == BUTTON_UP ? 0 : 2;
401
402                 group->buttons[button] = gtk_button_new ();
403                 gtk_table_attach (table, group->buttons[button],
404                         table_column, table_column + 1,
405                         table_row, table_row + 1,
406                         GTK_SHRINK, GTK_SHRINK, 0, 0);
407                 g_object_set (group->buttons[button], "can-focus", FALSE, NULL);
408
409                 /* Connect signals */
410                 g_signal_connect(group->buttons[button], "button-press-event",
411                         G_CALLBACK (hildon_time_picker_arrow_press), picker);
412                 g_signal_connect(group->buttons[button], "button-release-event",
413                         G_CALLBACK (hildon_time_picker_arrow_release), picker);
414             }
415
416             gtk_widget_set_name (group->buttons[BUTTON_UP],
417                     "hildon-time-picker-up");
418             gtk_widget_set_name (group->buttons[BUTTON_DOWN],
419                     "hildon-time-picker-down");
420         }
421     }
422
423     /* Label between hour and minutes */
424     colon_label = gtk_label_new (NULL);
425     hildon_time_editor_get_time_separators (GTK_LABEL(colon_label), NULL);
426
427     gtk_table_attach (table, colon_label, 2, 3, 1, 2,
428             GTK_SHRINK, GTK_SHRINK, 6, 0); /* FIXME: magic */
429     gtk_widget_set_name (colon_label, "osso-LargeFont" );
430
431     priv->minutes = 0;
432     priv->mul = 0;
433     priv->start_key_repeat = FALSE;
434     priv->timer_id = 0;
435     priv->button_press = FALSE;
436
437     gtk_table_set_row_spacing (table, 0, 6);
438     gtk_table_set_row_spacing (table, 1, 6);
439
440     if (priv->show_ampm)
441     {
442         gint table_column = widget_group_table_column_pos[WIDGET_GROUP_AMPM];
443         GtkWidget *ampmtotop = NULL;
444
445         /* Show the AM/PM label centered vertically */
446         ampmtotop = gtk_alignment_new (0, 0.5, 0, 0);
447         gtk_table_attach (table, ampmtotop, table_column, table_column + 1,
448                 1, 2, GTK_SHRINK, GTK_SHRINK, 0, 0);
449         gtk_container_add (GTK_CONTAINER (ampmtotop),
450                 priv->widgets[WIDGET_GROUP_AMPM].frame);
451
452         if (table_column != 0)
453             gtk_table_set_col_spacing (table, table_column - 1, 9);
454
455         /* Connect AM/PM signal handlers */
456         g_signal_connect (priv->widgets[WIDGET_GROUP_AMPM].eventbox,
457                 "button-release-event",
458                 G_CALLBACK(hildon_time_picker_ampm_release), picker);
459     }
460
461     gtk_widget_pop_composite_child ();
462
463     /* This dialog isn't modal */
464     gtk_window_set_modal (GTK_WINDOW (dialog), FALSE);
465     /* And final dialog packing */
466     gtk_dialog_set_has_separator (dialog, FALSE);
467     gtk_dialog_add_button (dialog, _("ecdg_bd_time_picker_close"),
468             GTK_RESPONSE_OK);
469
470     gtk_container_add (GTK_CONTAINER (maintocenter), GTK_WIDGET(table));
471     gtk_box_pack_start (GTK_BOX (dialog->vbox), maintocenter, TRUE, FALSE, 0);
472
473     /* Set default time to current time */
474     stamp = time (NULL);
475     local = localtime (&stamp);
476     hildon_time_picker_set_time (picker, local->tm_hour, local->tm_min);
477
478     gtk_widget_show_all (maintocenter);
479 }
480
481 static void
482 hildon_time_picker_set_property                 (GObject *object, 
483                                                  guint param_id,
484                                                  const GValue *value, 
485                                                  GParamSpec *pspec)
486 {
487     HildonTimePicker *picker = HILDON_TIME_PICKER (object);
488
489     switch (param_id)
490     {
491
492         case PROP_MINUTES:
493             hildon_time_picker_change_time (picker, g_value_get_uint(value));
494             break;
495
496         default:
497             G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
498             break;
499     }
500 }
501
502 static void
503 hildon_time_picker_finalize                     (GObject *object)
504 {
505     HildonTimePickerPrivate *priv = HILDON_TIME_PICKER_GET_PRIVATE (object);
506
507     g_assert (priv);
508
509     /* Make sure the timer is stopped */
510     if (priv->timer_id)
511         g_source_remove(priv->timer_id);
512
513     g_free(priv->am_symbol);
514     g_free(priv->pm_symbol);
515
516     if (G_OBJECT_CLASS(parent_class)->finalize)
517         G_OBJECT_CLASS(parent_class)->finalize(object);
518 }
519
520 static void
521 hildon_time_picker_get_property                 (GObject *object, 
522                                                  guint param_id,
523                                                  GValue *value, 
524                                                  GParamSpec *pspec)
525 {
526     HildonTimePickerPrivate *priv = HILDON_TIME_PICKER_GET_PRIVATE (object);
527     g_assert (priv);
528
529     switch( param_id )
530     {
531         case PROP_MINUTES:
532             g_value_set_uint (value, priv->minutes);
533             break;
534
535         default:
536             G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
537             break;
538     }
539 }
540
541 static void
542 hildon_time_picker_realize                      (GtkWidget *widget)
543 {
544     GTK_WIDGET_CLASS (parent_class)->realize(widget);
545
546     /* We only want the border for the dialog. */
547     gdk_window_set_decorations (widget->window, GDK_DECOR_BORDER);
548 }
549
550 static void
551 hildon_time_picker_style_set                    (GtkWidget *widget,
552                                                  GtkStyle *previous_style)
553 {
554     guint width, height;
555     gint i, button;
556     HildonTimePickerPrivate *priv = HILDON_TIME_PICKER_GET_PRIVATE (widget);
557     g_assert (priv);
558
559     GTK_WIDGET_CLASS (parent_class)->style_set(widget, previous_style);
560
561     /* Update hour/minute up/down buttons sizes from style properties */
562     gtk_widget_style_get (widget,
563             "arrow-width", &width,
564             "arrow-height", &height, NULL);
565
566     for (i = 0; i < WIDGET_GROUP_COUNT; i++)
567     {
568         if (priv->widgets[i].buttons[0] != NULL)
569         {
570             for (button = 0; button < BUTTON_COUNT; button++)
571             {
572                 gtk_widget_set_size_request (priv->widgets[i].buttons[button], width, height);
573             }
574         }
575     }
576 }
577
578 /* 
579  * Clicked hour/minute field. Move focus to it. 
580  */
581 static gboolean
582 hildon_time_picker_event_box_button_press       (GtkWidget *widget,
583                                                  GdkEventKey *event, 
584                                                  gpointer unused)
585 {
586     gtk_widget_grab_focus (widget);
587     return FALSE;
588 }
589
590 /* 
591  * Clicked AM/PM label. Move focus to it and move the time by 12 hours. 
592  */
593 static gboolean
594 hildon_time_picker_ampm_release                 (GtkWidget *widget, 
595                                                  GdkEvent *event,
596                                                  HildonTimePicker *picker)
597 {
598     HildonTimePickerPrivate *priv = HILDON_TIME_PICKER_GET_PRIVATE (picker);
599     g_assert (priv);
600
601     gtk_widget_grab_focus (widget);
602     hildon_time_picker_change_time (picker, priv->minutes > MINS_IN_12H ?
603             priv->minutes - MINS_IN_12H :
604             priv->minutes + MINS_IN_12H);
605
606     return FALSE;
607 }
608
609 static gboolean
610 hildon_time_picker_arrow_press                  (GtkWidget *widget, 
611                                                  GdkEvent *event,
612                                                  HildonTimePicker *picker)
613 {
614     HildonTimePickerPrivate *priv = HILDON_TIME_PICKER_GET_PRIVATE (picker);
615     gint i, button;
616     gint newval = 0;
617     gint key_repeat = 0;
618
619     /* Make sure we don't add repeat timer twice. Normally it shouldn't
620        happen but WM can cause button release to be lost. */
621     if (priv->button_press )
622         return FALSE;
623
624     priv->start_key_repeat = priv->button_press = TRUE;
625
626     /* Find the widget which was clicked */
627     priv->mul = 0;
628     for (i = 0; i < WIDGET_GROUP_COUNT; i++)
629     {
630         for (button = 0; button < BUTTON_COUNT; button++)
631         {
632             if (priv->widgets[i].buttons[button] == widget)
633             {
634                 /* Update multiplier and move the focus to the clicked field */
635                 priv->mul = button_multipliers[i][button];
636                 gtk_widget_grab_focus (priv->widgets[i].eventbox);
637                 break;
638             }
639         }
640     }
641     g_assert (priv->mul != 0);
642
643     /* Change the time now, wrapping if needed. */
644     newval = priv->minutes + priv->mul;
645     if( newval < 0 )
646         newval += MINS_IN_24H;
647
648     hildon_time_picker_change_time (picker, newval);
649
650     /* Get button press repeater timeout from settings (in milliseconds) */
651     g_object_get (gtk_widget_get_settings (widget), 
652                     "gtk-timeout-repeat", &key_repeat, NULL);
653
654     key_repeat *= 8;
655
656     /* Keep changing the time as long as button is being pressed.
657        The first repeat takes 3 times longer to start than the rest. */
658     
659     priv->timer_id = g_timeout_add (key_repeat * 3,
660             hildon_time_picker_key_repeat_timeout,
661             picker);
662
663     return FALSE;
664 }
665
666 static gboolean
667 hildon_time_picker_arrow_release                (GtkWidget *widget, 
668                                                  GdkEvent *event,
669                                                  HildonTimePicker *picker)
670 {
671     HildonTimePickerPrivate *priv = HILDON_TIME_PICKER_GET_PRIVATE (picker);
672     g_assert (priv);
673
674     if (priv->timer_id)
675     {
676         /* Stop repeat timer */
677         g_source_remove (priv->timer_id);
678         priv->timer_id = 0;
679     }
680     
681     priv->button_press = FALSE;
682     return FALSE;
683 }
684
685 static gboolean
686 hildon_time_picker_event_box_focus_in           (GtkWidget *widget, 
687                                                  GdkEvent *event,
688                                                  gpointer unused)
689 {
690     /* Draw the widget in selected state so focus shows clearly. */
691     gtk_widget_set_state (widget, GTK_STATE_SELECTED);
692     return FALSE;
693 }
694
695 static gboolean
696 hildon_time_picker_event_box_focus_out          (GtkWidget *widget, 
697                                                  GdkEvent *event,
698                                                  gpointer unused)
699 {
700     /* Draw the widget in normal state */
701     gtk_widget_set_state( widget, GTK_STATE_NORMAL );
702     return FALSE;
703 }
704
705 static gint
706 hildon_time_picker_lookup_eventbox_group        (HildonTimePicker *picker,
707                                                  GtkWidget *widget)
708 {
709     gint i;
710     HildonTimePickerPrivate *priv = HILDON_TIME_PICKER_GET_PRIVATE (picker);
711
712     g_assert (priv);
713
714     for (i = 0; i < WIDGET_GROUP_COUNT; i++)
715     {
716         if (priv->widgets[i].eventbox == widget)
717             return i;
718     }
719     return -1;
720 }
721
722 static gboolean
723 hildon_time_picker_event_box_key_press          (GtkWidget *widget, 
724                                                  GdkEventKey *event,
725                                                  HildonTimePicker *picker)
726 {
727     HildonTimePickerPrivate *priv = HILDON_TIME_PICKER_GET_PRIVATE (picker);
728     HildonTimePickerWidgetGroup *group;
729     gint group_idx;
730
731     g_assert (priv);
732
733     /* If mouse button is already being pressed, ignore this keypress */
734     if (priv->timer_id )
735         return TRUE;
736
737     group_idx = hildon_time_picker_lookup_eventbox_group (picker, widget);
738     group = group_idx < 0 ? NULL : &priv->widgets[group_idx];
739
740     /* Handle keypresses in hour/minute/AMPM fields */
741     switch (event->keyval)
742     {
743         case GDK_Up:
744         case GDK_Down:
745             if (group != NULL)
746             {
747                 gint button = event->keyval == GDK_Up ? BUTTON_UP : BUTTON_DOWN;
748
749                 if (group->buttons[button] != NULL)
750                 {
751                     /* Fake a button up/down press */
752                     hildon_time_picker_arrow_press (group->buttons[button], NULL, picker);
753                     gtk_widget_set_state (group->buttons[button], GTK_STATE_SELECTED);
754                 }
755                 else
756                 {
757                     /* Fake a AM/PM button release */
758                     g_assert (group_idx == WIDGET_GROUP_AMPM);
759                     hildon_time_picker_ampm_release (group->eventbox, NULL, picker);
760                 }
761             }
762             return TRUE;
763
764         case GDK_Left:
765             /* If we're in leftmost field, stop this keypress signal.
766                Otherwise let the default key handler move focus to field in left. */
767             if (priv->show_ampm && priv->ampm_left)
768             {
769                 /* AM/PM is the leftmost field */
770                 if (group_idx == WIDGET_GROUP_AMPM)
771                     return TRUE;
772             }
773             else
774             {
775                 /* Hours is the leftmost field */
776                 if (group_idx == WIDGET_GROUP_HOURS)
777                     return TRUE;
778             }
779             break;
780
781         case GDK_Right:
782             /* If we're in rightmost field, stop this keypress signal.
783                Otherwise let the default key handler move focus to field in right. */
784             if (priv->show_ampm && !priv->ampm_left)
785             {
786                 /* AM/PM is the rightmost field */
787                 if (group_idx == WIDGET_GROUP_AMPM)
788                     return TRUE;
789             }
790             else
791             {
792                 /* 1-minutes is the leftmost field */
793                 if (group_idx == WIDGET_GROUP_1_MINUTES)
794                     return TRUE;
795             }
796             break;
797
798         case GDK_Escape:
799             gtk_dialog_response (GTK_DIALOG (picker), GTK_RESPONSE_CANCEL);
800             return TRUE;
801
802         case GDK_Return:
803             gtk_dialog_response (GTK_DIALOG (picker), GTK_RESPONSE_OK);
804             return TRUE;
805     }
806
807     return FALSE;
808 }
809
810 static gboolean
811 hildon_time_picker_event_box_key_release        (GtkWidget *widget, 
812                                                  GdkEventKey *event,
813                                                  HildonTimePicker *picker)
814 {
815     HildonTimePickerWidgetGroup *group;
816     gint group_idx;
817     HildonTimePickerPrivate *priv = HILDON_TIME_PICKER_GET_PRIVATE (picker);
818
819     g_assert (priv);
820
821     /* Fake a button release if in key-press handler we faked a button press. */
822     switch( event->keyval )
823     {
824         case GDK_Up:
825         case GDK_Down:
826             group_idx = hildon_time_picker_lookup_eventbox_group (picker, widget);
827             if (group_idx >= 0)
828             {
829                 gint button = event->keyval == GDK_Up ? BUTTON_UP : BUTTON_DOWN;
830
831                 group = &priv->widgets[group_idx];
832                 if (group->buttons[button] != NULL)
833                 {
834                     /* Fake a button up/down press */
835                     gtk_widget_set_state (group->buttons[button], GTK_STATE_NORMAL);
836                     hildon_time_picker_arrow_release (group->buttons[button], NULL, picker);
837                 }
838             }
839             break;
840     }
841     return FALSE;
842 }
843
844 /* Button up/down is being pressed. Update the time. */
845 static gboolean
846 hildon_time_picker_key_repeat_timeout           (gpointer tpicker)
847 {
848     HildonTimePicker *picker;
849     HildonTimePickerPrivate *priv = NULL;
850     gint newval = 0;
851     gint key_repeat = 0;
852
853     picker = HILDON_TIME_PICKER(tpicker);
854     g_assert(picker != NULL);
855
856     priv = HILDON_TIME_PICKER_GET_PRIVATE (tpicker);
857     g_assert (priv);
858
859     GDK_THREADS_ENTER ();
860
861     /* Change the time, wrapping if needed */
862     newval = priv->minutes + priv->mul;
863     if (newval < 0)
864         newval += MINS_IN_24H;
865
866     hildon_time_picker_change_time (picker, newval);
867
868     if (priv->start_key_repeat)
869     {
870         /* Get button press repeater timeout from settings (in milliseconds) */
871         g_object_get (gtk_widget_get_settings ((GtkWidget *) tpicker), 
872                         "gtk-timeout-repeat", &key_repeat, NULL);
873
874         key_repeat *= 8;
875             
876         /* This is the first repeat. Shorten the timeout to key_repeat
877            (instead of the first time's 3*key_repeat) */
878         priv->timer_id = g_timeout_add (key_repeat,
879                 hildon_time_picker_key_repeat_timeout,
880                 picker);
881         priv->start_key_repeat = FALSE;
882
883         GDK_THREADS_LEAVE ();
884         return FALSE;
885     }
886
887     GDK_THREADS_LEAVE ();
888     return TRUE;
889 }
890
891 static void
892 hildon_time_picker_change_time                  (HildonTimePicker *picker, 
893                                                  guint minutes)
894 {
895     HildonTimePickerPrivate *priv = HILDON_TIME_PICKER_GET_PRIVATE (picker);
896
897     gchar str[3] = "00";
898     guint hours = 0;
899     gboolean ampm = TRUE;
900
901     g_assert (priv);
902
903     /* If the minutes isn't in valid range, wrap them. */
904     minutes %= MINS_IN_24H;
905
906     if (priv->minutes == minutes)
907         return;
908
909     /* Minutes changed. Update widgets to show the new time. */
910     priv->minutes = minutes;
911
912     if (priv->show_ampm)
913     {
914         /* am < 12:00 <= pm */
915         ampm = !((guint)(minutes / MINS_IN_12H));
916         /* 12:00 - 23:59 -> 00:00 - 11:59 */
917         minutes %= MINS_IN_12H;
918         if (minutes < MINS_IN_1H )
919             /* 00:mm is always shown as 12:mm */
920             minutes += MINS_IN_12H;
921
922         /* Update the AM/PM label */
923         gtk_label_set_text (priv->widgets[WIDGET_GROUP_AMPM].label,
924                 ampm ? priv->am_symbol : priv->pm_symbol);
925     }
926
927     /* Update hour and minute fields */
928     hours = minutes / MINS_IN_1H;
929     minutes %= MINS_IN_1H;
930
931     snprintf(str, sizeof (str), "%02d", hours);
932     gtk_label_set_text (priv->widgets[WIDGET_GROUP_HOURS].label, str);
933
934     snprintf(str, sizeof (str), "%d", minutes / 10);
935     gtk_label_set_text (priv->widgets[WIDGET_GROUP_10_MINUTES].label, str);
936
937     snprintf(str, sizeof (str), "%d", minutes % 10);
938     gtk_label_set_text(priv->widgets[WIDGET_GROUP_1_MINUTES].label, str);
939
940     g_object_notify (G_OBJECT(picker), "minutes");
941 }
942
943 /**
944  * hildon_time_picker_new:
945  * @parent: parent window
946  *
947  * #HildonTimePicker shows time picker dialog. The close button is placed
948  * in the dialog's action area and time picker is placed in dialogs vbox.
949  * The actual time picker consists of two #GtkLabel fields - one for hours 
950  * and one for minutes - and an AM/PM button. A colon (:) is placed
951  * between hour and minute fields.
952  *
953  * Returns: pointer to a new #HildonTimePicker widget.
954  */
955 GtkWidget*
956 hildon_time_picker_new                          (GtkWindow *parent)
957 {
958     GtkWidget *widget = g_object_new (HILDON_TYPE_TIME_PICKER,
959             "minutes", 360, NULL );
960
961     if (parent)
962         gtk_window_set_transient_for (GTK_WINDOW(widget), parent);
963
964     return GTK_WIDGET (widget);
965 }
966
967 /**
968  * hildon_time_picker_set_time:
969  * @picker: the #HildonTimePicker widget
970  * @hours: hours
971  * @minutes: minutes
972  *
973  * Sets the time of the #HildonTimePicker widget.
974  */
975 void
976 hildon_time_picker_set_time                     (HildonTimePicker *picker,
977                                                  guint hours, 
978                                                  guint minutes )
979 {
980     g_return_if_fail (HILDON_IS_TIME_PICKER(picker));
981     hildon_time_picker_change_time (picker, hours * MINS_IN_1H + minutes);
982 }
983
984 /**
985  * hildon_time_picker_get_time:
986  * @picker: the #HildonTimePicker widget
987  * @hours: hours
988  * @minutes: minutes
989  *
990  * Gets the time of the #HildonTimePicker widget.
991  */
992 void 
993 hildon_time_picker_get_time                     (HildonTimePicker *picker,
994                                                  guint *hours, 
995                                                  guint *minutes )
996 {
997     guint current;
998     g_return_if_fail (HILDON_IS_TIME_PICKER (picker));
999
1000     HildonTimePickerPrivate *priv = HILDON_TIME_PICKER_GET_PRIVATE (picker);
1001     g_assert (priv);
1002
1003     current = priv->minutes;
1004     *hours = current / MINS_IN_1H;
1005     *minutes = current % MINS_IN_1H;
1006 }
1007
1008