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