1 /*
2  * Copyright (C) 2009 - 2012 Vivien Malerba <malerba@gnome-db.org>
3  * Copyright (C) 2010 David King <davidk@openismus.com>
4  * Copyright (C) 2011 Murray Cumming <murrayc@murrayc.com>
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, write to the
18  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
19  * Boston, MA  02110-1301, USA.
20  */
21 
22 #include <glib/gi18n-lib.h>
23 #include <gdk/gdkkeysyms.h>
24 #include <gdk/gdk.h>
25 #include <string.h>
26 
27 #include "gdaui-formatted-entry.h"
28 
29 struct _GdauiFormattedEntryPrivate {
30 	gchar   *format; /* UTF-8! */
31 	gint     format_clen; /* in characters, not gchar */
32 	gchar   *mask; /* ASCII! */
33 	gint     mask_len; /* in gchar */
34 
35 	GdauiFormattedEntryInsertFunc insert_func;
36 	gpointer                      insert_func_data;
37 };
38 
39 static void gdaui_formatted_entry_class_init   (GdauiFormattedEntryClass *klass);
40 static void gdaui_formatted_entry_init         (GdauiFormattedEntry *entry);
41 static void gdaui_formatted_entry_finalize     (GObject *object);
42 static void gdaui_formatted_entry_set_property (GObject *object,
43 						guint param_id,
44 						const GValue *value,
45 						GParamSpec *pspec);
46 static void gdaui_formatted_entry_get_property (GObject *object,
47 						guint param_id,
48 						GValue *value,
49 						GParamSpec *pspec);
50 static gchar *gdaui_formatted_entry_get_empty_text (GdauiEntry *entry);
51 static void gdaui_formatted_entry_assume_insert (GdauiEntry *entry, const gchar *text, gint text_length, gint *virt_pos, gint offset);
52 static void gdaui_formatted_entry_assume_delete (GdauiEntry *entry, gint virt_start_pos, gint virt_end_pos, gint offset);
53 
54 /* properties */
55 enum
56 {
57         PROP_0,
58 	PROP_FORMAT,
59 	PROP_MASK
60 };
61 
62 static GObjectClass *parent_class = NULL;
63 
64 GType
gdaui_formatted_entry_get_type(void)65 gdaui_formatted_entry_get_type (void)
66 {
67 	static GType type = 0;
68 
69 	if (G_UNLIKELY (type == 0)) {
70 		static const GTypeInfo type_info = {
71 			sizeof (GdauiFormattedEntryClass),
72 			NULL,		/* base_init */
73 			NULL,		/* base_finalize */
74 			(GClassInitFunc) gdaui_formatted_entry_class_init,
75 			NULL,		/* class_finalize */
76 			NULL,		/* class_data */
77 			sizeof (GdauiFormattedEntry),
78 			0,		/* n_preallocs */
79 			(GInstanceInitFunc) gdaui_formatted_entry_init,
80 			0
81 		};
82 
83 		type = g_type_register_static (GDAUI_TYPE_ENTRY, "GdauiFormattedEntry", &type_info, 0);
84 	}
85 
86 	return type;
87 }
88 
89 static void
gdaui_formatted_entry_class_init(GdauiFormattedEntryClass * klass)90 gdaui_formatted_entry_class_init (GdauiFormattedEntryClass *klass)
91 {
92 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
93 
94 	parent_class = g_type_class_peek_parent (klass);
95 
96 	object_class->finalize = gdaui_formatted_entry_finalize;
97 	GDAUI_ENTRY_CLASS (klass)->assume_insert = gdaui_formatted_entry_assume_insert;
98 	GDAUI_ENTRY_CLASS (klass)->assume_delete = gdaui_formatted_entry_assume_delete;
99 	GDAUI_ENTRY_CLASS (klass)->get_empty_text = gdaui_formatted_entry_get_empty_text;
100 
101 	/* Properties */
102         object_class->set_property = gdaui_formatted_entry_set_property;
103         object_class->get_property = gdaui_formatted_entry_get_property;
104 
105         g_object_class_install_property (object_class, PROP_FORMAT,
106                                          g_param_spec_string ("format", NULL, NULL, NULL,
107 							      G_PARAM_READABLE | G_PARAM_WRITABLE));
108         g_object_class_install_property (object_class, PROP_MASK,
109                                          g_param_spec_string ("mask", NULL, NULL, NULL,
110 							      G_PARAM_READABLE | G_PARAM_WRITABLE));
111 }
112 
113 static void
gdaui_formatted_entry_init(GdauiFormattedEntry * entry)114 gdaui_formatted_entry_init (GdauiFormattedEntry *entry)
115 {
116 	entry->priv = g_new0 (GdauiFormattedEntryPrivate, 1);
117 	entry->priv->format = NULL;
118 	entry->priv->mask = NULL;
119 	entry->priv->insert_func = NULL;
120 	entry->priv->insert_func_data = NULL;
121 }
122 
123 static void
gdaui_formatted_entry_finalize(GObject * object)124 gdaui_formatted_entry_finalize (GObject *object)
125 {
126 	GdauiFormattedEntry *entry;
127 
128         g_return_if_fail (object != NULL);
129         g_return_if_fail (GDAUI_IS_ENTRY (object));
130 
131         entry = GDAUI_FORMATTED_ENTRY (object);
132         if (entry->priv) {
133 		g_free (entry->priv->format);
134 		g_free (entry->priv->mask);
135                 g_free (entry->priv);
136                 entry->priv = NULL;
137         }
138 
139         /* parent class */
140         parent_class->finalize (object);
141 }
142 
143 static void
gdaui_formatted_entry_set_property(GObject * object,guint param_id,const GValue * value,GParamSpec * pspec)144 gdaui_formatted_entry_set_property (GObject *object,
145 				    guint param_id,
146 				    const GValue *value,
147 				    GParamSpec *pspec)
148 {
149 	GdauiFormattedEntry *entry;
150 	const gchar *str;
151 	gchar *otext;
152 
153         entry = GDAUI_FORMATTED_ENTRY (object);
154 	otext = gdaui_entry_get_text (GDAUI_ENTRY (entry));
155         if (entry->priv) {
156                 switch (param_id) {
157                 case PROP_FORMAT:
158 			g_free (entry->priv->format);
159 			entry->priv->format = NULL;
160 			entry->priv->format_clen = 0;
161 
162 			str = g_value_get_string (value);
163 			if (str) {
164 				if (! g_utf8_validate (str, -1, NULL))
165 					g_warning (_("Invalid UTF-8 format!"));
166 				else {
167 					entry->priv->format = g_strdup (str);
168 					entry->priv->format_clen = g_utf8_strlen (str, -1);
169 					gdaui_entry_set_width_chars (GDAUI_ENTRY (entry),
170 								     entry->priv->format_clen);
171 				}
172 			}
173                         break;
174                 case PROP_MASK:
175 			g_free (entry->priv->mask);
176 			entry->priv->mask = NULL;
177 			entry->priv->mask_len = 0;
178 
179 			str = g_value_get_string (value);
180 			if (str) {
181 				entry->priv->mask = g_strdup (str);
182 				entry->priv->mask_len = strlen (str);
183 			}
184                         break;
185                 default:
186                         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
187                         break;
188                 }
189         }
190 	gdaui_entry_set_text (GDAUI_ENTRY (entry), otext);
191 	g_free (otext);
192 }
193 
194 static void
gdaui_formatted_entry_get_property(GObject * object,guint param_id,GValue * value,GParamSpec * pspec)195 gdaui_formatted_entry_get_property (GObject *object,
196 				    guint param_id,
197 				    GValue *value,
198 				    GParamSpec *pspec)
199 {
200 	GdauiFormattedEntry *entry;
201 
202         entry = GDAUI_FORMATTED_ENTRY (object);
203         if (entry->priv) {
204                 switch (param_id) {
205                 case PROP_FORMAT:
206 			g_value_set_string (value, entry->priv->format);
207                         break;
208                 case PROP_MASK:
209 			g_value_set_string (value, entry->priv->mask);
210                         break;
211                 default:
212                         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
213                         break;
214                 }
215         }
216 }
217 
218 /*
219  * is_writable
220  * @fentry:
221  * @pos: the position (in characters) in @fentry->priv->format
222  * @ptr: the character (in @fentry->priv->format)
223  *
224  * Returns: %TRUE if it is a writable loaction
225  */
226 static gboolean
is_writable(GdauiFormattedEntry * fentry,gint pos,const gchar * ptr)227 is_writable (GdauiFormattedEntry *fentry, gint pos, const gchar *ptr)
228 {
229 	if (((*ptr == '0') ||
230 	     (*ptr == '9') ||
231 	     (*ptr == '@') ||
232 	     (*ptr == '^') ||
233 	     (*ptr == '#') ||
234 	     (*ptr == '*')) &&
235 	    (!fentry->priv->mask ||
236 	     (fentry->priv->mask &&
237 	      (pos < fentry->priv->mask_len) &&
238 	      (fentry->priv->mask [pos] != ' '))))
239 		return TRUE;
240 	else
241 		return FALSE;
242 }
243 
244 /*
245  * is_allowed
246  * @fentry:
247  * @ptr: the character (in @fentry->priv->format)
248  * @wc: the character to be inserted
249  *
250  * Returns: %TRUE if @wc can be used to replace @ptr
251  */
252 static gboolean
is_allowed(G_GNUC_UNUSED GdauiFormattedEntry * fentry,const gchar * ptr,const gunichar wc,gunichar * out_wc)253 is_allowed (G_GNUC_UNUSED GdauiFormattedEntry *fentry, const gchar *ptr, const gunichar wc, gunichar *out_wc)
254 {
255 /* TODO: Use this?
256 	gunichar fwc;
257 
258 	fwc = g_utf8_get_char (ptr);
259 */
260 	*out_wc = wc;
261 	if (*ptr == '0')
262 		return g_unichar_isdigit (wc);
263 	else if (*ptr == '9')
264 		return g_unichar_isdigit (wc) && (wc != g_utf8_get_char ("0"));
265 	else if (*ptr == '@')
266 		return g_unichar_isalpha (wc);
267 	else if (*ptr == '^') {
268 		gboolean isa = g_unichar_isalpha (wc);
269 		if (isa)
270 			*out_wc = g_unichar_toupper (wc);
271 		return isa;
272 	}
273 	else if (*ptr == '#')
274 		return g_unichar_isalnum (wc);
275 	else if (*ptr == '*')
276 		return g_unichar_isprint (wc);
277 	else {
278 		g_warning (_("Unknown format character starting at %s"), ptr);
279 		return FALSE;
280 	}
281 }
282 
283 static gchar *
gdaui_formatted_entry_get_empty_text(GdauiEntry * entry)284 gdaui_formatted_entry_get_empty_text (GdauiEntry *entry)
285 {
286 	GdauiFormattedEntry *fentry;
287 
288 	fentry = (GdauiFormattedEntry*) entry;
289 	if (fentry->priv->format) {
290 		GString *string;
291 
292 		string = g_string_new ("");
293 		gchar *ptr;
294 		gint i;
295 		for (ptr = fentry->priv->format, i = 0;
296 		     ptr && *ptr;
297 		     ptr = g_utf8_next_char (ptr), i++) {
298 			if (is_writable (fentry, i, ptr))
299 				g_string_append_c (string, '_');
300 			else {
301 				gunichar wc;
302 				wc = g_utf8_get_char (ptr);
303 				g_string_append_unichar (string, wc);
304 			}
305 		}
306 		return g_string_free (string, FALSE);
307 	}
308 	else
309 		return NULL;
310 }
311 
312 static void
gdaui_formatted_entry_assume_insert(GdauiEntry * entry,const gchar * text,G_GNUC_UNUSED gint text_length,gint * virt_pos,gint offset)313 gdaui_formatted_entry_assume_insert (GdauiEntry *entry, const gchar *text, G_GNUC_UNUSED gint text_length,
314 				     gint *virt_pos, gint offset)
315 {
316 	GdauiFormattedEntry *fentry;
317 	gint i, pos;
318 
319 	fentry = (GdauiFormattedEntry*) entry;
320 	if (!fentry->priv->format)
321 		return;
322 
323 	const gchar *ptr, *fptr;
324 	pos = *virt_pos;
325 	for (fptr = fentry->priv->format, i = 0;
326 	     (i < pos) && fptr && *fptr;
327 	     fptr = g_utf8_next_char (fptr), i++);
328 
329 	if (i != pos)
330 		return;
331 
332 	_gdaui_entry_block_changes (entry);
333 	gboolean inserted = FALSE;
334 	gunichar wc;
335 	for (ptr = text, i = 0; ptr && *ptr && *fptr; ptr = g_utf8_next_char (ptr)) {
336 		while ((pos < fentry->priv->format_clen) &&
337 		       !is_writable (fentry, pos, fptr)) {
338 			fptr = g_utf8_next_char (fptr);
339 			if (!fptr || !*fptr) {
340 				_gdaui_entry_unblock_changes (entry);
341 				return;
342 			}
343 			pos++;
344 		}
345 
346 		wc = g_utf8_get_char (ptr);
347 		if ((pos < fentry->priv->format_clen) &&
348 		    is_allowed (fentry, fptr, wc, &wc)){
349 			/* Ok, insert *ptr (<=> text[i] if it was ASCII) */
350 			gint rpos = pos + offset;
351 			gint usize;
352 			gchar buf [6];
353 
354 			usize = g_unichar_to_utf8 (wc, buf);
355 			gtk_editable_delete_text ((GtkEditable*) entry, rpos, rpos + 1);
356 			gtk_editable_insert_text ((GtkEditable*) entry, buf, usize, &rpos);
357 			inserted = TRUE;
358 			pos++;
359 			fptr = g_utf8_next_char (fptr);
360 		}
361 	}
362 	_gdaui_entry_unblock_changes (entry);
363 	*virt_pos = pos;
364 
365 	if (!inserted && fentry->priv->insert_func) {
366 		ptr = g_utf8_next_char (text);
367 		if (!*ptr) {
368 			wc = g_utf8_get_char (text);
369 			fentry->priv->insert_func (fentry, wc, *virt_pos, fentry->priv->insert_func_data);
370 		}
371 	}
372 }
373 
374 static void
gdaui_formatted_entry_assume_delete(GdauiEntry * entry,gint virt_start_pos,gint virt_end_pos,gint offset)375 gdaui_formatted_entry_assume_delete (GdauiEntry *entry, gint virt_start_pos, gint virt_end_pos, gint offset)
376 {
377 	GdauiFormattedEntry *fentry;
378 	gchar *fptr;
379 	gint i;
380 
381 	fentry = (GdauiFormattedEntry*) entry;
382 	if (!fentry->priv->format)
383 		return;
384 
385 #ifdef GDA_DEBUG
386 	gint clen;
387 	gchar *otext;
388 	otext = gdaui_entry_get_text (entry);
389 	if (otext) {
390 		clen = g_utf8_strlen (otext, -1);
391 		g_assert (clen == fentry->priv->format_clen);
392 		g_free (otext);
393 	}
394 #endif
395 
396 	g_assert (virt_end_pos <= fentry->priv->format_clen);
397 
398 	/* move fptr to the @virt_start_pos in fentry->priv->format */
399 	for (fptr = fentry->priv->format, i = 0;
400 	     (i < virt_start_pos) && *fptr;
401 	     fptr = g_utf8_next_char (fptr), i++);
402 	if (i != virt_start_pos)
403 		return;
404 
405 	_gdaui_entry_block_changes (entry);
406 	for (;
407 	     (i < virt_end_pos) && fptr && *fptr;
408 	     fptr = g_utf8_next_char (fptr), i++) {
409 		if (!is_writable (fentry, i, fptr)) {
410 			if (virt_end_pos - virt_start_pos == 1) {
411 				gint npos;
412 				npos = gtk_editable_get_position ((GtkEditable*) entry);
413 				while ((i >= 0) && !is_writable (fentry, i, fptr)) {
414 					virt_start_pos --;
415 					virt_end_pos --;
416 					i--;
417 					fptr = g_utf8_find_prev_char (fentry->priv->format, fptr);
418 					npos --;
419 				}
420 				if (i < 0) {
421 					_gdaui_entry_unblock_changes (entry);
422 					return;
423 				}
424 				else
425 					gtk_editable_set_position ((GtkEditable*) entry, npos);
426 			}
427 			else
428 				continue;
429 		}
430 		gint rpos = i + offset;
431 		gtk_editable_delete_text ((GtkEditable*) entry, rpos, rpos + 1);
432 		gtk_editable_insert_text ((GtkEditable*) entry, "_", 1, &rpos);
433 	}
434 	_gdaui_entry_unblock_changes (entry);
435 }
436 
437 /**
438  * gdaui_formatted_entry_new:
439  * @format: a format string
440  * @mask: (allow-none): a mask string, or %NULL
441  *
442  * Creates a new #GdauiFormattedEntry widget.
443  *
444  * Characters in @format are of two types:
445  *   writeable: writeable characters which will be replaced with and underscore and where text will be entered
446  *   fixed: every other characters are fixed characters, where text cant' be edited, and will be displayed AS IS
447  *
448  * Possible values for writeable characters are:
449  * <itemizedlist>
450  *   <listitem><para>'0': digits</para></listitem>
451  *   <listitem><para>'9': digits excluded 0</para></listitem>
452  *   <listitem><para>'@': alpha</para></listitem>
453  *   <listitem><para>'^': alpha converted to upper case</para></listitem>
454  *   <listitem><para>'#': alphanumeric</para></listitem>
455  *   <listitem><para>'*': any char</para></listitem>
456  * </itemizedlist>
457  *
458  * if @mask is not %NULL, then it should only contains the follogin characters, which are used side by side with
459  * @format's characters:
460  * <itemizedlist>
461  *   <listitem><para>'_': the corresponding character in @format is actually used as a writable character</para></listitem>
462  *   <listitem><para>'-': the corresponding character in @format is actually used as a writable character, but
463  *              the character will be removed from gdaui_formatted_entry_get_text()'s result if it was not
464  *              filled by the user</para></listitem>
465  *   <listitem><para>' ': the corresponding character in @format will not be considered as a writable character
466  *              but as a non writable character</para></listitem>
467  * </itemizedlist>
468  * it is then interpreted in the following way: for a character C in @format, if the character at the same
469  * position in @mask is the space character (' '), then C will not interpreted as a writable format
470  * character as defined above. @mask does not be to have the same length as @format.
471  *
472  * Returns: (transfer full): the newly created #GdauiFormattedEntry widget.
473  */
474 GtkWidget*
gdaui_formatted_entry_new(const gchar * format,const gchar * mask)475 gdaui_formatted_entry_new (const gchar *format, const gchar *mask)
476 {
477 	GObject *obj;
478 
479 	obj = g_object_new (GDAUI_TYPE_FORMATTED_ENTRY, "format", format, "mask", mask, NULL);
480 	return GTK_WIDGET (obj);
481 }
482 
483 /**
484  * gdaui_formatted_entry_get_text:
485  * @entry: a #GdauiFormattedEntry widget
486  *
487  * Get @entry's contents. This function is similar to gdaui_get_text() except
488  * that it optionnally uses the information contained in @mask when gdaui_formatted_entry_new()
489  * was called to format the output differently.
490  *
491  * Returns: (transfer full): a new string, or %NULL
492  */
493 gchar *
gdaui_formatted_entry_get_text(GdauiFormattedEntry * entry)494 gdaui_formatted_entry_get_text (GdauiFormattedEntry *entry)
495 {
496 	gchar *text;
497 	g_return_val_if_fail (GDAUI_IS_FORMATTED_ENTRY (entry), NULL);
498 
499 	text = gdaui_entry_get_text ((GdauiEntry*) entry);
500 	if (text && entry->priv->mask) {
501 		gchar *tptr, *mptr;
502 		gint len;
503 		len = strlen (text);
504 		for (tptr = text, mptr = entry->priv->mask;
505 		     *tptr && *mptr;
506 		     mptr++) {
507 			if ((*mptr == '-') && (*tptr == '_')) {
508 				/* remove that char */
509 				memmove (tptr, tptr+1, len - (tptr - text));
510 			}
511 			else
512 				tptr = g_utf8_next_char (tptr);
513 		}
514 	}
515 
516 	return text;
517 }
518 
519 /**
520  * gdaui_formatted_entry_set_insert_func:
521  * @entry: a #GdauiFormattedEntry widget
522  * @insert_func: (allow-none) (scope notified): a #GdauiFormattedEntryInsertFunc, or %NULL
523  * @data: (allow-none): a pointer which will be passed to @insert_func
524  *
525  * Specifies that @entry should call @insert_func when the user wants to insert a char
526  * which is anot allowed, to perform other actions
527  */
528 void
gdaui_formatted_entry_set_insert_func(GdauiFormattedEntry * entry,GdauiFormattedEntryInsertFunc insert_func,gpointer data)529 gdaui_formatted_entry_set_insert_func (GdauiFormattedEntry *entry, GdauiFormattedEntryInsertFunc insert_func,
530 				       gpointer data)
531 {
532 	g_return_if_fail (GDAUI_IS_FORMATTED_ENTRY (entry));
533 
534 	entry->priv->insert_func = insert_func;
535 	entry->priv->insert_func_data = data;
536 }
537