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