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