Migrate some entries to HildonEntry
[modest] / src / widgets / modest-validating-entry.c
1 /* Copyright (c) 2007, Nokia Corporation
2  * All rights reserved.
3  *
4  */
5
6 #include "modest-validating-entry.h"
7 #include <gtk/gtksignal.h> /* For the gtk_signal_stop_emit_by_name() convenience function. */
8 #include <string.h> /* For strlen(). */
9
10 /* Include config.h so that _() works: */
11 #ifdef HAVE_CONFIG_H
12 #include <config.h>
13 #endif
14
15 #ifdef MODEST_TOOLKIT_HILDON2
16 G_DEFINE_TYPE (ModestValidatingEntry, modest_validating_entry, HILDON_TYPE_ENTRY);
17 #else
18 G_DEFINE_TYPE (ModestValidatingEntry, modest_validating_entry, GTK_TYPE_ENTRY);
19 #endif
20
21 #define VALIDATING_ENTRY_GET_PRIVATE(o) \
22         (G_TYPE_INSTANCE_GET_PRIVATE ((o), MODEST_TYPE_VALIDATING_ENTRY, ModestValidatingEntryPrivate))
23
24 typedef struct _ModestValidatingEntryPrivate ModestValidatingEntryPrivate;
25
26 struct _ModestValidatingEntryPrivate
27 {
28         /* A list of gunichar, rather than char*,
29          * because gunichar is easier to deal with internally,
30          * but gchar* is easier to supply from the external interface.
31          */
32         GList *list_prevent;
33         
34         gboolean prevent_whitespace;
35         
36         EasySetupValidatingEntryFunc func;
37         gpointer func_user_data;
38
39         EasySetupValidatingEntryMaxFunc max_func;
40         gpointer max_func_user_data;
41 };
42
43 static void
44 modest_validating_entry_get_property (GObject *object, guint property_id,
45                                                                                                                         GValue *value, GParamSpec *pspec)
46 {
47         switch (property_id) {
48         default:
49                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
50         }
51 }
52
53 static void
54 modest_validating_entry_set_property (GObject *object, guint property_id,
55                                                                                                                         const GValue *value, GParamSpec *pspec)
56 {
57         switch (property_id) {
58         default:
59                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
60         }
61 }
62
63 static void
64 modest_validating_entry_dispose (GObject *object)
65 {
66         if (G_OBJECT_CLASS (modest_validating_entry_parent_class)->dispose)
67                 G_OBJECT_CLASS (modest_validating_entry_parent_class)->dispose (object);
68 }
69
70 static void
71 modest_validating_entry_finalize (GObject *object)
72 {
73         ModestValidatingEntryPrivate *priv = VALIDATING_ENTRY_GET_PRIVATE (object);
74         
75         /* Free the list and its items: */
76         if (priv->list_prevent) {
77                 g_list_foreach (priv->list_prevent, (GFunc)&g_free, NULL);
78                 g_list_free (priv->list_prevent);
79         }
80         
81         G_OBJECT_CLASS (modest_validating_entry_parent_class)->finalize (object);
82 }
83
84 static void
85 modest_validating_entry_class_init (ModestValidatingEntryClass *klass)
86 {
87         GObjectClass *object_class = G_OBJECT_CLASS (klass);
88
89         g_type_class_add_private (klass, sizeof (ModestValidatingEntryPrivate));
90
91         object_class->get_property = modest_validating_entry_get_property;
92         object_class->set_property = modest_validating_entry_set_property;
93         object_class->dispose = modest_validating_entry_dispose;
94         object_class->finalize = modest_validating_entry_finalize;
95 }
96
97 static gint
98 on_list_compare(gconstpointer a, gconstpointer b)
99 {
100         gunichar* unichar_a = (gunichar*)(a);
101         gunichar* unichar_b = (gunichar*)(b);
102         if(*unichar_a == *unichar_b)
103                 return 0;
104         else
105                 return -1; /* Really, we should return > and <, but we don't use this for sorting. */
106 }
107                                              
108 static void 
109 on_insert_text(GtkEditable *editable,
110         gchar *new_text, gint new_text_length, 
111         gint *position,
112     gpointer user_data)
113 {
114         ModestValidatingEntry *self = MODEST_VALIDATING_ENTRY (user_data);
115         ModestValidatingEntryPrivate *priv = VALIDATING_ENTRY_GET_PRIVATE (self);
116         
117         if(!new_text_length)
118                 return;
119                 
120         /* Note: new_text_length is documented as the number of bytes, not characters. */
121         if(!g_utf8_validate (new_text, new_text_length, NULL))
122                 return;
123         
124         /* Look at each UTF-8 character in the text (it could be several via a drop or a paste),
125          * and check them */
126         gboolean allow = TRUE;
127         gchar *iter = new_text; /* new_text seems to be NULL-terminated, though that is not documented. */
128         while (iter)
129         {
130                 if(priv->list_prevent) {
131                         /* If the character is in our prevent list, 
132                          * then do not allow this text to be entered.
133                          * 
134                          * This prevents entry of all text, without removing the unwanted characters.
135                          * It is debatable whether that is the best thing to do.
136                          */
137                         gunichar one_char = g_utf8_get_char (iter);
138                         GList *found = g_list_find_custom(priv->list_prevent, &one_char, &on_list_compare);
139                         if(found) {
140                                 allow = FALSE;
141                                 if (priv->func)
142                                 {
143                                         priv->func(self, iter, priv->func_user_data);
144                                 }
145                                 break;
146                         }       
147                 }
148                 
149                 if(priv->prevent_whitespace) {
150                         /* Check for whitespace characters: */
151                         gunichar one_char = g_utf8_get_char (iter);
152                         if (g_unichar_isspace (one_char)) {
153                                 allow = FALSE;
154                                 if (priv->func)
155                                 {
156                                         priv->func(self, NULL, priv->func_user_data);
157                                 }
158                                 break;
159                         }
160                 }
161
162                 /* Crashes. Don't know why: iter = g_utf8_next_char (iter); 
163                  * Maybe it doesn't check for null-termination. */      
164                 iter = g_utf8_find_next_char (iter, new_text + new_text_length);
165         }
166         
167         /* Prevent more than the max characters.
168          * The regular GtkEntry does this already, but we also want to call a specified callback,
169          * so that the application can show a warning dialog. */
170         if(priv->max_func) {
171                 const gint max_num = gtk_entry_get_max_length (GTK_ENTRY (self));
172                 if (max_num > 0) {
173                         const gchar *existing_text = gtk_entry_get_text (GTK_ENTRY(self));
174                         const gint existing_length = existing_text ? g_utf8_strlen (existing_text, -1) : 0;
175                         const gint new_length_chars = g_utf8_strlen (new_text, new_text_length);
176                         
177                         if ((existing_length + new_length_chars) > max_num) {
178                                 priv->max_func (self, priv->max_func_user_data);
179                                 /* We shouldn't need to stop the signal because the underlying code will check too.
180                                 * Well, that would maybe be a performance optimization, 
181                                  * but it's generally safer not to interfere too much. */       
182                         }
183                 }
184         }
185         
186         if(!allow) {
187                 /* The signal documentation says 
188                  * "by connecting to this signal and then stopping the signal with 
189                  * gtk_signal_emit_stop(), it is possible to modify the inserted text, 
190                  * or prevent it from being inserted entirely."
191                  */
192                  gtk_signal_emit_stop_by_name (GTK_OBJECT (self), "insert-text");
193         }
194
195
196                                             
197 static void
198 modest_validating_entry_init (ModestValidatingEntry *self)
199 {
200         /* Connect to the GtkEditable::insert-text signal 
201          * so we can filter out some characters:
202          * We connect _before_ so we can stop the default signal handler from running.
203          */
204         g_signal_connect (G_OBJECT (self), "insert-text", (GCallback)&on_insert_text, self);
205 }
206
207 ModestValidatingEntry*
208 modest_validating_entry_new (void)
209 {
210         return g_object_new (MODEST_TYPE_VALIDATING_ENTRY, NULL);
211 }
212
213 /** Specify characters that may not be entered into this GtkEntry.
214  *  
215  * list: A list of gchar* strings. Each one identifies a UTF-8 character.
216  */
217 void modest_validating_entry_set_unallowed_characters (ModestValidatingEntry *self, GList *list)
218 {
219         ModestValidatingEntryPrivate *priv = VALIDATING_ENTRY_GET_PRIVATE (self);
220             
221         /* Free the list and its items: */      
222         if (priv->list_prevent) {
223                 g_list_foreach (priv->list_prevent, (GFunc)&g_free, NULL);
224                 g_list_free (priv->list_prevent);
225         }
226      
227     /* Do a deep copy of the list, converting gchar* to gunichar: */
228     priv->list_prevent = NULL;
229     GList *iter = NULL;               
230     for (iter = list; iter != NULL; iter = iter->next) {
231         gunichar *one_char = g_new0 (gunichar, 1);
232         if(iter->data)
233                 *one_char = g_utf8_get_char ((gchar*)iter->data);
234         else
235                 *one_char = 0;
236                 
237         priv->list_prevent = g_list_append (priv->list_prevent, one_char);      
238     }
239 }
240
241 /** Specify that no whitespace characters may be entered into this GtkEntry.
242  *  
243  */
244 void modest_validating_entry_set_unallowed_characters_whitespace (ModestValidatingEntry *self)
245 {
246         ModestValidatingEntryPrivate *priv = VALIDATING_ENTRY_GET_PRIVATE (self);
247         priv->prevent_whitespace = TRUE;
248 }
249
250 /** Set a callback to be called when the maximum number of characters have been entered.
251  * This may be used to show an informative dialog.
252  */
253 void modest_validating_entry_set_max_func (ModestValidatingEntry *self, EasySetupValidatingEntryMaxFunc func, gpointer user_data)
254 {
255         ModestValidatingEntryPrivate *priv = VALIDATING_ENTRY_GET_PRIVATE (self);
256         priv->max_func = func;
257         priv->max_func_user_data = user_data;
258 }
259
260 /** Set a callback to be called when a character was prevented so that a
261  * note can be shown by the application to inform the user. For whitespaces,
262  * character will be NULL
263  */
264 void modest_validating_entry_set_func (ModestValidatingEntry *self, EasySetupValidatingEntryFunc func, gpointer user_data)
265 {
266         ModestValidatingEntryPrivate *priv = VALIDATING_ENTRY_GET_PRIVATE (self);
267         priv->func = func;
268         priv->func_user_data = user_data;
269 }
270