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