addr_compl.c (51543B)
1 /* 2 * Claws Mail -- a GTK based, lightweight, and fast e-mail client 3 * 4 * Copyright (C) 2000-2024 the Claws Mail team and Alfons Hoogervorst 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 3 of the License, or 9 * (at your option) any later version. 10 * 11 * This program 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 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 #include "defs.h" 21 22 #include <glib.h> 23 #include <glib/gi18n.h> 24 #include <gdk/gdkkeysyms.h> 25 #include <gtk/gtk.h> 26 27 #include <string.h> 28 #include <ctype.h> 29 #include <wchar.h> 30 #include <wctype.h> 31 32 #include "addr_compl.h" 33 #include "addritem.h" 34 #include "utils.h" 35 #include "prefs_common.h" 36 #include "claws.h" 37 #include "hooks.h" 38 #include "gtkutils.h" 39 #include "stock_pixmap.h" 40 #include <pthread.h> 41 42 #include "addrindex.h" 43 44 45 /*! 46 *\brief For the GtkListStore 47 */ 48 enum { 49 ADDR_COMPL_ICON, 50 ADDR_COMPL_ADDRESS, 51 ADDR_COMPL_ISGROUP, 52 ADDR_COMPL_GROUPLIST, 53 N_ADDR_COMPL_COLUMNS 54 }; 55 56 /* 57 * How it works: 58 * 59 * The address book is read into memory. We set up an address list 60 * containing all address book entries. Next we make the completion 61 * list, which contains all the completable strings, and store a 62 * reference to the address entry it belongs to. 63 * After calling the g_completion_complete(), we get a reference 64 * to a valid email address. 65 * 66 * Completion is very simplified. We never complete on another prefix, 67 * i.e. we neglect the next smallest possible prefix for the current 68 * completion cache. This is simply done so we might break up the 69 * addresses a little more (e.g. break up alfons@proteus.demon.nl into 70 * something like alfons, proteus, demon, nl; and then completing on 71 * any of those words). 72 */ 73 74 /** 75 * completion_entry - structure used to complete addresses, with a reference 76 * the the real address information. 77 */ 78 typedef struct 79 { 80 gchar *string; /* string to complete */ 81 address_entry *ref; /* address the string belongs to */ 82 } completion_entry; 83 84 /*******************************************************************************/ 85 86 static gint g_ref_count; /* list ref count */ 87 static GList *g_completion_list = NULL; /* list of strings to be checked */ 88 static GList *g_address_list = NULL; /* address storage */ 89 static GCompletion *g_completion; /* completion object */ 90 91 static GHashTable *_groupAddresses_ = NULL; 92 static gboolean _allowCommas_ = TRUE; 93 94 /* To allow for continuing completion we have to keep track of the state 95 * using the following variables. No need to create a context object. */ 96 97 static gint g_completion_count; /* nr of addresses incl. the prefix */ 98 static gint g_completion_next; /* next prev address */ 99 static GSList *g_completion_addresses; /* unique addresses found in the 100 completion cache. */ 101 static gchar *g_completion_prefix; /* last prefix. (this is cached here 102 * because the prefix passed to g_completion 103 * is g_utf8_strdown()'ed */ 104 105 static gchar *completion_folder_path = NULL; 106 107 /*******************************************************************************/ 108 109 /* 110 * Define the structure of the completion window. 111 */ 112 typedef struct _CompletionWindow CompletionWindow; 113 struct _CompletionWindow { 114 gint listCount; 115 gchar *searchTerm; 116 GtkWidget *window; 117 GtkWidget *entry; 118 GtkWidget *list_view; 119 120 gboolean in_mouse; /*!< mouse press pending... */ 121 gboolean destroying; /*!< destruction in progress */ 122 }; 123 124 static GtkListStore *addr_compl_create_store (void); 125 126 static GtkWidget *addr_compl_list_view_create (CompletionWindow *window); 127 128 static void addr_compl_create_list_view_columns (GtkWidget *list_view); 129 130 static gboolean list_view_button_press (GtkWidget *widget, 131 GdkEventButton *event, 132 CompletionWindow *window); 133 134 static gboolean list_view_button_release (GtkWidget *widget, 135 GdkEventButton *event, 136 CompletionWindow *window); 137 138 static gboolean addr_compl_selected (GtkTreeSelection *selector, 139 GtkTreeModel *model, 140 GtkTreePath *path, 141 gboolean currently_selected, 142 gpointer data); 143 144 static gboolean addr_compl_defer_select_destruct(CompletionWindow *window); 145 146 /** 147 * Function used by GTK to find the string data to be used for completion. 148 * \param data Pointer to data being processed. 149 */ 150 static gchar *completion_func(gpointer data) 151 { 152 cm_return_val_if_fail(data != NULL, NULL); 153 154 return ((completion_entry *)data)->string; 155 } 156 157 static gint addr_completion_func(const gchar *needle, const gchar *haystack, 158 gsize n) 159 { 160 if (needle == NULL || haystack == NULL) 161 return 1; 162 163 return (strcasestr(haystack, needle) != NULL ? 0 : 1); 164 } 165 166 /** 167 * Function used by GTK to compare elements for sorting 168 * name match beginning > name match after space > email address 169 * match beginning and full match before @ > email adress 170 * match beginning. Otherwise match position in string. 171 * \param a first element in comparsion 172 * \param b second element in comparison 173 */ 174 static gint weight_addr_match(const address_entry* addr) 175 { 176 gint n_weight = addr->name ? strlen(addr->name): 0; 177 gint a_weight = addr->address ? strlen(addr->address) : n_weight; 178 gchar* match = NULL; 179 180 if (addr->name) 181 match = strcasestr(addr->name, g_completion_prefix); 182 183 if (match != NULL) { 184 if (match == addr->name) 185 n_weight = -4; 186 else if (match > addr->name && *(match - 1) == ' ') 187 n_weight = -3; 188 else 189 n_weight = match - addr->name; 190 } 191 192 if (addr->address) { 193 match = strcasestr(addr->address, g_completion_prefix); 194 if (match != NULL) { 195 if (match == addr->address) 196 a_weight = -1; 197 else 198 a_weight = match - addr->address; 199 200 if (strlen(match) > strlen(g_completion_prefix) 201 && *(match + strlen(g_completion_prefix)) == '@') 202 a_weight--; 203 } 204 } 205 206 if (n_weight == -4 && a_weight < 0) 207 n_weight = -5; 208 209 return MIN(a_weight, n_weight); 210 } 211 212 static gint addr_comparison_func(gconstpointer a, gconstpointer b) 213 { 214 const address_entry* a_ref = (const address_entry*)a; 215 const address_entry* b_ref = (const address_entry*)b; 216 gint a_weight = weight_addr_match(a_ref); 217 gint b_weight = weight_addr_match(b_ref); 218 gint cmp; 219 220 if (a_weight < b_weight) 221 return -1; 222 else if (a_weight > b_weight) 223 return 1; 224 else { 225 if (!a_ref->name || !b_ref->name) 226 cmp = !!a_ref->name - !!b_ref->name; 227 else 228 cmp = strcmp(a_ref->name, b_ref->name); 229 if (!cmp) 230 { 231 if (!a_ref->address || !b_ref->address) 232 cmp = !!a_ref->address - !!b_ref->address; 233 else 234 cmp = g_strcmp0(a_ref->address, b_ref->address); 235 } 236 return cmp; 237 } 238 } 239 240 /** 241 * Initialize all completion index data. 242 */ 243 static void init_all(void) 244 { 245 g_completion = g_completion_new(completion_func); 246 cm_return_if_fail(g_completion != NULL); 247 } 248 249 /** 250 * set the compare function (default is strncmp) 251 */ 252 static void set_match_any_part(const gboolean any_part) 253 { 254 if (any_part && prefs_common.address_search_wildcard) 255 g_completion_set_compare(g_completion, addr_completion_func); 256 else 257 g_completion_set_compare(g_completion, strncmp); 258 } 259 260 static void free_all_addresses(void) 261 { 262 GList *walk; 263 if (!g_address_list) 264 return; 265 walk = g_address_list; 266 for (; walk != NULL; walk = g_list_next(walk)) { 267 address_entry *ae = (address_entry *) walk->data; 268 g_free(ae->name); 269 g_free(ae->address); 270 g_list_free(ae->grp_emails); 271 g_free(walk->data); 272 } 273 g_list_free(g_address_list); 274 g_address_list = NULL; 275 if (_groupAddresses_) 276 g_hash_table_destroy(_groupAddresses_); 277 _groupAddresses_ = NULL; 278 } 279 280 static void clear_completion_cache(void); 281 static void free_completion_list(void) 282 { 283 GList *walk; 284 if (!g_completion_list) 285 return; 286 287 clear_completion_cache(); 288 if (g_completion) 289 g_completion_clear_items(g_completion); 290 291 walk = g_list_first(g_completion_list); 292 for (; walk != NULL; walk = g_list_next(walk)) { 293 completion_entry *ce = (completion_entry *) walk->data; 294 g_free(ce->string); 295 g_free(walk->data); 296 } 297 g_list_free(g_completion_list); 298 g_completion_list = NULL; 299 } 300 /** 301 * Free up all completion index data. 302 */ 303 static void free_all(void) 304 { 305 free_completion_list(); 306 free_all_addresses(); 307 g_completion_free(g_completion); 308 g_completion = NULL; 309 } 310 311 /** 312 * Append specified address entry to the index. 313 * \param str Index string value. 314 * \param ae Entry containing address data. 315 */ 316 void addr_compl_add_address1(const char *str, address_entry *ae) 317 { 318 completion_entry *ce1; 319 ce1 = g_new0(completion_entry, 1), 320 /* GCompletion list is case sensitive */ 321 ce1->string = g_utf8_strdown(str, -1); 322 ce1->ref = ae; 323 324 g_completion_list = g_list_prepend(g_completion_list, ce1); 325 } 326 327 /** 328 * Adds address to the completion list. This function looks complicated, but 329 * it's only allocation checks. Each value will be included in the index. 330 * \param name Recipient name. 331 * \param address EMail address. 332 * \param alias Alias to append. 333 * \param grp_emails the emails in case of a group. List should be freed later, 334 * but not its strings 335 * \return <code>0</code> if entry appended successfully, or <code>-1</code> 336 * if failure. 337 */ 338 static gint add_address(const gchar *name, const gchar *address, 339 const gchar *nick, const gchar *alias, GList *grp_emails) 340 { 341 address_entry *ae; 342 343 if (!address && !grp_emails) 344 return -1; 345 346 if (!name) 347 name = ""; 348 349 ae = g_new0(address_entry, 1); 350 cm_return_val_if_fail(ae != NULL, -1); 351 352 ae->name = g_strdup(name); 353 ae->address = g_strdup(address); 354 ae->grp_emails = grp_emails; 355 g_address_list = g_list_prepend(g_address_list, ae); 356 357 addr_compl_add_address1(name, ae); 358 359 if (address != NULL && *address != '\0') 360 addr_compl_add_address1(address, ae); 361 362 if (nick != NULL && *nick != '\0') 363 addr_compl_add_address1(nick, ae); 364 365 if (alias != NULL && *alias != '\0') 366 addr_compl_add_address1(alias, ae); 367 368 return 0; 369 } 370 371 /** 372 * Read address book, creating all entries in the completion index. 373 */ 374 static void read_address_book(gchar *folderpath) { 375 free_all_addresses(); 376 free_completion_list(); 377 378 addrindex_load_completion( add_address, folderpath ); 379 380 /* plugins may hook in here to modify/extend the completion list */ 381 if(!folderpath) { 382 hooks_invoke(ADDDRESS_COMPLETION_BUILD_ADDRESS_LIST_HOOKLIST, &g_address_list); 383 } 384 385 g_address_list = g_list_reverse(g_address_list); 386 g_completion_list = g_list_reverse(g_completion_list); 387 /* merge the completion entry list into g_completion */ 388 if (g_completion_list) { 389 g_completion_add_items(g_completion, g_completion_list); 390 if (debug_get_mode()) 391 debug_print("read %d items in %s\n", 392 g_list_length(g_completion_list), 393 folderpath?folderpath:"(null)"); 394 } 395 } 396 397 /** 398 * Test whether there is a completion pending. 399 * \return <code>TRUE</code> if pending. 400 */ 401 static gboolean is_completion_pending(void) 402 { 403 /* check if completion pending, i.e. we might satisfy a request for the next 404 * or previous address */ 405 return g_completion_count; 406 } 407 408 /** 409 * Clear the completion cache. 410 */ 411 static void clear_completion_cache(void) 412 { 413 if (is_completion_pending()) { 414 g_free(g_completion_prefix); 415 416 if (g_completion_addresses) { 417 g_slist_free(g_completion_addresses); 418 g_completion_addresses = NULL; 419 } 420 421 g_completion_count = g_completion_next = 0; 422 } 423 } 424 425 /** 426 * Prepare completion index. This function should be called prior to attempting 427 * address completion. 428 * \return The number of addresses in the completion list. 429 */ 430 guint start_address_completion(gchar *folderpath) 431 { 432 gboolean different_book = FALSE; 433 clear_completion_cache(); 434 435 if (g_strcmp0(completion_folder_path,folderpath)) 436 different_book = TRUE; 437 438 g_free(completion_folder_path); 439 if (folderpath != NULL) 440 completion_folder_path = g_strdup(folderpath); 441 else 442 completion_folder_path = NULL; 443 444 if (!g_ref_count) { 445 init_all(); 446 /* open the address book */ 447 read_address_book(folderpath); 448 } else if (different_book) 449 read_address_book(folderpath); 450 451 g_ref_count++; 452 debug_print("start_address_completion(%s) ref count %d\n", 453 folderpath?folderpath:"(null)", g_ref_count); 454 455 return g_list_length(g_completion_list); 456 } 457 458 /** 459 * Retrieve a possible address (or a part) from an entry box. To make life 460 * easier, we only look at the last valid address component; address 461 * completion only works at the last string component in the entry box. 462 * 463 * \param entry Address entry field. 464 * \param start_pos Address of start position of address. 465 * \return Possible address. 466 */ 467 static gchar *get_address_from_edit(GtkEntry *entry, gint *start_pos) 468 { 469 const gchar *edit_text, *p; 470 gint cur_pos; 471 gboolean in_quote = FALSE; 472 gboolean in_bracket = FALSE; 473 gchar *str; 474 475 edit_text = gtk_entry_get_text(entry); 476 if (edit_text == NULL) return NULL; 477 478 cur_pos = gtk_editable_get_position(GTK_EDITABLE(entry)); 479 480 /* scan for a separator. doesn't matter if walk points at null byte. */ 481 for (p = g_utf8_offset_to_pointer(edit_text, cur_pos); 482 p > edit_text; 483 p = g_utf8_prev_char(p)) { 484 if (*p == '"') { 485 in_quote = TRUE; 486 } else if (!in_quote) { 487 if (!in_bracket && *p == ',') { 488 break; 489 } else if (*p == '<') 490 in_bracket = TRUE; 491 else if (*p == '>') 492 in_bracket = FALSE; 493 } 494 } 495 496 /* have something valid */ 497 if (g_utf8_strlen(p, -1) == 0) 498 return NULL; 499 500 #define IS_VALID_CHAR(x) \ 501 (g_ascii_isalnum(x) || (x) == '"' || (x) == '<' || (((unsigned char)(x)) > 0x7f)) 502 503 /* now scan back until we hit a valid character */ 504 for (; *p && !IS_VALID_CHAR(*p); p = g_utf8_next_char(p)) 505 ; 506 507 #undef IS_VALID_CHAR 508 509 if (g_utf8_strlen(p, -1) == 0) 510 return NULL; 511 512 if (start_pos) *start_pos = g_utf8_pointer_to_offset(edit_text, p); 513 514 str = g_strdup(p); 515 516 return str; 517 } 518 519 static gchar *get_complete_address_from_name_email(const gchar *name, const gchar *email) 520 { 521 gchar *address = NULL; 522 if (!name || name[0] == '\0') 523 address = g_strdup_printf("<%s>", email); 524 else if (strchr_with_skip_quote(name, '"', ',')) 525 address = g_strdup_printf 526 ("\"%s\" <%s>", name, email); 527 else 528 address = g_strdup_printf 529 ("%s <%s>", name, email); 530 return address; 531 } 532 533 /** 534 * Replace an incompleted address with a completed one. 535 * \param entry Address entry field. 536 * \param newtext New text. 537 * \param start_pos Insertion point in entry field. 538 */ 539 static void replace_address_in_edit(GtkEntry *entry, const gchar *newtext, 540 gint start_pos, gboolean is_group, GList *grp_emails) 541 { 542 if (!newtext) return; 543 gtk_editable_delete_text(GTK_EDITABLE(entry), start_pos, -1); 544 if (!is_group) { 545 gtk_editable_insert_text(GTK_EDITABLE(entry), newtext, strlen(newtext), 546 &start_pos); 547 } else { 548 gchar *addresses = NULL; 549 GList *cur = grp_emails; 550 for (; cur; cur = cur->next) { 551 gchar *tmp; 552 ItemEMail *email = (ItemEMail *)cur->data; 553 ItemPerson *person = ( ItemPerson * ) ADDRITEM_PARENT(email); 554 555 gchar *addr = get_complete_address_from_name_email( 556 ADDRITEM_NAME(person), email->address); 557 if (addresses) 558 tmp = g_strdup_printf("%s, %s", addresses, addr); 559 else 560 tmp = g_strdup_printf("%s", addr); 561 g_free(addr); 562 g_free(addresses); 563 addresses = tmp; 564 } 565 gtk_editable_insert_text(GTK_EDITABLE(entry), addresses, strlen(addresses), 566 &start_pos); 567 g_free(addresses); 568 } 569 gtk_editable_set_position(GTK_EDITABLE(entry), -1); 570 } 571 572 /** 573 * Attempt to complete an address, and returns the number of addresses found. 574 * Use <code>get_complete_address()</code> to get an entry from the index. 575 * 576 * \param str Search string to find. 577 * \return Zero if no match was found, otherwise the number of addresses; the 578 * original prefix (search string) will appear at index 0. 579 */ 580 guint complete_address(const gchar *str) 581 { 582 GList *result = NULL; 583 gchar *d = NULL; 584 guint count = 0; 585 guint cpl = 0; 586 completion_entry *ce = NULL; 587 588 cm_return_val_if_fail(str != NULL, 0); 589 590 /* g_completion is case sensitive */ 591 d = g_utf8_strdown(str, -1); 592 593 clear_completion_cache(); 594 g_completion_prefix = g_strdup(str); 595 596 result = g_completion_complete(g_completion, d, NULL); 597 598 count = g_list_length(result); 599 if (count) { 600 /* create list with unique addresses */ 601 for (cpl = 0, result = g_list_first(result); 602 result != NULL; 603 result = g_list_next(result)) { 604 ce = (completion_entry *)(result->data); 605 if (NULL == g_slist_find(g_completion_addresses, 606 ce->ref)) { 607 cpl++; 608 g_completion_addresses = 609 g_slist_append(g_completion_addresses, 610 ce->ref); 611 } 612 } 613 count = cpl + 1; /* index 0 is the original prefix */ 614 g_completion_next = 1; /* we start at the first completed one */ 615 if (prefs_common.address_search_wildcard) 616 g_completion_addresses = g_slist_sort(g_completion_addresses, 617 addr_comparison_func); 618 } else { 619 g_free(g_completion_prefix); 620 g_completion_prefix = NULL; 621 } 622 623 g_completion_count = count; 624 625 g_free(d); 626 627 return count; 628 } 629 630 /** 631 * complete_matches_found() returns the number of matched addresses according 632 * to the completion mechanism. Unlike complete_address(), the returned value 633 * doesn't count str itself. If there's no match, it returns 0. 634 * To get a list of completion matches, see complete_address() instead. 635 */ 636 guint complete_matches_found(const gchar *str) 637 { 638 GList *result = NULL; 639 gchar *d = NULL; 640 641 cm_return_val_if_fail(str != NULL, 0); 642 643 /* g_completion is case sensitive */ 644 d = g_utf8_strdown(str, -1); 645 646 clear_completion_cache(); 647 g_completion_prefix = g_strdup(str); 648 649 result = g_completion_complete(g_completion, d, NULL); 650 651 g_free(g_completion_prefix); 652 g_free(d); 653 654 return g_list_length(result); 655 } 656 657 /** 658 * Return a complete address from the index. 659 * \param index Index of entry that was found (by the previous call to 660 * <code>complete_address()</code> 661 * \return Completed address string; this should be freed when done. 662 */ 663 gchar *get_complete_address(gint index) 664 { 665 const address_entry *p; 666 gchar *address = NULL; 667 668 if (index < g_completion_count) { 669 if (index == 0) 670 address = g_strdup(g_completion_prefix); 671 else { 672 /* get something from the unique addresses */ 673 p = (address_entry *)g_slist_nth_data 674 (g_completion_addresses, index - 1); 675 if (p != NULL && p->address != NULL) { 676 address = get_complete_address_from_name_email(p->name, p->address); 677 } else if (p != NULL && p->address == NULL && p->name != NULL) { 678 /* that's a group */ 679 address = g_strdup_printf("%s (%s) <!--___group___-->", p->name, _("Group")); 680 if (!_groupAddresses_) { 681 _groupAddresses_ = g_hash_table_new(NULL, g_direct_equal); 682 } 683 if (!g_hash_table_lookup(_groupAddresses_, GINT_TO_POINTER(g_str_hash(address)))) { 684 g_hash_table_insert(_groupAddresses_, GINT_TO_POINTER(g_str_hash(address)), p->grp_emails); 685 686 } 687 } 688 } 689 } 690 691 return address; 692 } 693 694 /** 695 * Return the next complete address match from the completion index. 696 * \return Completed address string; this should be freed when done. 697 */ 698 static gchar *get_next_complete_address(void) 699 { 700 if (is_completion_pending()) { 701 gchar *res; 702 703 res = get_complete_address(g_completion_next); 704 g_completion_next += 1; 705 if (g_completion_next >= g_completion_count) 706 g_completion_next = 0; 707 708 return res; 709 } else 710 return NULL; 711 } 712 713 /** 714 * Return a count of the completed matches in the completion index. 715 * \return Number of matched entries. 716 */ 717 static guint get_completion_count(void) 718 { 719 if (is_completion_pending()) 720 return g_completion_count; 721 else 722 return 0; 723 } 724 725 /** 726 * Invalidate address completion index. This function should be called whenever 727 * the address book changes. This forces data to be read into the completion 728 * data. 729 * \return Number of entries in index. 730 */ 731 gint invalidate_address_completion(void) 732 { 733 if (g_ref_count) { 734 /* simply the same as start_address_completion() */ 735 debug_print("Invalidation request for address completion\n"); 736 read_address_book(completion_folder_path); 737 clear_completion_cache(); 738 } 739 740 return g_list_length(g_completion_list); 741 } 742 743 /** 744 * Finished with completion index. This function should be called after 745 * matching addresses. 746 * \return Reference count. 747 */ 748 gint end_address_completion(void) 749 { 750 gboolean different_folder = FALSE; 751 clear_completion_cache(); 752 753 /* reset the folderpath to NULL */ 754 if (completion_folder_path) { 755 g_free(completion_folder_path); 756 completion_folder_path = NULL; 757 different_folder = TRUE; 758 } 759 if (0 == --g_ref_count) 760 free_all(); 761 762 debug_print("end_address_completion ref count %d\n", g_ref_count); 763 if (g_ref_count && different_folder) { 764 debug_print("still ref'd, different folder\n"); 765 invalidate_address_completion(); 766 } 767 768 return g_ref_count; 769 } 770 771 /** 772 * Completion window. 773 */ 774 static CompletionWindow *_compWindow_ = NULL; 775 776 /** 777 * Mutex to protect callback from multiple threads. 778 */ 779 static pthread_mutex_t _completionMutex_ = PTHREAD_MUTEX_INITIALIZER; 780 781 /** 782 * Completion queue list. 783 */ 784 static GList *_displayQueue_ = NULL; 785 /** 786 * Current query ID. 787 */ 788 static gint _queryID_ = 0; 789 790 /** 791 * Completion idle ID. 792 */ 793 static guint _completionIdleID_ = 0; 794 795 /* 796 * address completion entry ui. the ui (completion list was inspired by galeon's 797 * auto completion list). remaining things powered by claws's completion engine. 798 */ 799 800 #define ENTRY_DATA_TAB_HOOK "tab_hook" /* used to lookup entry */ 801 #define ENTRY_DATA_ALLOW_COMMAS "allowcommas" /* used to know whether to present groups */ 802 803 static void address_completion_mainwindow_set_focus (GtkWindow *window, 804 GtkWidget *widget, 805 gpointer data); 806 static gboolean address_completion_entry_key_pressed (GtkEntry *entry, 807 GdkEventKey *ev, 808 gpointer data); 809 static gboolean address_completion_complete_address_in_entry 810 (GtkEntry *entry, 811 gboolean next); 812 static void address_completion_create_completion_window (GtkEntry *entry); 813 814 static gboolean completion_window_button_press 815 (GtkWidget *widget, 816 GdkEventButton *event, 817 CompletionWindow *compWin ); 818 819 static gboolean completion_window_key_press 820 (GtkWidget *widget, 821 GdkEventKey *event, 822 CompletionWindow *compWin ); 823 static void address_completion_create_completion_window( GtkEntry *entry_ ); 824 825 /** 826 * Create a completion window object. 827 * \return Initialized completion window. 828 */ 829 static CompletionWindow *addrcompl_create_window( void ) { 830 CompletionWindow *cw; 831 832 cw = g_new0( CompletionWindow, 1 ); 833 cw->listCount = 0; 834 cw->searchTerm = NULL; 835 cw->window = NULL; 836 cw->entry = NULL; 837 cw->list_view = NULL; 838 cw->in_mouse = FALSE; 839 cw->destroying = FALSE; 840 841 return cw; 842 } 843 844 /** 845 * Destroy completion window. 846 * \param cw Window to destroy. 847 */ 848 static void addrcompl_destroy_window( CompletionWindow *cw ) { 849 GdkDisplay *display; 850 GdkSeat *seat; 851 852 display = gdk_display_get_default(); 853 seat = gdk_display_get_default_seat(display); 854 /* Stop all searches currently in progress */ 855 addrindex_stop_search( _queryID_ ); 856 /* Remove idler function... or application may not terminate */ 857 if( _completionIdleID_ != 0 ) { 858 g_source_remove( _completionIdleID_ ); 859 _completionIdleID_ = 0; 860 } 861 862 /* Now destroy window */ 863 if( cw ) { 864 /* Clear references to widgets */ 865 cw->entry = NULL; 866 cw->list_view = NULL; 867 868 /* Free objects */ 869 if( cw->window ) { 870 gtk_widget_hide( cw->window ); 871 gtk_widget_destroy( cw->window ); 872 } 873 cw->window = NULL; 874 cw->destroying = FALSE; 875 cw->in_mouse = FALSE; 876 } 877 878 /* Re-enable keyboard, required at least for Gtk3/Win32 */ 879 gdk_seat_ungrab(seat); 880 } 881 882 /** 883 * Free up completion window. 884 * \param cw Window to free. 885 */ 886 static void addrcompl_free_window( CompletionWindow *cw ) { 887 if( cw ) { 888 addrcompl_destroy_window( cw ); 889 890 g_free( cw->searchTerm ); 891 cw->searchTerm = NULL; 892 893 /* Clear references */ 894 cw->listCount = 0; 895 896 /* Free object */ 897 g_free( cw ); 898 } 899 } 900 901 /** 902 * Advance selection to previous/next item in list. 903 * \param list_view List to process. 904 * \param forward Set to <i>TRUE</i> to select next or <i>FALSE</i> for 905 * previous entry. 906 */ 907 static void completion_window_advance_selection(GtkTreeView *list_view, gboolean forward) 908 { 909 GtkTreeSelection *selection; 910 GtkTreeIter iter; 911 GtkTreeModel *model; 912 913 cm_return_if_fail(list_view != NULL); 914 915 selection = gtk_tree_view_get_selection(list_view); 916 if (!gtk_tree_selection_get_selected(selection, &model, &iter)) 917 return; 918 919 if (forward) { 920 forward = gtk_tree_model_iter_next(model, &iter); 921 if (forward) 922 gtk_tree_selection_select_iter(selection, &iter); 923 } else { 924 GtkTreePath *prev; 925 926 prev = gtk_tree_model_get_path(model, &iter); 927 if (!prev) 928 return; 929 930 if (gtk_tree_path_prev(prev)) 931 gtk_tree_selection_select_path(selection, prev); 932 933 gtk_tree_path_free(prev); 934 } 935 } 936 937 /** 938 * Resize window to accommodate maximum number of address entries. 939 * \param cw Completion window. 940 */ 941 static void addrcompl_resize_window( CompletionWindow *cw ) { 942 GdkDisplay *display; 943 GtkRequisition r; 944 GdkGrabStatus status; 945 gint x, y, width; 946 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(cw->list_view)); 947 GtkTreeIter iter; 948 GdkRectangle cell_rect; 949 gint extra_height = 0; 950 951 gdk_window_get_position(gtk_widget_get_window(cw->window), &x, &y); 952 width = gdk_window_get_width(gtk_widget_get_window(cw->window)); 953 954 gtk_widget_queue_resize_no_redraw(cw->list_view); 955 gtk_widget_get_preferred_size(cw->list_view, &r, NULL); 956 957 if (model && gtk_tree_model_get_iter_first(model, &iter)) { 958 GtkTreePath *path = gtk_tree_model_get_path(model, &iter); 959 960 gtk_tree_view_get_cell_area(GTK_TREE_VIEW(cw->list_view), path, NULL, &cell_rect); 961 gtk_tree_path_free(path); 962 extra_height = cell_rect.height + 4; 963 } 964 gtk_widget_set_size_request(cw->window, width, r.height + extra_height); 965 966 display = gdk_display_get_default(); 967 status = gdk_seat_grab(gdk_display_get_default_seat(display), 968 gtk_widget_get_window(cw->window), 969 GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | 970 GDK_BUTTON_RELEASE_MASK, 971 TRUE, NULL, NULL, NULL, NULL); 972 if (status != GDK_GRAB_SUCCESS) 973 g_warning("gdk_seat_grab failed with status %d", status); 974 gtk_grab_add(cw->window); 975 976 } 977 978 static GdkPixbuf *group_pixbuf = NULL; 979 static GdkPixbuf *email_pixbuf = NULL; 980 981 /** 982 * Add an address the completion window address list. 983 * \param cw Completion window. 984 * \param address Address to add. 985 */ 986 static void addrcompl_add_entry( CompletionWindow *cw, gchar *address ) { 987 GtkListStore *store; 988 GtkTreeIter iter; 989 GtkTreeSelection *selection; 990 gboolean is_group = FALSE; 991 GList *grp_emails = NULL; 992 store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(cw->list_view))); 993 GdkPixbuf *pixbuf; 994 995 if (!group_pixbuf) { 996 stock_pixbuf_gdk(STOCK_PIXMAP_ADDR_TWO, &group_pixbuf); 997 g_object_ref(G_OBJECT(group_pixbuf)); 998 } 999 if (!email_pixbuf) { 1000 stock_pixbuf_gdk(STOCK_PIXMAP_ADDR_ONE, &email_pixbuf); 1001 g_object_ref(G_OBJECT(email_pixbuf)); 1002 } 1003 /* g_print( "\t\tAdding :%s\n", address ); */ 1004 if (strstr(address, " <!--___group___-->")) { 1005 is_group = TRUE; 1006 if (_groupAddresses_) 1007 grp_emails = g_hash_table_lookup(_groupAddresses_, GINT_TO_POINTER(g_str_hash(address))); 1008 *(strstr(address, " <!--___group___-->")) = '\0'; 1009 pixbuf = group_pixbuf; 1010 } else if (strchr(address, '@') && strchr(address, '<') && 1011 strchr(address, '>')) { 1012 pixbuf = email_pixbuf; 1013 } else 1014 pixbuf = NULL; 1015 1016 if (is_group && !_allowCommas_) 1017 return; 1018 gtk_list_store_append(store, &iter); 1019 gtk_list_store_set(store, &iter, 1020 ADDR_COMPL_ICON, pixbuf, 1021 ADDR_COMPL_ADDRESS, address, 1022 ADDR_COMPL_ISGROUP, is_group, 1023 ADDR_COMPL_GROUPLIST, grp_emails, 1024 -1); 1025 cw->listCount++; 1026 1027 /* Resize window */ 1028 addrcompl_resize_window( cw ); 1029 gtk_grab_add( cw->window ); 1030 1031 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(cw->list_view)); 1032 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter)) 1033 return; 1034 1035 if (cw->listCount == 1) { 1036 /* Select first row for now */ 1037 gtk_tree_selection_select_iter(selection, &iter); 1038 } 1039 } 1040 1041 void addrcompl_reflect_prefs_pixmap_theme(void) { 1042 if (group_pixbuf) { 1043 g_object_unref(G_OBJECT(group_pixbuf)); 1044 group_pixbuf = NULL; 1045 } 1046 if (email_pixbuf) { 1047 g_object_unref(G_OBJECT(email_pixbuf)); 1048 email_pixbuf = NULL; 1049 } 1050 } 1051 1052 /** 1053 * Completion idle function. This function is called by the main (UI) thread 1054 * during UI idle time while an address search is in progress. Items from the 1055 * display queue are processed and appended to the address list. 1056 * 1057 * \param data Target completion window to receive email addresses. 1058 * \return <i>TRUE</i> to ensure that idle event do not get ignored. 1059 */ 1060 static gboolean addrcompl_idle( gpointer data ) { 1061 GList *node; 1062 gchar *address; 1063 1064 /* Process all entries in display queue */ 1065 pthread_mutex_lock( & _completionMutex_ ); 1066 if( _displayQueue_ ) { 1067 node = _displayQueue_; 1068 node = g_list_next(node); /* skip search term */ 1069 while( node ) { 1070 address = node->data; 1071 /* g_print( "address ::: %s :::\n", address ); */ 1072 addrcompl_add_entry( _compWindow_, address ); 1073 g_free( address ); 1074 node = g_list_next( node ); 1075 } 1076 g_list_free( _displayQueue_ ); 1077 _displayQueue_ = NULL; 1078 } 1079 pthread_mutex_unlock( & _completionMutex_ ); 1080 claws_do_idle(); 1081 1082 return TRUE; 1083 } 1084 1085 /** 1086 * Callback entry point. The background thread (if any) appends the address 1087 * list to the display queue. 1088 * \param sender Sender of query. 1089 * \param queryID Query ID of search request. 1090 * \param listEMail List of zero of more email objects that met search 1091 * criteria. 1092 * \param data Query data. 1093 */ 1094 static gint addrcompl_callback_entry( 1095 gpointer sender, gint queryID, GList *listEMail, gpointer data ) 1096 { 1097 GList *node; 1098 gchar *address; 1099 1100 /* g_print( "addrcompl_callback_entry::queryID=%d\n", queryID ); */ 1101 pthread_mutex_lock( & _completionMutex_ ); 1102 if( queryID == _queryID_ ) { 1103 /* Append contents to end of display queue */ 1104 node = listEMail; 1105 while( node ) { 1106 ItemEMail *email = node->data; 1107 1108 address = addritem_format_email( email ); 1109 /* g_print( "\temail/address ::%s::\n", address ); */ 1110 _displayQueue_ = g_list_append( _displayQueue_, address ); 1111 node = g_list_next( node ); 1112 } 1113 } 1114 g_list_free( listEMail ); 1115 pthread_mutex_unlock( & _completionMutex_ ); 1116 1117 return 0; 1118 } 1119 1120 /** 1121 * Clear the display queue. 1122 */ 1123 static void addrcompl_clear_queue( void ) { 1124 /* Clear out display queue */ 1125 pthread_mutex_lock( & _completionMutex_ ); 1126 1127 g_list_free_full( _displayQueue_, g_free ); 1128 _displayQueue_ = NULL; 1129 1130 pthread_mutex_unlock( & _completionMutex_ ); 1131 } 1132 1133 /** 1134 * Add a single address entry into the display queue. 1135 * \param address Address to append. 1136 */ 1137 static void addrcompl_add_queue( gchar *address ) { 1138 pthread_mutex_lock( & _completionMutex_ ); 1139 _displayQueue_ = g_list_append( _displayQueue_, address ); 1140 pthread_mutex_unlock( & _completionMutex_ ); 1141 } 1142 1143 /** 1144 * Load list with entries from local completion index. 1145 */ 1146 static void addrcompl_load_local( void ) { 1147 guint count = 0; 1148 1149 for (count = 0; count < get_completion_count(); count++) { 1150 gchar *address; 1151 1152 address = get_complete_address( count ); 1153 /* g_print( "\taddress ::%s::\n", address ); */ 1154 1155 /* Append contents to end of display queue */ 1156 addrcompl_add_queue( address ); 1157 } 1158 } 1159 1160 /** 1161 * Start the search. 1162 */ 1163 static void addrcompl_start_search( void ) { 1164 gchar *searchTerm; 1165 1166 searchTerm = g_strdup( _compWindow_->searchTerm ); 1167 1168 /* Setup the search */ 1169 _queryID_ = addrindex_setup_search( 1170 searchTerm, NULL, addrcompl_callback_entry ); 1171 g_free( searchTerm ); 1172 /* g_print( "addrcompl_start_search::queryID=%d\n", _queryID_ ); */ 1173 1174 /* Load local stuff */ 1175 addrcompl_load_local(); 1176 1177 /* Sit back and wait until something happens */ 1178 _completionIdleID_ = 1179 g_idle_add( (GSourceFunc) addrcompl_idle, NULL ); 1180 /* g_print( "addrindex_start_search::queryID=%d\n", _queryID_ ); */ 1181 1182 addrindex_start_search( _queryID_ ); 1183 } 1184 1185 /** 1186 * Apply the current selection in the list to the entry field. Focus is also 1187 * moved to the next widget so that Tab key works correctly. 1188 * \param list_view List to process. 1189 * \param entry Address entry field. 1190 * \param move_focus Move focus to the next widget ? 1191 */ 1192 static void completion_window_apply_selection(GtkTreeView *list_view, 1193 GtkEntry *entry, 1194 gboolean move_focus) 1195 { 1196 gchar *address = NULL, *text = NULL; 1197 gint cursor_pos; 1198 GtkWidget *parent; 1199 GtkTreeSelection *selection; 1200 GtkTreeModel *model; 1201 GtkTreeIter iter; 1202 gboolean is_group = FALSE; 1203 cm_return_if_fail(list_view != NULL); 1204 cm_return_if_fail(entry != NULL); 1205 GList *grp_emails = NULL; 1206 1207 selection = gtk_tree_view_get_selection(list_view); 1208 if (!gtk_tree_selection_get_selected(selection, &model, &iter)) 1209 return; 1210 1211 /* First remove the idler */ 1212 if( _completionIdleID_ != 0 ) { 1213 g_source_remove( _completionIdleID_ ); 1214 _completionIdleID_ = 0; 1215 } 1216 1217 /* Process selected item */ 1218 gtk_tree_model_get(model, &iter, ADDR_COMPL_ADDRESS, &text, 1219 ADDR_COMPL_ISGROUP, &is_group, 1220 ADDR_COMPL_GROUPLIST, &grp_emails, 1221 -1); 1222 1223 address = get_address_from_edit(entry, &cursor_pos); 1224 g_free(address); 1225 replace_address_in_edit(entry, text, cursor_pos, is_group, grp_emails); 1226 g_free(text); 1227 1228 /* Move focus to next widget */ 1229 parent = gtk_widget_get_parent(GTK_WIDGET(entry)); 1230 if( parent && move_focus) { 1231 gtk_widget_child_focus( parent, GTK_DIR_TAB_FORWARD ); 1232 } 1233 } 1234 1235 /** 1236 * Start address completion. Should be called when creating the main window 1237 * containing address completion entries. 1238 * \param mainwindow Main window. 1239 */ 1240 void address_completion_start(GtkWidget *mainwindow) 1241 { 1242 start_address_completion(NULL); 1243 set_match_any_part(TRUE); 1244 1245 /* register focus change hook */ 1246 g_signal_connect(G_OBJECT(mainwindow), "set_focus", 1247 G_CALLBACK(address_completion_mainwindow_set_focus), 1248 mainwindow); 1249 } 1250 1251 /** 1252 * Need unique data to make unregistering signal handler possible for the auto 1253 * completed entry. 1254 */ 1255 #define COMPLETION_UNIQUE_DATA (GINT_TO_POINTER(0xfeefaa)) 1256 1257 /** 1258 * Register specified entry widget for address completion. 1259 * \param entry Address entry field. 1260 */ 1261 void address_completion_register_entry(GtkEntry *entry, gboolean allow_commas) 1262 { 1263 cm_return_if_fail(entry != NULL); 1264 cm_return_if_fail(GTK_IS_ENTRY(entry)); 1265 1266 /* add hooked property */ 1267 g_object_set_data(G_OBJECT(entry), ENTRY_DATA_TAB_HOOK, entry); 1268 g_object_set_data(G_OBJECT(entry), ENTRY_DATA_ALLOW_COMMAS, GINT_TO_POINTER(allow_commas)); 1269 1270 /* add keypress event */ 1271 g_signal_connect_closure 1272 (G_OBJECT(entry), "key_press_event", 1273 g_cclosure_new(G_CALLBACK(address_completion_entry_key_pressed), 1274 COMPLETION_UNIQUE_DATA, 1275 NULL), 1276 FALSE); /* magic */ 1277 } 1278 1279 /** 1280 * Unregister specified entry widget from address completion operations. 1281 * \param entry Address entry field. 1282 */ 1283 void address_completion_unregister_entry(GtkEntry *entry) 1284 { 1285 GObject *entry_obj; 1286 1287 cm_return_if_fail(entry != NULL); 1288 cm_return_if_fail(GTK_IS_ENTRY(entry)); 1289 1290 entry_obj = g_object_get_data(G_OBJECT(entry), ENTRY_DATA_TAB_HOOK); 1291 cm_return_if_fail(entry_obj); 1292 cm_return_if_fail(G_OBJECT(entry_obj) == G_OBJECT(entry)); 1293 1294 /* has the hooked property? */ 1295 g_object_set_data(G_OBJECT(entry), ENTRY_DATA_TAB_HOOK, NULL); 1296 1297 /* remove the hook */ 1298 g_signal_handlers_disconnect_by_func(G_OBJECT(entry), 1299 G_CALLBACK(address_completion_entry_key_pressed), 1300 COMPLETION_UNIQUE_DATA); 1301 } 1302 1303 /** 1304 * End address completion. Should be called when main window with address 1305 * completion entries terminates. NOTE: this function assumes that it is 1306 * called upon destruction of the window. 1307 * \param mainwindow Main window. 1308 */ 1309 void address_completion_end(GtkWidget *mainwindow) 1310 { 1311 /* if address_completion_end() is really called on closing the window, 1312 * we don't need to unregister the set_focus_cb */ 1313 end_address_completion(); 1314 } 1315 1316 /* if focus changes to another entry, then clear completion cache */ 1317 static void address_completion_mainwindow_set_focus(GtkWindow *window, 1318 GtkWidget *widget, 1319 gpointer data) 1320 { 1321 1322 if (widget && GTK_IS_ENTRY(widget) && 1323 g_object_get_data(G_OBJECT(widget), ENTRY_DATA_TAB_HOOK)) { 1324 _allowCommas_ = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), ENTRY_DATA_ALLOW_COMMAS)); 1325 clear_completion_cache(); 1326 } 1327 } 1328 1329 /** 1330 * Listener that watches for tab or other keystroke in address entry field. 1331 * \param entry Address entry field. 1332 * \param ev Event object. 1333 * \param data User data. 1334 * \return <i>TRUE</i>. 1335 */ 1336 static gboolean address_completion_entry_key_pressed(GtkEntry *entry, 1337 GdkEventKey *ev, 1338 gpointer data) 1339 { 1340 if (ev->keyval == GDK_KEY_Tab) { 1341 addrcompl_clear_queue(); 1342 _allowCommas_ = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(entry), ENTRY_DATA_ALLOW_COMMAS)); 1343 if( address_completion_complete_address_in_entry( entry, TRUE ) ) { 1344 /* route a void character to the default handler */ 1345 /* this is a dirty hack; we're actually changing a key 1346 * reported by the system. */ 1347 ev->keyval = GDK_KEY_AudibleBell_Enable; 1348 ev->state &= ~GDK_SHIFT_MASK; 1349 1350 /* Create window */ 1351 address_completion_create_completion_window(entry); 1352 1353 /* Start remote queries */ 1354 addrcompl_start_search(); 1355 1356 return TRUE; 1357 } 1358 else { 1359 /* old behaviour */ 1360 } 1361 } else if (ev->keyval == GDK_KEY_Shift_L 1362 || ev->keyval == GDK_KEY_Shift_R 1363 || ev->keyval == GDK_KEY_Control_L 1364 || ev->keyval == GDK_KEY_Control_R 1365 || ev->keyval == GDK_KEY_Caps_Lock 1366 || ev->keyval == GDK_KEY_Shift_Lock 1367 || ev->keyval == GDK_KEY_Meta_L 1368 || ev->keyval == GDK_KEY_Meta_R 1369 || ev->keyval == GDK_KEY_Alt_L 1370 || ev->keyval == GDK_KEY_Alt_R) { 1371 /* these buttons should not clear the cache... */ 1372 } else 1373 clear_completion_cache(); 1374 1375 return FALSE; 1376 } 1377 /** 1378 * Initialize search term for address completion. 1379 * \param entry Address entry field. 1380 */ 1381 static gboolean address_completion_complete_address_in_entry(GtkEntry *entry, 1382 gboolean next) 1383 { 1384 gint ncount, cursor_pos; 1385 gchar *searchTerm, *new = NULL; 1386 1387 cm_return_val_if_fail(entry != NULL, FALSE); 1388 1389 if (!gtk_widget_has_focus(GTK_WIDGET(entry))) return FALSE; 1390 1391 /* get an address component from the cursor */ 1392 searchTerm = get_address_from_edit( entry, &cursor_pos ); 1393 if( ! searchTerm ) return FALSE; 1394 /* g_print( "search for :::%s:::\n", searchTerm ); */ 1395 1396 /* Clear any existing search */ 1397 g_free( _compWindow_->searchTerm ); 1398 _compWindow_->searchTerm = g_strdup( searchTerm ); 1399 1400 /* Perform search on local completion index */ 1401 ncount = complete_address( searchTerm ); 1402 if( 0 < ncount ) { 1403 new = get_next_complete_address(); 1404 g_free( new ); 1405 } 1406 /* Select the address if there is only one match */ 1407 if (ncount == 2) { 1408 /* Display selected address in entry field */ 1409 gchar *addr = get_complete_address(1); 1410 if (addr && !strstr(addr, " <!--___group___-->")) { 1411 replace_address_in_edit(entry, addr, cursor_pos, FALSE, NULL); 1412 /* Discard the window */ 1413 clear_completion_cache(); 1414 } 1415 g_free(addr); 1416 } 1417 /* Make sure that drop-down appears uniform! */ 1418 else if( ncount == 0 ) { 1419 addrcompl_add_queue( searchTerm ); 1420 } else { 1421 g_free( searchTerm ); 1422 } 1423 1424 return TRUE; 1425 } 1426 1427 /** 1428 * Create new address completion window for specified entry. 1429 * \param entry_ Entry widget to associate with window. 1430 */ 1431 static void address_completion_create_completion_window( GtkEntry *entry_ ) 1432 { 1433 gint x, y; 1434 GdkRectangle rect; 1435 GtkWidget *scroll, *list_view; 1436 GdkGrabStatus status; 1437 GdkDisplay *display; 1438 GtkRequisition r; 1439 GtkWidget *window; 1440 GtkWidget *entry = GTK_WIDGET(entry_); 1441 1442 /* Create new window and list */ 1443 window = gtk_window_new(GTK_WINDOW_POPUP); 1444 list_view = addr_compl_list_view_create(_compWindow_); 1445 1446 /* Destroy any existing window */ 1447 addrcompl_destroy_window( _compWindow_ ); 1448 1449 /* Create new object */ 1450 _compWindow_->window = window; 1451 _compWindow_->entry = entry; 1452 _compWindow_->list_view = list_view; 1453 _compWindow_->listCount = 0; 1454 _compWindow_->in_mouse = FALSE; 1455 1456 scroll = gtk_scrolled_window_new(NULL, NULL); 1457 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), 1458 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); 1459 gtk_container_add(GTK_CONTAINER(window), scroll); 1460 gtk_container_add(GTK_CONTAINER(scroll), list_view); 1461 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scroll), 1462 GTK_SHADOW_OUT); 1463 1464 /* Use entry widget to position initial window */ 1465 gtk_widget_get_allocation(entry, &rect); 1466 1467 /* rect.x and rect.y are relative to parent of our GtkEntry, 1468 * we need to convert them to absolute coordinates */ 1469 gdk_window_get_root_coords( 1470 gtk_widget_get_window(gtk_widget_get_parent(entry)), 1471 rect.x, rect.y, &x, &y); 1472 1473 /* Move the window to just below the GtkEntry */ 1474 gtk_window_move(GTK_WINDOW(window), x, y + rect.height); 1475 1476 /* Resize window to fit initial (empty) address list */ 1477 gtk_widget_get_preferred_size(list_view, &r, NULL); 1478 gtk_widget_set_size_request(window, rect.width, r.height); 1479 gtk_widget_show_all(window); 1480 1481 /* Setup handlers */ 1482 g_signal_connect(G_OBJECT(list_view), "button_press_event", 1483 G_CALLBACK(list_view_button_press), 1484 _compWindow_); 1485 1486 g_signal_connect(G_OBJECT(list_view), "button_release_event", 1487 G_CALLBACK(list_view_button_release), 1488 _compWindow_); 1489 1490 g_signal_connect(G_OBJECT(window), 1491 "button-press-event", 1492 G_CALLBACK(completion_window_button_press), 1493 _compWindow_ ); 1494 g_signal_connect(G_OBJECT(window), 1495 "key-press-event", 1496 G_CALLBACK(completion_window_key_press), 1497 _compWindow_ ); 1498 display = gdk_display_get_default(); 1499 status = gdk_seat_grab(gdk_display_get_default_seat(display), 1500 gtk_widget_get_window(window), 1501 GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | 1502 GDK_BUTTON_RELEASE_MASK, 1503 TRUE, NULL, NULL, NULL, NULL); 1504 if (status != GDK_GRAB_SUCCESS) 1505 g_warning("gdk_seat_grab failed with status %d", status); 1506 gtk_grab_add( window ); 1507 } 1508 1509 /** 1510 * Respond to button press in completion window. Check if mouse click is 1511 * anywhere outside the completion window. In that case the completion 1512 * window is destroyed, and the original searchTerm is restored. 1513 * 1514 * \param widget Window object. 1515 * \param event Event. 1516 * \param compWin Reference to completion window. 1517 */ 1518 static gboolean completion_window_button_press(GtkWidget *widget, 1519 GdkEventButton *event, 1520 CompletionWindow *compWin ) 1521 { 1522 GtkWidget *event_widget, *entry; 1523 gchar *searchTerm; 1524 gint cursor_pos; 1525 gboolean restore = TRUE; 1526 1527 cm_return_val_if_fail(compWin != NULL, FALSE); 1528 1529 entry = compWin->entry; 1530 cm_return_val_if_fail(entry != NULL, FALSE); 1531 1532 /* Test where mouse was clicked */ 1533 event_widget = gtk_get_event_widget((GdkEvent *)event); 1534 if (event_widget != widget) { 1535 while (event_widget) { 1536 if (event_widget == widget) 1537 return FALSE; 1538 else if (event_widget == entry) { 1539 restore = FALSE; 1540 break; 1541 } 1542 event_widget = gtk_widget_get_parent(event_widget); 1543 } 1544 } 1545 1546 if (restore) { 1547 /* Clicked outside of completion window - restore */ 1548 searchTerm = _compWindow_->searchTerm; 1549 g_free(get_address_from_edit(GTK_ENTRY(entry), &cursor_pos)); 1550 replace_address_in_edit(GTK_ENTRY(entry), searchTerm, cursor_pos, FALSE, NULL); 1551 } 1552 1553 clear_completion_cache(); 1554 addrcompl_destroy_window( _compWindow_ ); 1555 1556 return TRUE; 1557 } 1558 1559 /** 1560 * Respond to key press in completion window. 1561 * \param widget Window object. 1562 * \param event Event. 1563 * \param compWind Reference to completion window. 1564 */ 1565 static gboolean completion_window_key_press(GtkWidget *widget, 1566 GdkEventKey *event, 1567 CompletionWindow *compWin ) 1568 { 1569 GdkEventKey tmp_event; 1570 GtkWidget *entry; 1571 gchar *searchTerm; 1572 gint cursor_pos; 1573 GtkWidget *list_view; 1574 GtkWidget *parent; 1575 cm_return_val_if_fail(compWin != NULL, FALSE); 1576 1577 entry = compWin->entry; 1578 list_view = compWin->list_view; 1579 cm_return_val_if_fail(entry != NULL, FALSE); 1580 1581 /* allow keyboard navigation in the alternatives tree view */ 1582 if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down || 1583 event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down) { 1584 completion_window_advance_selection 1585 (GTK_TREE_VIEW(list_view), 1586 event->keyval == GDK_KEY_Down || 1587 event->keyval == GDK_KEY_Page_Down ? TRUE : FALSE); 1588 return TRUE; 1589 } 1590 1591 /* make tab move to next field */ 1592 if( event->keyval == GDK_KEY_Tab ) { 1593 /* Reference to parent */ 1594 parent = gtk_widget_get_parent(GTK_WIDGET(entry)); 1595 1596 /* Discard the window */ 1597 clear_completion_cache(); 1598 addrcompl_destroy_window( _compWindow_ ); 1599 1600 /* Move focus to next widget */ 1601 if( parent ) { 1602 gtk_widget_child_focus( parent, GTK_DIR_TAB_FORWARD ); 1603 } 1604 return FALSE; 1605 } 1606 1607 /* make backtab move to previous field */ 1608 if( event->keyval == GDK_KEY_ISO_Left_Tab ) { 1609 /* Reference to parent */ 1610 parent = gtk_widget_get_parent(GTK_WIDGET(entry)); 1611 1612 /* Discard the window */ 1613 clear_completion_cache(); 1614 addrcompl_destroy_window( _compWindow_ ); 1615 1616 /* Move focus to previous widget */ 1617 if( parent ) { 1618 gtk_widget_child_focus( parent, GTK_DIR_TAB_BACKWARD ); 1619 } 1620 return FALSE; 1621 } 1622 _allowCommas_ = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(entry), ENTRY_DATA_ALLOW_COMMAS)); 1623 1624 /* look for presses that accept the selection */ 1625 if (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_space || 1626 event->keyval == GDK_KEY_KP_Enter || 1627 (_allowCommas_ && event->keyval == GDK_KEY_comma)) { 1628 /* User selected address with a key press */ 1629 1630 /* Display selected address in entry field */ 1631 completion_window_apply_selection( 1632 GTK_TREE_VIEW(list_view), GTK_ENTRY(entry), 1633 event->keyval != GDK_KEY_comma); 1634 1635 if (event->keyval == GDK_KEY_comma) { 1636 gint pos = gtk_editable_get_position(GTK_EDITABLE(entry)); 1637 gtk_editable_insert_text(GTK_EDITABLE(entry), ", ", 2, &pos); 1638 gtk_editable_set_position(GTK_EDITABLE(entry), pos + 1); 1639 } 1640 1641 /* Discard the window */ 1642 clear_completion_cache(); 1643 addrcompl_destroy_window( _compWindow_ ); 1644 return FALSE; 1645 } 1646 1647 /* key state keys should never be handled */ 1648 if (event->keyval == GDK_KEY_Shift_L 1649 || event->keyval == GDK_KEY_Shift_R 1650 || event->keyval == GDK_KEY_Control_L 1651 || event->keyval == GDK_KEY_Control_R 1652 || event->keyval == GDK_KEY_Caps_Lock 1653 || event->keyval == GDK_KEY_Shift_Lock 1654 || event->keyval == GDK_KEY_Meta_L 1655 || event->keyval == GDK_KEY_Meta_R 1656 || event->keyval == GDK_KEY_Alt_L 1657 || event->keyval == GDK_KEY_Alt_R) { 1658 return FALSE; 1659 } 1660 1661 /* some other key, let's restore the searchTerm (orignal text) */ 1662 searchTerm = _compWindow_->searchTerm; 1663 g_free(get_address_from_edit(GTK_ENTRY(entry), &cursor_pos)); 1664 replace_address_in_edit(GTK_ENTRY(entry), searchTerm, cursor_pos, FALSE, NULL); 1665 1666 /* make sure anything we typed comes in the edit box */ 1667 tmp_event.type = event->type; 1668 tmp_event.window = gtk_widget_get_window(GTK_WIDGET(entry)); 1669 tmp_event.send_event = TRUE; 1670 tmp_event.time = event->time; 1671 tmp_event.state = event->state; 1672 tmp_event.keyval = event->keyval; 1673 tmp_event.length = event->length; 1674 tmp_event.string = event->string; 1675 gtk_widget_event(entry, (GdkEvent *)&tmp_event); 1676 1677 /* and close the completion window */ 1678 clear_completion_cache(); 1679 addrcompl_destroy_window( _compWindow_ ); 1680 1681 return TRUE; 1682 } 1683 1684 /* 1685 * ============================================================================ 1686 * Publically accessible functions. 1687 * ============================================================================ 1688 */ 1689 1690 /** 1691 * Setup completion object. 1692 */ 1693 void addrcompl_initialize( void ) { 1694 /* g_print( "addrcompl_initialize...\n" ); */ 1695 if( ! _compWindow_ ) { 1696 _compWindow_ = addrcompl_create_window(); 1697 } 1698 _queryID_ = 0; 1699 _completionIdleID_ = 0; 1700 /* g_print( "addrcompl_initialize...done\n" ); */ 1701 } 1702 1703 /** 1704 * Teardown completion object. 1705 */ 1706 void addrcompl_teardown( void ) { 1707 /* g_print( "addrcompl_teardown...\n" ); */ 1708 addrcompl_free_window( _compWindow_ ); 1709 _compWindow_ = NULL; 1710 1711 addrcompl_clear_queue(); 1712 1713 _completionIdleID_ = 0; 1714 /* g_print( "addrcompl_teardown...done\n" ); */ 1715 } 1716 1717 /* 1718 * tree view functions 1719 */ 1720 1721 static GtkListStore *addr_compl_create_store(void) 1722 { 1723 return gtk_list_store_new(N_ADDR_COMPL_COLUMNS, 1724 GDK_TYPE_PIXBUF, 1725 G_TYPE_STRING, 1726 G_TYPE_BOOLEAN, 1727 G_TYPE_POINTER, 1728 -1); 1729 } 1730 1731 static GtkWidget *addr_compl_list_view_create(CompletionWindow *window) 1732 { 1733 GtkTreeView *list_view; 1734 GtkTreeSelection *selector; 1735 GtkTreeModel *model; 1736 1737 model = GTK_TREE_MODEL(addr_compl_create_store()); 1738 list_view = GTK_TREE_VIEW(gtk_tree_view_new_with_model(model)); 1739 g_object_unref(model); 1740 1741 gtk_tree_view_set_headers_visible(list_view, FALSE); 1742 1743 selector = gtk_tree_view_get_selection(list_view); 1744 gtk_tree_selection_set_mode(selector, GTK_SELECTION_BROWSE); 1745 gtk_tree_selection_set_select_function(selector, addr_compl_selected, 1746 window, NULL); 1747 1748 /* create the columns */ 1749 addr_compl_create_list_view_columns(GTK_WIDGET(list_view)); 1750 1751 return GTK_WIDGET(list_view); 1752 } 1753 1754 static void addr_compl_create_list_view_columns(GtkWidget *list_view) 1755 { 1756 GtkTreeViewColumn *column; 1757 GtkCellRenderer *renderer; 1758 1759 renderer = gtk_cell_renderer_pixbuf_new(); 1760 column = gtk_tree_view_column_new_with_attributes 1761 ("", renderer, 1762 "pixbuf", ADDR_COMPL_ICON, NULL); 1763 gtk_tree_view_append_column(GTK_TREE_VIEW(list_view), column); 1764 renderer = gtk_cell_renderer_text_new(); 1765 column = gtk_tree_view_column_new_with_attributes 1766 ("", renderer, "text", ADDR_COMPL_ADDRESS, NULL); 1767 gtk_tree_view_append_column(GTK_TREE_VIEW(list_view), column); 1768 } 1769 1770 static gboolean list_view_button_press(GtkWidget *widget, GdkEventButton *event, 1771 CompletionWindow *window) 1772 { 1773 if (window && event && event->type == GDK_BUTTON_PRESS) { 1774 window->in_mouse = TRUE; 1775 } 1776 return FALSE; 1777 } 1778 1779 static gboolean list_view_button_release(GtkWidget *widget, GdkEventButton *event, 1780 CompletionWindow *window) 1781 { 1782 if (window && event && event->type == GDK_BUTTON_RELEASE) { 1783 window->in_mouse = FALSE; 1784 } 1785 return FALSE; 1786 } 1787 1788 static gboolean addr_compl_selected(GtkTreeSelection *selector, 1789 GtkTreeModel *model, 1790 GtkTreePath *path, 1791 gboolean currently_selected, 1792 gpointer data) 1793 { 1794 CompletionWindow *window = data; 1795 1796 if (currently_selected) 1797 return TRUE; 1798 1799 if (!window->in_mouse) 1800 return TRUE; 1801 1802 /* XXX: select the entry and kill window later... select is called before 1803 * any other mouse events handlers including the tree view internal one; 1804 * not using a time out would result in a crash. if this doesn't work 1805 * safely, maybe we should set variables when receiving button presses 1806 * in the tree view. */ 1807 if (!window->destroying) { 1808 window->destroying = TRUE; 1809 g_idle_add((GSourceFunc) addr_compl_defer_select_destruct, data); 1810 } 1811 1812 return TRUE; 1813 } 1814 1815 static gboolean addr_compl_defer_select_destruct(CompletionWindow *window) 1816 { 1817 GtkEntry *entry = GTK_ENTRY(window->entry); 1818 1819 completion_window_apply_selection(GTK_TREE_VIEW(window->list_view), 1820 entry, TRUE); 1821 1822 clear_completion_cache(); 1823 1824 addrcompl_destroy_window(window); 1825 return FALSE; 1826 } 1827 1828 gboolean found_in_addressbook(const gchar *address) 1829 { 1830 gchar *addr = NULL; 1831 gboolean found = FALSE; 1832 gint num_addr = 0; 1833 1834 if (!address) 1835 return FALSE; 1836 1837 addr = g_strdup(address); 1838 extract_address(addr); 1839 num_addr = complete_address(addr); 1840 if (num_addr > 1) { 1841 /* skip first item (this is the search string itself) */ 1842 int i = 1; 1843 for (; i < num_addr && !found; i++) { 1844 gchar *caddr = get_complete_address(i); 1845 extract_address(caddr); 1846 if (strcasecmp(caddr, addr) == 0) 1847 found = TRUE; 1848 g_free(caddr); 1849 } 1850 } 1851 g_free(addr); 1852 return found; 1853 }