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