1 /* 2 * Copyright (C) 2009 - 2011 Vivien Malerba <malerba@gnome-db.org> 3 * Copyright (C) 2010 David King <davidk@openismus.com> 4 * 5 * This library is free software; you can redistribute it and/or 6 * modify it under the terms of the GNU Lesser General Public 7 * License as published by the Free Software Foundation; either 8 * version 2 of the License, or (at your option) any later version. 9 * 10 * This library is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 * Lesser General Public License for more details. 14 * 15 * You should have received a copy of the GNU Lesser General Public 16 * License along with this library; if not, write to the 17 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, 18 * Boston, MA 02110-1301, USA. 19 */ 20 21 #include <glib/gi18n-lib.h> 22 #include <gdk/gdkkeysyms.h> 23 #include <gdk/gdk.h> 24 #include <string.h> 25 26 #include "gdaui-entry.h" 27 28 struct _GdauiEntryPrivate { 29 gchar *prefix; 30 gint prefix_len; 31 gint prefix_clen; /* UTF8 len */ 32 gchar *suffix; 33 gint suffix_len; 34 gint suffix_clen; /* UTF8 len */ 35 gint maxlen; /* UTF8 len */ 36 gboolean isnull; 37 guchar internal_changes; 38 }; 39 40 #define ENTER_INTERNAL_CHANGES(entry) (entry)->priv->internal_changes ++ 41 #define LEAVE_INTERNAL_CHANGES(entry) (entry)->priv->internal_changes -- 42 43 static void gdaui_entry_class_init (GdauiEntryClass *klass); 44 static void gdaui_entry_init (GdauiEntry *entry); 45 static void gdaui_entry_finalize (GObject *object); 46 static void gdaui_entry_set_property (GObject *object, 47 guint param_id, 48 const GValue *value, 49 GParamSpec *pspec); 50 static void gdaui_entry_get_property (GObject *object, 51 guint param_id, 52 GValue *value, 53 GParamSpec *pspec); 54 55 void 56 _gdaui_entry_block_changes (GdauiEntry *entry) 57 { 58 ENTER_INTERNAL_CHANGES(entry); 59 } 60 61 void 62 _gdaui_entry_unblock_changes (GdauiEntry *entry) 63 { 64 LEAVE_INTERNAL_CHANGES(entry); 65 } 66 67 68 static gchar *truncate_utf8_string (gchar *text, gint pos); 69 static void adjust_display (GdauiEntry *entry, gchar *existing_text); 70 71 /* properties */ 72 enum 73 { 74 PROP_0, 75 PROP_PREFIX, 76 PROP_SUFFIX, 77 PROP_MAXLEN 78 }; 79 80 static void signal_handlers_block (GdauiEntry *entry); 81 static void signal_handlers_unblock (GdauiEntry *entry); 82 83 static void changed_cb (GtkEditable *editable, gpointer data); 84 static void delete_text_cb (GtkEditable *editable, gint start_pos, gint end_pos, gpointer data); 85 static void insert_text_cb (GtkEditable *editable, const gchar *text, gint length, gint *position, gpointer data); 86 87 88 static GObjectClass *parent_class = NULL; 89 90 GType 91 gdaui_entry_get_type (void) 92 { 93 static GType type = 0; 94 95 if (G_UNLIKELY (type == 0)) { 96 static const GTypeInfo type_info = { 97 sizeof (GdauiEntryClass), 98 NULL, /* base_init */ 99 NULL, /* base_finalize */ 100 (GClassInitFunc) gdaui_entry_class_init, 101 NULL, /* class_finalize */ 102 NULL, /* class_data */ 103 sizeof (GdauiEntry), 104 0, /* n_preallocs */ 105 (GInstanceInitFunc) gdaui_entry_init, 106 0 107 }; 108 109 type = g_type_register_static (GTK_TYPE_ENTRY, "GdauiEntry", &type_info, 0); 110 } 111 112 return type; 113 } 114 115 static void 116 gdaui_entry_class_init (GdauiEntryClass *klass) 117 { 118 GObjectClass *object_class = G_OBJECT_CLASS (klass); 119 120 parent_class = g_type_class_peek_parent (klass); 121 122 object_class->finalize = gdaui_entry_finalize; 123 klass->assume_insert = NULL; 124 klass->assume_delete = NULL; 125 klass->get_empty_text = NULL; 126 127 /* Properties */ 128 object_class->set_property = gdaui_entry_set_property; 129 object_class->get_property = gdaui_entry_get_property; 130 131 g_object_class_install_property (object_class, PROP_PREFIX, 132 g_param_spec_string ("prefix", NULL, NULL, NULL, 133 G_PARAM_READABLE | G_PARAM_WRITABLE)); 134 g_object_class_install_property (object_class, PROP_SUFFIX, 135 g_param_spec_string ("suffix", NULL, NULL, NULL, 136 G_PARAM_READABLE | G_PARAM_WRITABLE)); 137 g_object_class_override_property (object_class, PROP_MAXLEN, "max-length"); 138 } 139 140 static void 141 gdaui_entry_init (GdauiEntry *entry) 142 { 143 entry->priv = g_new0 (GdauiEntryPrivate, 1); 144 entry->priv->prefix = NULL; 145 entry->priv->suffix = NULL; 146 entry->priv->maxlen = 65535; /* eg. unlimited for GtkEntry */ 147 entry->priv->isnull = TRUE; 148 entry->priv->internal_changes = 0; 149 150 g_signal_connect (G_OBJECT (entry), "delete-text", 151 G_CALLBACK (delete_text_cb), NULL); 152 153 g_signal_connect (G_OBJECT (entry), "insert-text", 154 G_CALLBACK (insert_text_cb), NULL); 155 156 g_signal_connect (G_OBJECT (entry), "changed", 157 G_CALLBACK (changed_cb), NULL); 158 } 159 160 static void 161 gdaui_entry_finalize (GObject *object) 162 { 163 GdauiEntry *entry; 164 165 g_return_if_fail (object != NULL); 166 g_return_if_fail (GDAUI_IS_ENTRY (object)); 167 168 entry = GDAUI_ENTRY (object); 169 if (entry->priv) { 170 g_free (entry->priv->prefix); 171 g_free (entry->priv->suffix); 172 g_free (entry->priv); 173 entry->priv = NULL; 174 } 175 176 /* parent class */ 177 parent_class->finalize (object); 178 } 179 180 static void 181 gdaui_entry_set_property (GObject *object, 182 guint param_id, 183 const GValue *value, 184 GParamSpec *pspec) 185 { 186 GdauiEntry *entry; 187 const gchar *str; 188 gchar *otext; 189 190 entry = GDAUI_ENTRY (object); 191 if (entry->priv) { 192 switch (param_id) { 193 case PROP_PREFIX: 194 otext = gdaui_entry_get_text (entry); 195 g_free (entry->priv->prefix); 196 entry->priv->prefix = NULL; 197 entry->priv->prefix_len = 0; 198 199 str = g_value_get_string (value); 200 if (str) { 201 if (! g_utf8_validate (str, -1, NULL)) 202 g_warning (_("Invalid UTF-8 format!")); 203 else { 204 entry->priv->prefix = g_strdup (str); 205 entry->priv->prefix_len = strlen (str); 206 entry->priv->prefix_clen = g_utf8_strlen (str, -1); 207 } 208 } 209 adjust_display (entry, otext); 210 g_free (otext); 211 break; 212 case PROP_SUFFIX: 213 otext = gdaui_entry_get_text (entry); 214 g_free (entry->priv->suffix); 215 entry->priv->suffix = NULL; 216 entry->priv->suffix_len = 0; 217 218 str = g_value_get_string (value); 219 if (str) { 220 if (! g_utf8_validate (str, -1, NULL)) 221 g_warning (_("Invalid UTF-8 format!")); 222 else { 223 entry->priv->suffix = g_strdup (str); 224 entry->priv->suffix_len = strlen (str); 225 entry->priv->suffix_clen = g_utf8_strlen (str, -1); 226 } 227 } 228 adjust_display (entry, otext); 229 g_free (otext); 230 break; 231 case PROP_MAXLEN: 232 entry->priv->maxlen = g_value_get_int (value); 233 otext = gdaui_entry_get_text (entry); 234 adjust_display (entry, otext); 235 g_free (otext); 236 break; 237 default: 238 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); 239 break; 240 } 241 } 242 } 243 244 static void 245 gdaui_entry_get_property (GObject *object, 246 guint param_id, 247 GValue *value, 248 GParamSpec *pspec) 249 { 250 GdauiEntry *entry; 251 252 entry = GDAUI_ENTRY (object); 253 if (entry->priv) { 254 switch (param_id) { 255 case PROP_PREFIX: 256 g_value_set_string (value, entry->priv->prefix); 257 break; 258 case PROP_SUFFIX: 259 g_value_set_string (value, entry->priv->suffix); 260 break; 261 case PROP_MAXLEN: 262 g_value_set_int (value, entry->priv->maxlen); 263 break; 264 default: 265 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); 266 break; 267 } 268 } 269 } 270 271 static void 272 signal_handlers_block (GdauiEntry *entry) 273 { 274 ENTER_INTERNAL_CHANGES (entry); 275 g_signal_handlers_block_by_func (entry, G_CALLBACK (insert_text_cb), NULL); 276 g_signal_handlers_block_by_func (entry, G_CALLBACK (delete_text_cb), NULL); 277 } 278 279 static void 280 signal_handlers_unblock (GdauiEntry *entry) 281 { 282 g_signal_handlers_unblock_by_func (entry, G_CALLBACK (insert_text_cb), NULL); 283 g_signal_handlers_unblock_by_func (entry, G_CALLBACK (delete_text_cb), NULL); 284 LEAVE_INTERNAL_CHANGES (entry); 285 } 286 287 /* 288 * truncate_utf8_string 289 * @text: a string, not %NULL 290 * @pos: the position where the string wil be truncated <=> text[pos]=0 for ASCII 291 * 292 * Returns: @text 293 */ 294 static gchar * 295 truncate_utf8_string (gchar *text, gint pos) 296 { 297 gchar *ptr; 298 gint i; 299 for (ptr = text, i = 0; (i < pos) && ptr && *ptr; ptr = g_utf8_next_char (ptr), i++); 300 if (i == pos) 301 *ptr = 0; 302 return text; 303 } 304 305 /* 306 * Computes new new display 307 * 308 * WARNING: @existing_text may be modified!!! 309 */ 310 static void 311 adjust_display (GdauiEntry *entry, gchar *existing_text) 312 { 313 gchar *tmp; 314 315 if (!entry->priv->isnull) { 316 signal_handlers_block (entry); 317 if (g_utf8_strlen (existing_text, -1) > entry->priv->maxlen) 318 truncate_utf8_string (existing_text, entry->priv->maxlen); 319 tmp = g_strdup_printf ("%s%s%s", 320 entry->priv->prefix ? entry->priv->prefix : "", 321 existing_text ? existing_text : "", 322 entry->priv->suffix ? entry->priv->suffix : ""); 323 324 gtk_entry_set_text (GTK_ENTRY (entry), tmp); /* emits a "changed" signal */ 325 g_free (tmp); 326 signal_handlers_unblock (entry); 327 } 328 } 329 330 /** 331 * gdaui_entry_new: 332 * @prefix: (allow-none): a prefix (not modifiable) string, or %NULL 333 * @suffix: (allow-none): a suffix (not modifiable) string, or %NULL 334 * 335 * Creates a new #GdauiEntry widget. 336 * 337 * Returns: (transfer full): the newly created #GdauiEntry widget. 338 */ 339 GtkWidget* 340 gdaui_entry_new (const gchar *prefix, const gchar *suffix) 341 { 342 GObject *obj; 343 344 obj = g_object_new (GDAUI_TYPE_ENTRY, "prefix", prefix, "suffix", suffix, NULL); 345 return GTK_WIDGET (obj); 346 } 347 348 /** 349 * gdaui_entry_set_max_length: 350 * @entry: a #GdauiEntry. 351 * @max: the maximum length of the entry, or 0 for no maximum. 352 * 353 * Sets the maximum allowed length of the contents of the widget. 354 * If the current contents are longer than the given length, then they will be truncated to fit. 355 * 356 * The difference with gtk_entry_set_max_length() is that the max length does not take into account 357 * the prefix and/or suffix parts which may have been set. 358 */ 359 void 360 gdaui_entry_set_max_length (GdauiEntry *entry, gint max) 361 { 362 g_return_if_fail (GDAUI_IS_ENTRY (entry)); 363 364 g_object_set (G_OBJECT (entry), "max-length", max, NULL); 365 } 366 367 /** 368 * gdaui_entry_get_text: 369 * @entry: a #GdauiEntry. 370 * 371 * Get a new string containing the contents of the widget as a string without the 372 * prefix and/or suffix and/or format if they have been specified. This method differs 373 * from calling gtk_entry_get_text() since the latest will return the complete text 374 * in @entry including prefix and/or suffix and/or format. 375 * 376 * Note: %NULL may be returned if this method is called while the widget is working on some 377 * internal modifications, or if gdaui_entry_set_text() was called with a %NULL 378 * as its @text argument. 379 * 380 * Returns: a new string, or %NULL 381 */ 382 gchar * 383 gdaui_entry_get_text (GdauiEntry *entry) 384 { 385 gchar *text; 386 387 g_return_val_if_fail (GDAUI_IS_ENTRY (entry), NULL); 388 389 if (entry->priv->isnull) 390 text = NULL; 391 else { 392 const gchar *ctext; 393 gint len; 394 ctext = gtk_entry_get_text (GTK_ENTRY (entry)); 395 if (ctext) { 396 len = strlen (ctext); 397 text = g_strdup (ctext); 398 if (entry->priv->prefix) { 399 len -= entry->priv->prefix_len; 400 memmove (text, text + entry->priv->prefix_len, len+1); 401 } 402 if (entry->priv->suffix) { 403 len -= entry->priv->suffix_len; 404 text [len] = 0; 405 } 406 } 407 else 408 text = g_strdup (""); 409 } 410 411 return text; 412 } 413 414 /** 415 * gdaui_entry_set_text: 416 * @entry: a #GdauiEntry widget 417 * @text: (allow-none): the text to set into @entry, or %NULL 418 * 419 * Sets @text into @entry. 420 * 421 * As a side effect, if @text is %NULL, then the entry will 422 * be completely empty, whereas if @text is the empty string (""), then 423 * @entry will display the prefix and/or suffix and/or format string if they have 424 * been set. Except this case, calling this method is similar to calling 425 * gtk_entry_set_text() 426 */ 427 void 428 gdaui_entry_set_text (GdauiEntry *entry, const gchar *text) 429 { 430 g_return_if_fail (GDAUI_IS_ENTRY (entry)); 431 432 if (text) { 433 entry->priv->isnull = TRUE; 434 signal_handlers_block (entry); 435 gtk_entry_set_text (GTK_ENTRY (entry), ""); 436 signal_handlers_unblock (entry); 437 ENTER_INTERNAL_CHANGES(entry); 438 gtk_entry_set_text (GTK_ENTRY (entry), text); /* emits the "insert-text" signal which is treated */ 439 entry->priv->isnull = FALSE; /* in case it has not been set */ 440 LEAVE_INTERNAL_CHANGES(entry); 441 g_signal_emit_by_name (entry, "changed"); 442 } 443 else { 444 entry->priv->isnull = TRUE; 445 signal_handlers_block (entry); 446 gtk_entry_set_text (GTK_ENTRY (entry), ""); 447 signal_handlers_unblock (entry); 448 g_signal_emit_by_name (entry, "changed"); 449 } 450 } 451 452 /** 453 * gdaui_entry_set_prefix: 454 * @entry: a #GdauiEntry widget 455 * @prefix: a prefix string 456 * 457 * Sets @prefix as a prefix string of @entry: that string will always be displayed in the 458 * text entry, will not be modifiable, and won't be part of the returned text 459 */ 460 void 461 gdaui_entry_set_prefix (GdauiEntry *entry, const gchar *prefix) 462 { 463 g_return_if_fail (GDAUI_IS_ENTRY (entry)); 464 465 g_object_set (G_OBJECT (entry), "prefix", prefix, NULL); 466 } 467 468 /** 469 * gdaui_entry_set_suffix: 470 * @entry: a #GdauiEntry widget 471 * @suffix: a suffix string 472 * 473 * Sets @suffix as a suffix string of @entry: that string will always be displayed in the 474 * text entry, will not be modifiable, and won't be part of the returned text 475 */ 476 void 477 gdaui_entry_set_suffix (GdauiEntry *entry, const gchar *suffix) 478 { 479 g_return_if_fail (GDAUI_IS_ENTRY (entry)); 480 481 g_object_set (G_OBJECT (entry), "suffix", suffix, NULL); 482 } 483 484 485 /** 486 * gdaui_entry_set_width_chars: 487 * @entry: a #GdauiEntry widget 488 * @max_width: maximum width, or -1 489 * 490 * Sets @entry's maximum width in characters, without taking into account 491 * any prefix or suffix (which will automatically be handled). If you want to take 492 * a prefix or suffix into account direclty, then use gtk_entry_set_width_chars() 493 */ 494 void 495 gdaui_entry_set_width_chars (GdauiEntry *entry, gint max_width) 496 { 497 g_return_if_fail (GDAUI_IS_ENTRY (entry)); 498 if (max_width < 0) 499 gtk_entry_set_width_chars (GTK_ENTRY (entry), -1); 500 else { 501 max_width += entry->priv->prefix_clen; 502 max_width += entry->priv->suffix_clen; 503 gtk_entry_set_width_chars (GTK_ENTRY (entry), max_width); 504 } 505 } 506 507 /* 508 * callbacks 509 */ 510 511 static void 512 changed_cb (GtkEditable *editable, G_GNUC_UNUSED gpointer data) 513 { 514 GdauiEntry *entry = (GdauiEntry*) editable; 515 if (entry->priv->internal_changes > 0) 516 g_signal_stop_emission_by_name (editable, "changed"); 517 } 518 519 static void 520 delete_text_cb (GtkEditable *editable, gint start_pos, gint end_pos, G_GNUC_UNUSED gpointer data) 521 { 522 const gchar *otext = NULL; 523 gint len = 0; 524 gint nstart = start_pos, nend = end_pos; 525 GdauiEntry *entry = GDAUI_ENTRY (editable); 526 527 signal_handlers_block (entry); 528 if (entry->priv->prefix) { 529 if (nstart < entry->priv->prefix_clen) 530 nstart = entry->priv->prefix_clen; 531 } 532 if (nend < 0) { 533 otext = gtk_entry_get_text ((GtkEntry*) entry); 534 len = g_utf8_strlen (otext, -1); 535 nend = len; 536 } 537 538 if (nend - nstart < 1) { 539 g_signal_stop_emission_by_name (editable, "delete-text"); 540 signal_handlers_unblock (entry); 541 return; 542 } 543 544 if (entry->priv->suffix) { 545 if (!otext) { 546 otext = gtk_entry_get_text ((GtkEntry*) entry); 547 len = g_utf8_strlen (otext, -1); 548 } 549 if (nend - nstart == 1) { 550 if ((nstart >= len - entry->priv->suffix_clen)) { 551 nstart = len - entry->priv->suffix_clen - 1; 552 nend = nstart + 1; 553 g_signal_stop_emission_by_name (editable, "delete-text"); 554 signal_handlers_unblock (entry); 555 gtk_editable_set_position (editable, nend); 556 gtk_editable_delete_text (editable, nstart, nend); 557 return; 558 } 559 } 560 if (nend > len - entry->priv->suffix_clen) 561 nend = len - entry->priv->suffix_clen; 562 } 563 564 if (GDAUI_ENTRY_GET_CLASS (editable)->assume_delete) { 565 g_signal_stop_emission_by_name (editable, "delete-text"); 566 GDAUI_ENTRY_GET_CLASS (editable)->assume_delete (entry, nstart - entry->priv->prefix_clen, 567 nend - entry->priv->prefix_clen, 568 entry->priv->prefix_clen); 569 //g_print ("Subclass assumes text delete\n"); 570 } 571 else if ((nstart != start_pos) || (nend != end_pos)) { 572 g_signal_stop_emission_by_name (editable, "delete-text"); 573 if (nstart != nend) 574 gtk_editable_delete_text (editable, nstart, nend); 575 } 576 577 signal_handlers_unblock (entry); 578 g_signal_emit_by_name (entry, "changed"); 579 } 580 581 582 static void 583 insert_text_cb (GtkEditable *editable, const gchar *text, gint text_length, gint *position, 584 G_GNUC_UNUSED gpointer data) 585 { 586 const gchar *otext; 587 gint clen; 588 GdauiEntry *entry = GDAUI_ENTRY (editable); 589 gint text_clen; 590 gint start; 591 592 if (gtk_editable_get_selection_bounds (editable, &start, NULL)) 593 *position = start; 594 595 signal_handlers_block (entry); 596 597 if (entry->priv->isnull) { 598 gchar *etext = NULL; 599 entry->priv->isnull = FALSE; 600 if (GDAUI_ENTRY_GET_CLASS (editable)->get_empty_text) 601 etext = GDAUI_ENTRY_GET_CLASS (editable)->get_empty_text (entry); 602 adjust_display (entry, etext ? etext : ""); 603 g_free (etext); 604 } 605 606 otext = gtk_entry_get_text ((GtkEntry*) entry); 607 clen = g_utf8_strlen (otext, -1); 608 609 /* adjust insert position */ 610 if (entry->priv->prefix) { 611 if (*position < entry->priv->prefix_clen) 612 *position = entry->priv->prefix_clen; 613 } 614 if (entry->priv->suffix) { 615 if (*position > clen - entry->priv->suffix_clen) 616 *position = clen - entry->priv->suffix_clen; 617 } 618 619 /* test if the whole insertion is Ok */ 620 text_clen = g_utf8_strlen (text, text_length); 621 if (clen - entry->priv->prefix_clen - entry->priv->suffix_clen + text_clen > entry->priv->maxlen) { 622 gchar *itext; 623 gint nallowed; 624 nallowed = entry->priv->maxlen - (clen - entry->priv->prefix_clen - entry->priv->suffix_clen); 625 g_signal_stop_emission_by_name (editable, "insert-text"); 626 itext = g_strdup (text); 627 itext [nallowed] = 0; /* FIXME: convert nallowed to gchar */ 628 /*g_print ("Corrected by length insert text: [%s]\n", itext);*/ 629 if (*itext) 630 gtk_editable_insert_text (editable, itext, nallowed, position); 631 g_free (itext); 632 633 signal_handlers_unblock (entry); 634 g_signal_emit_by_name (entry, "changed"); 635 } 636 else if (GDAUI_ENTRY_GET_CLASS (editable)->assume_insert) { 637 g_signal_stop_emission_by_name (editable, "insert-text"); 638 //g_print ("Subclass assumes text insert\n"); 639 gint pos = *position - entry->priv->prefix_clen; 640 GDAUI_ENTRY_GET_CLASS (editable)->assume_insert (entry, text, text_length, 641 &pos, entry->priv->prefix_clen); 642 *position = pos + entry->priv->prefix_clen; 643 644 signal_handlers_unblock (entry); 645 g_signal_emit_by_name (entry, "changed"); 646 } 647 else 648 signal_handlers_unblock (entry); 649 } 650