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