Fix focus out handler of number editor
[modest] / src / hildon2 / modest-number-editor.c
1 /*
2  * This file is a part of modest
3  *
4  * Copyright (C) 2005, 2006, 2008 Nokia Corporation, all rights reserved.
5  *
6  */
7
8 /**
9  * SECTION:modest-number-editor
10  * @short_description: A widget used to enter a number within a pre-defined range.
11  *
12  * ModestNumberEditor is used to enter a number from a specific range. 
13  * There are two buttons to scroll the value in number field. 
14  * Manual input is also possible.
15  *
16  * <example>
17  * <title>ModestNumberEditor example</title>
18  * <programlisting>
19  * number_editor = modest_number_editor_new (-250, 500);
20  * modest_number_editor_set_range (number_editor, 0, 100);
21  * </programlisting>
22  * </example>
23  */
24
25 #undef                                          MODEST_DISABLE_DEPRECATED
26
27 #ifdef                                          HAVE_CONFIG_H
28 #include                                        <config.h>
29 #endif
30
31 #include                                        <string.h>
32 #include                                        <stdio.h>
33 #include                                        <stdlib.h>
34 #include                                        <libintl.h>
35 #include                                        <gdk/gdkkeysyms.h>
36
37 #include                                        "modest-number-editor.h"
38 #include                                        "modest-marshal.h"
39 #include                                        <hildon/hildon-banner.h>
40 #include                                        "modest-text-utils.h"
41
42 #define                                         _(String) dgettext("modest-libs", String)
43
44 typedef struct                                  _ModestNumberEditorPrivate ModestNumberEditorPrivate;
45
46 #define                                         MODEST_NUMBER_EDITOR_GET_PRIVATE(obj) \
47                                                 (G_TYPE_INSTANCE_GET_PRIVATE ((obj), MODEST_TYPE_NUMBER_EDITOR, \
48                                                 ModestNumberEditorPrivate));
49
50 struct                                          _ModestNumberEditorPrivate
51 {
52         gint start; /* Minimum */
53         gint end;   /* Maximum */
54         gint default_val;
55         gboolean is_valid;
56
57         /* Timer IDs */
58         guint select_all_idle_id; /* Selection repaint hack
59                                      see modest_number_editor_select_all */
60 };
61
62
63 static void
64 modest_number_editor_class_init                 (ModestNumberEditorClass *editor_class);
65
66 static void
67 modest_number_editor_init                       (ModestNumberEditor *editor);
68
69 static gboolean
70 modest_number_editor_entry_focusout             (GtkWidget *widget, 
71                                                  GdkEventFocus *event,
72                                                  gpointer data);
73
74 static void
75 modest_number_editor_entry_changed              (GtkWidget *widget, 
76                                                  gpointer data);
77
78 static void
79 modest_number_editor_finalize                   (GObject *self);
80
81 static gboolean
82 modest_number_editor_range_error                (ModestNumberEditor *editor,
83                                                  ModestNumberEditorErrorType type);
84
85 static gboolean
86 modest_number_editor_select_all                 (ModestNumberEditor *editor);
87
88 static void
89 modest_number_editor_validate_value             (ModestNumberEditor *editor, 
90                                                  gboolean allow_intermediate);
91     
92 static void 
93 modest_number_editor_set_property               (GObject * object,
94                                                  guint prop_id,
95                                                  const GValue * value,
96                                                  GParamSpec * pspec);
97
98 static void
99 modest_number_editor_get_property               (GObject *object,
100                                                  guint prop_id,
101                                                  GValue *value, 
102                                                  GParamSpec * pspec);
103
104 enum
105 {
106     RANGE_ERROR,
107     VALID_CHANGED,
108     LAST_SIGNAL
109 };
110
111 enum {
112     PROP_0,
113     PROP_VALUE
114 };
115
116 static GtkContainerClass*                       parent_class;
117
118 static guint                                    ModestNumberEditor_signal[LAST_SIGNAL] = {0};
119
120 /**
121  * modest_number_editor_get_type:
122  *
123  * Returns GType for ModestNumberEditor.
124  *
125  * Returns: ModestNumberEditor type
126  */
127 GType G_GNUC_CONST
128 modest_number_editor_get_type                   (void)
129 {
130     static GType editor_type = 0;
131
132     if (!editor_type)
133     {
134         static const GTypeInfo editor_info =
135         {
136             sizeof (ModestNumberEditorClass),
137             NULL,       /* base_init */
138             NULL,       /* base_finalize */
139             (GClassInitFunc) modest_number_editor_class_init,
140             NULL,       /* class_finalize */
141             NULL,       /* class_data */
142             sizeof (ModestNumberEditor),
143             0,  /* n_preallocs */
144             (GInstanceInitFunc) modest_number_editor_init,
145         };
146         editor_type = g_type_register_static (HILDON_TYPE_ENTRY,
147                 "ModestNumberEditor",
148                 &editor_info, 0);
149     }
150     return editor_type;
151 }
152
153 static void
154 modest_number_editor_class_init                 (ModestNumberEditorClass *editor_class)
155 {
156     GObjectClass *gobject_class = G_OBJECT_CLASS (editor_class);
157
158     g_type_class_add_private (editor_class,
159             sizeof (ModestNumberEditorPrivate));
160
161     parent_class = g_type_class_peek_parent (editor_class);
162
163     editor_class->range_error = modest_number_editor_range_error;
164
165     gobject_class->finalize                 = modest_number_editor_finalize;
166     gobject_class->set_property             = modest_number_editor_set_property;
167     gobject_class->get_property             = modest_number_editor_get_property;
168
169     /**
170      * ModestNumberEditor:value:
171      *
172      * The current value of the number editor.
173      */
174     g_object_class_install_property (gobject_class, PROP_VALUE,
175             g_param_spec_int ("value",
176                 "Value",
177                 "The current value of number editor",
178                 G_MININT,
179                 G_MAXINT,
180                 0, G_PARAM_READWRITE));
181
182     ModestNumberEditor_signal[RANGE_ERROR] =
183         g_signal_new ("range_error", MODEST_TYPE_NUMBER_EDITOR,
184                 G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET
185                 (ModestNumberEditorClass, range_error),
186                 g_signal_accumulator_true_handled, NULL,
187                 modest_marshal_BOOLEAN__ENUM,
188                 G_TYPE_BOOLEAN, 1, MODEST_TYPE_NUMBER_EDITOR_ERROR_TYPE);
189
190     ModestNumberEditor_signal[VALID_CHANGED] =
191         g_signal_new ("valid_changed", MODEST_TYPE_NUMBER_EDITOR,
192                 G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET
193                 (ModestNumberEditorClass, valid_changed),
194                 NULL, NULL,
195                 g_cclosure_marshal_VOID__BOOLEAN,
196                 G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
197 }
198
199 static void
200 modest_number_editor_finalize                   (GObject *self)
201 {
202     ModestNumberEditorPrivate *priv;
203
204     priv = MODEST_NUMBER_EDITOR_GET_PRIVATE (self);
205     g_assert (priv);
206
207     if (priv->select_all_idle_id)
208         g_source_remove (priv->select_all_idle_id);
209
210     /* Call parent class finalize, if have one */
211     if (G_OBJECT_CLASS (parent_class)->finalize)
212         G_OBJECT_CLASS (parent_class)->finalize(self);
213 }
214
215 static void
216 modest_number_editor_init                       (ModestNumberEditor *editor)
217 {
218     ModestNumberEditorPrivate *priv;
219
220     priv = MODEST_NUMBER_EDITOR_GET_PRIVATE (editor);
221     g_assert (priv);
222
223     priv->select_all_idle_id = 0;
224
225     /* Connect child widget signals */
226     g_signal_connect (GTK_OBJECT (editor), "changed",
227             G_CALLBACK (modest_number_editor_entry_changed),
228             editor);
229
230     g_signal_connect (GTK_OBJECT (editor), "focus-out-event",
231             G_CALLBACK (modest_number_editor_entry_focusout),
232             editor);
233
234     hildon_gtk_entry_set_input_mode (GTK_ENTRY (editor), 
235                                      HILDON_GTK_INPUT_MODE_NUMERIC);
236
237     modest_number_editor_set_range (editor, G_MININT, G_MAXINT);
238
239     priv->is_valid = TRUE;
240 }
241
242 /* Format given number to editor field, no checks performed, all signals
243    are sent normally. */
244 static void
245 modest_number_editor_real_set_value             (ModestNumberEditor *editor, 
246                                                  gint value)
247 {
248     /* FIXME: That looks REALLY bad */
249     gchar buffer[32];
250
251     /* Update text in entry to new value */
252     g_snprintf (buffer, sizeof (buffer), "%d", value);
253     gtk_entry_set_text (GTK_ENTRY (editor), buffer);
254 }
255
256 static void
257 add_select_all_idle                             (ModestNumberEditor *editor)
258 {
259     ModestNumberEditorPrivate *priv;
260
261     priv = MODEST_NUMBER_EDITOR_GET_PRIVATE (editor);
262
263     if (! priv->select_all_idle_id)
264     {
265         priv->select_all_idle_id =
266             g_idle_add((GSourceFunc) modest_number_editor_select_all, editor);
267     }    
268 }
269
270 static void
271 modest_number_editor_validate_value             (ModestNumberEditor *editor, 
272                                                  gboolean allow_intermediate)
273 {
274         ModestNumberEditorPrivate *priv;
275         gint error_code, fixup_value;
276         const gchar *text;
277         long value;
278         gchar *tail;
279         gboolean r;
280         gboolean is_valid = TRUE;
281
282         g_assert (MODEST_IS_NUMBER_EDITOR(editor));
283         
284         priv = MODEST_NUMBER_EDITOR_GET_PRIVATE (editor);
285         g_assert (priv);
286
287         text = gtk_entry_get_text (GTK_ENTRY (editor));
288         error_code = -1;
289         fixup_value = priv->default_val;
290
291         if (text && text[0]) {
292                 /* Try to convert entry text to number */
293                 value = strtol (text, &tail, 10);
294
295                 /* Check if conversion succeeded */
296                 if (tail[0] == 0) {
297                         /* Check if value is in allowed range. This is tricky in those
298                            cases when user is editing a value. 
299                            For example: Range = [100, 500] and user have just inputted "4".
300                            This should not lead into error message. Otherwise value is
301                            resetted back to "100" and next "4" press will reset it back
302                            and so on. */
303
304                         if (allow_intermediate) {
305                                 /* We now have the following error cases:
306                                  * If inputted value as above maximum and
307                                  maximum is either positive or then maximum
308                                  negative and value is positive.
309                                  * If inputted value is below minimum and minimum
310                                  is negative or minumum positive and value
311                                  negative or zero.
312                                  In all other cases situation can be fixed just by
313                                  adding new numbers to the string.
314                                 */
315                                 if (value > priv->end && (priv->end >= 0 || (priv->end < 0 && value >= 0))) {
316                                         error_code = MODEST_NUMBER_EDITOR_ERROR_MAXIMUM_VALUE_EXCEED;
317                                         fixup_value = priv->end;
318                                         is_valid = FALSE;
319                                 } else if (value < priv->start && (priv->start < 0 || (priv->start >= 0 && value <= 0))) {
320                                         error_code = MODEST_NUMBER_EDITOR_ERROR_MINIMUM_VALUE_EXCEED;
321                                         fixup_value = priv->start;
322                                         is_valid = FALSE;
323                                 }
324                         } else {
325                                 if (value > priv->end) {
326                                         error_code = MODEST_NUMBER_EDITOR_ERROR_MAXIMUM_VALUE_EXCEED;
327                                         fixup_value = priv->end;
328                                         is_valid = FALSE;
329                                 } else if (value < priv->start) {
330                                         error_code = MODEST_NUMBER_EDITOR_ERROR_MINIMUM_VALUE_EXCEED;
331                                         fixup_value = priv->start;
332                                         is_valid = FALSE;
333                                 }
334                         }
335                         /* The only valid case when conversion can fail is when we
336                            have plain '-', intermediate forms are allowed AND
337                            minimum bound is negative */
338                 } else {
339                         is_valid = FALSE;
340                         if (! allow_intermediate || strcmp (text, "-") != 0 || priv->start >= 0)
341                                 error_code = MODEST_NUMBER_EDITOR_ERROR_ERRONEOUS_VALUE;
342                 }
343         } else {
344                 is_valid = FALSE;
345                 if (! allow_intermediate)
346                         error_code = MODEST_NUMBER_EDITOR_ERROR_ERRONEOUS_VALUE;
347         }
348
349         if (error_code != -1) {
350                 /* If entry is empty and intermediate forms are nor allowed, 
351                    emit error signal */
352                 /* Change to default value */
353                 modest_number_editor_set_value (editor, fixup_value);
354                 g_signal_emit (editor, ModestNumberEditor_signal[RANGE_ERROR], 0, error_code, &r);
355                 add_select_all_idle (editor);
356         }
357
358         if (priv->is_valid != is_valid) {
359                 priv->is_valid = is_valid;
360                 g_signal_emit (editor, ModestNumberEditor_signal[VALID_CHANGED], 0, is_valid);
361         }
362 }
363
364 static void 
365 modest_number_editor_entry_changed              (GtkWidget *widget, 
366                                                  gpointer data)
367 {
368     g_assert (MODEST_IS_NUMBER_EDITOR (data));
369     modest_number_editor_validate_value (MODEST_NUMBER_EDITOR (data), TRUE);
370     g_object_notify (G_OBJECT (data), "value");
371 }
372
373 static gboolean
374 modest_number_editor_entry_focusout             (GtkWidget *widget, 
375                                                  GdkEventFocus *event,
376                                                  gpointer data)
377 {
378         GtkWidget *window;
379         
380         g_assert (MODEST_IS_NUMBER_EDITOR(data));
381
382         window = gtk_widget_get_toplevel (widget);
383         if (window && gtk_window_has_toplevel_focus (GTK_WINDOW (window)))
384                 modest_number_editor_validate_value (MODEST_NUMBER_EDITOR(data), FALSE);
385
386         return FALSE;
387 }
388
389 static gboolean
390 modest_number_editor_range_error                (ModestNumberEditor *editor,
391                                                  ModestNumberEditorErrorType type)
392 {
393
394     gint min, max;
395     gchar *err_msg = NULL;
396     ModestNumberEditorPrivate *priv;
397
398     priv = MODEST_NUMBER_EDITOR_GET_PRIVATE (editor);
399     g_assert (priv);
400
401     min = priv->start;
402     max = priv->end;
403
404     /* Construct error message */
405     switch (type)
406     {
407         case MODEST_NUMBER_EDITOR_ERROR_MAXIMUM_VALUE_EXCEED:
408             err_msg = g_strdup_printf (_HL("ckct_ib_maximum_value"), max, max);
409             break;
410
411         case MODEST_NUMBER_EDITOR_ERROR_MINIMUM_VALUE_EXCEED:
412             err_msg = g_strdup_printf (_HL("ckct_ib_minimum_value"), min, min);
413             break;
414
415         case MODEST_NUMBER_EDITOR_ERROR_ERRONEOUS_VALUE:
416             err_msg =
417                 g_strdup_printf (_HL("ckct_ib_set_a_value_within_range"), min, max);
418             break;
419     }
420
421     /* Infoprint error */
422     if (err_msg)
423     {
424         hildon_banner_show_information (GTK_WIDGET (GTK_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET(editor),
425                                         GTK_TYPE_WINDOW))), NULL, err_msg);
426         g_free(err_msg);
427     }
428
429     return TRUE;
430 }
431
432 /**
433  * modest_number_editor_new:
434  * @min: minimum accepted value
435  * @max: maximum accepted value
436  * 
437  * Creates new number editor
438  *
439  * Returns: a new #ModestNumberEditor widget
440  */
441 GtkWidget*
442 modest_number_editor_new                        (gint min, 
443                                                  gint max)
444 {
445     ModestNumberEditor *editor = g_object_new (MODEST_TYPE_NUMBER_EDITOR, NULL);
446
447     /* Set user inputted range to editor */
448     modest_number_editor_set_range (editor, min, max);
449
450     return GTK_WIDGET (editor);
451 }
452
453 /**
454  * modest_number_editor_set_range:
455  * @editor: a #ModestNumberEditor widget
456  * @min: minimum accepted value
457  * @max: maximum accepted value
458  *
459  * Sets accepted number range for editor
460  */
461 void
462 modest_number_editor_set_range                  (ModestNumberEditor *editor, 
463                                                  gint min, 
464                                                  gint max)
465 {
466     ModestNumberEditorPrivate *priv;
467     gchar buffer_min[32], buffer_max[32];
468     gint a, b;
469
470     g_return_if_fail (MODEST_IS_NUMBER_EDITOR (editor));
471
472     priv = MODEST_NUMBER_EDITOR_GET_PRIVATE (editor);
473     g_assert (priv);
474
475     /* Set preferences */
476     priv->start = MIN (min, max);
477     priv->end = MAX (min, max);
478
479     /* Find maximum allowed length of value */
480     g_snprintf (buffer_min, sizeof (buffer_min), "%d", min);
481     g_snprintf (buffer_max, sizeof (buffer_max), "%d", max);
482     a = strlen (buffer_min);
483     b = strlen (buffer_max);
484
485     /* Set maximum size of entry */
486     gtk_entry_set_width_chars (GTK_ENTRY (editor), MAX (a, b));
487     modest_number_editor_set_value (editor, priv->start);
488 }
489
490 /**
491  * modest_number_editor_is_valid:
492  * @editor: pointer to #ModestNumberEditor
493  *
494  * Returns: if @editor contents are valid
495  */
496 gboolean
497 modest_number_editor_is_valid                  (ModestNumberEditor *editor)
498 {
499     ModestNumberEditorPrivate *priv;
500
501     g_return_val_if_fail (MODEST_IS_NUMBER_EDITOR (editor), FALSE);
502
503     priv = MODEST_NUMBER_EDITOR_GET_PRIVATE (editor);
504     g_assert (priv);
505
506     return priv->is_valid;
507
508 }
509
510 /**
511  * modest_number_editor_get_value:
512  * @editor: pointer to #ModestNumberEditor
513  *
514  * Returns: current NumberEditor value
515  */
516 gint
517 modest_number_editor_get_value                  (ModestNumberEditor *editor)
518 {
519     ModestNumberEditorPrivate *priv;
520
521     g_return_val_if_fail (MODEST_IS_NUMBER_EDITOR (editor), 0);
522
523     priv = MODEST_NUMBER_EDITOR_GET_PRIVATE (editor);
524     g_assert (priv);
525
526     return atoi (gtk_entry_get_text (GTK_ENTRY (editor)));
527 }
528
529 /**
530  * modest_number_editor_set_value:
531  * @editor: pointer to #ModestNumberEditor
532  * @value: numeric value for number editor
533  *
534  * Sets numeric value for number editor
535  */
536 void
537 modest_number_editor_set_value                  (ModestNumberEditor *editor, 
538                                                  gint value)
539 {
540     ModestNumberEditorPrivate *priv;
541
542     g_return_if_fail (MODEST_IS_NUMBER_EDITOR (editor));
543
544     priv = MODEST_NUMBER_EDITOR_GET_PRIVATE (editor);
545     g_assert (priv);
546
547     g_return_if_fail (value <= priv->end);
548     g_return_if_fail (value >= priv->start);
549
550     priv->default_val = value;
551     modest_number_editor_real_set_value (editor, value);
552     g_object_notify (G_OBJECT(editor), "value");
553 }
554
555 /* When calling gtk_entry_set_text, the entry widget does things that can
556  * cause the whole widget to redraw. This redrawing is delayed and if any
557  * selections are made right after calling the gtk_entry_set_text the
558  * setting of the selection might seem to have no effect.
559  *
560  * If the selection is delayed with a lower priority than the redrawing,
561  * the selection should stick. Calling this function with g_idle_add should
562  * do it.
563  */
564 static gboolean
565 modest_number_editor_select_all                 (ModestNumberEditor *editor)
566 {   
567     ModestNumberEditorPrivate *priv;
568
569     g_return_val_if_fail (MODEST_IS_NUMBER_EDITOR (editor), FALSE);
570
571     priv = MODEST_NUMBER_EDITOR_GET_PRIVATE (editor);
572     g_assert (priv);
573
574     GDK_THREADS_ENTER ();
575     gtk_editable_select_region (GTK_EDITABLE (editor), 0, -1);
576     priv->select_all_idle_id = 0;
577     GDK_THREADS_LEAVE ();
578     return FALSE;
579
580
581 static void
582 modest_number_editor_set_property               (GObject *object,
583                                                  guint prop_id,
584                                                  const GValue *value, 
585                                                  GParamSpec *pspec)
586 {
587     ModestNumberEditor *editor;
588
589     editor = MODEST_NUMBER_EDITOR (object);
590
591     switch (prop_id) {
592
593         case PROP_VALUE:
594             modest_number_editor_set_value (editor, g_value_get_int (value));
595             break;
596
597         default:
598             G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
599             break;
600     }
601 }
602
603 static void
604 modest_number_editor_get_property               (GObject *object,
605                                                  guint prop_id, 
606                                                  GValue *value, 
607                                                  GParamSpec *pspec)
608 {
609     ModestNumberEditor *editor;
610
611     editor = MODEST_NUMBER_EDITOR (object);
612
613     switch (prop_id) {
614
615         case PROP_VALUE:
616             g_value_set_int(value, modest_number_editor_get_value (editor));
617             break;
618
619         default:
620             G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
621             break;
622     }
623 }
624
625
626 /* enumerations from "modest-number-editor.h" */
627 GType
628 modest_number_editor_error_type_get_type (void)
629 {
630   static GType etype = 0;
631   if (etype == 0) {
632     static const GEnumValue values[] = {
633       { MODEST_NUMBER_EDITOR_ERROR_MAXIMUM_VALUE_EXCEED, "MODEST_NUMBER_EDITOR_ERROR_MAXIMUM_VALUE_EXCEED", "maximum-value-exceed" },
634       { MODEST_NUMBER_EDITOR_ERROR_MINIMUM_VALUE_EXCEED, "MODEST_NUMBER_EDITOR_ERROR_MINIMUM_VALUE_EXCEED", "minimum-value-exceed" },
635       { MODEST_NUMBER_EDITOR_ERROR_ERRONEOUS_VALUE, "MODEST_NUMBER_EDITOR_ERROR_ERRONEOUS_VALUE", "erroneous-value" },
636       { 0, NULL, NULL }
637     };
638     etype = g_enum_register_static ("ModestNumberEditorErrorType", values);
639   }
640   return etype;
641 }
642