2 * Copyright (C) 2010 Collabora Ltd.
3 * Copyright (C) 2007-2010 Nokia Corporation.
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.1 of the License, or (at your option) any later version.
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.
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 * Authors: Felix Kaser <felix.kaser@collabora.co.uk>
20 * Xavier Claessens <xavier.claessens@collabora.co.uk>
21 * Claudio Saavedra <csaavedra@igalia.com>
25 #include "tpaw-live-search.h"
27 #include "empathy-utils.h"
29 G_DEFINE_TYPE (TpawLiveSearch, tpaw_live_search, GTK_TYPE_HBOX)
31 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, TpawLiveSearch)
35 GtkWidget *search_entry;
36 GtkWidget *hook_widget;
38 GPtrArray *stripped_words;
55 static guint signals[LAST_SIGNAL];
57 static void live_search_hook_widget_destroy_cb (GtkWidget *object,
63 * Returns a stripped version of @ch, removing any case, accentuation
64 * mark, or any special mark on it.
67 stripped_char (gunichar ch)
72 utype = g_unichar_type (ch);
76 case G_UNICODE_CONTROL:
77 case G_UNICODE_FORMAT:
78 case G_UNICODE_UNASSIGNED:
79 case G_UNICODE_NON_SPACING_MARK:
80 case G_UNICODE_COMBINING_MARK:
81 case G_UNICODE_ENCLOSING_MARK:
84 case G_UNICODE_PRIVATE_USE:
85 case G_UNICODE_SURROGATE:
86 case G_UNICODE_LOWERCASE_LETTER:
87 case G_UNICODE_MODIFIER_LETTER:
88 case G_UNICODE_OTHER_LETTER:
89 case G_UNICODE_TITLECASE_LETTER:
90 case G_UNICODE_UPPERCASE_LETTER:
91 case G_UNICODE_DECIMAL_NUMBER:
92 case G_UNICODE_LETTER_NUMBER:
93 case G_UNICODE_OTHER_NUMBER:
94 case G_UNICODE_CONNECT_PUNCTUATION:
95 case G_UNICODE_DASH_PUNCTUATION:
96 case G_UNICODE_CLOSE_PUNCTUATION:
97 case G_UNICODE_FINAL_PUNCTUATION:
98 case G_UNICODE_INITIAL_PUNCTUATION:
99 case G_UNICODE_OTHER_PUNCTUATION:
100 case G_UNICODE_OPEN_PUNCTUATION:
101 case G_UNICODE_CURRENCY_SYMBOL:
102 case G_UNICODE_MODIFIER_SYMBOL:
103 case G_UNICODE_MATH_SYMBOL:
104 case G_UNICODE_OTHER_SYMBOL:
105 case G_UNICODE_LINE_SEPARATOR:
106 case G_UNICODE_PARAGRAPH_SEPARATOR:
107 case G_UNICODE_SPACE_SEPARATOR:
109 ch = g_unichar_tolower (ch);
110 g_unichar_fully_decompose (ch, FALSE, &retval, 1);
117 append_word (GPtrArray **word_array,
122 if (*word_array == NULL)
123 *word_array = g_ptr_array_new_with_free_func (g_free);
124 g_ptr_array_add (*word_array, g_string_free (*word, FALSE));
130 tpaw_live_search_strip_utf8_string (const gchar *string)
132 GPtrArray *word_array = NULL;
133 GString *word = NULL;
136 if (EMP_STR_EMPTY (string))
139 for (p = string; *p != '\0'; p = g_utf8_next_char (p))
143 /* Make the char lower-case, remove its accentuation marks, and ignore it
144 * if it is just unicode marks */
145 sc = stripped_char (g_utf8_get_char (p));
149 /* If it is not alpha-num, it is separator between words */
150 if (!g_unichar_isalnum (sc))
152 append_word (&word_array, &word);
156 /* It is alpha-num, append this char to current word, or start new word */
158 word = g_string_new (NULL);
159 g_string_append_unichar (word, sc);
162 append_word (&word_array, &word);
168 live_search_match_prefix (const gchar *string,
172 const gchar *prefix_p;
173 gboolean next_word = FALSE;
175 if (prefix == NULL || prefix[0] == 0)
178 if (EMP_STR_EMPTY (string))
182 for (p = string; *p != '\0'; p = g_utf8_next_char (p))
186 /* Make the char lower-case, remove its accentuation marks, and ignore it
187 * if it is just unicode marks */
188 sc = stripped_char (g_utf8_get_char (p));
192 /* If we want to go to next word, ignore alpha-num chars */
193 if (next_word && g_unichar_isalnum (sc))
197 /* Ignore word separators */
198 if (!g_unichar_isalnum (sc))
201 /* If this char does not match prefix_p, go to next word and start again
202 * from the beginning of prefix */
203 if (sc != g_utf8_get_char (prefix_p))
210 /* prefix_p match, verify to next char. If this was the last of prefix,
211 * it means it completely machted and we are done. */
212 prefix_p = g_utf8_next_char (prefix_p);
213 if (*prefix_p == '\0')
221 tpaw_live_search_match_words (const gchar *string,
229 for (i = 0; i < words->len; i++)
230 if (!live_search_match_prefix (string, g_ptr_array_index (words, i)))
237 fire_key_navigation_sig (TpawLiveSearch *self,
242 g_signal_emit (self, signals[KEYNAV], 0, event, &ret);
247 live_search_entry_key_pressed_cb (GtkEntry *entry,
251 TpawLiveSearch *self = TPAW_LIVE_SEARCH (user_data);
253 /* if esc key pressed, hide the search */
254 if (event->keyval == GDK_KEY_Escape)
256 gtk_widget_hide (GTK_WIDGET (self));
260 /* emit key navigation signal, so other widgets can respond to it properly */
261 if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down
262 || event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down
263 || event->keyval == GDK_KEY_Menu)
265 return fire_key_navigation_sig (self, event);
268 if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End ||
269 event->keyval == GDK_KEY_space)
271 /* If the live search is visible, the entry should catch the Home/End
272 * and space events */
273 if (!gtk_widget_get_visible (GTK_WIDGET (self)))
275 return fire_key_navigation_sig (self, event);
283 live_search_text_changed (GtkEntry *entry,
286 TpawLiveSearch *self = TPAW_LIVE_SEARCH (user_data);
287 TpawLiveSearchPriv *priv = GET_PRIV (self);
290 text = gtk_entry_get_text (entry);
292 if (EMP_STR_EMPTY (text))
293 gtk_widget_hide (GTK_WIDGET (self));
295 gtk_widget_show (GTK_WIDGET (self));
297 if (priv->stripped_words != NULL)
298 g_ptr_array_unref (priv->stripped_words);
300 priv->stripped_words = tpaw_live_search_strip_utf8_string (text);
302 g_object_notify (G_OBJECT (self), "text");
306 live_search_close_pressed (GtkEntry *entry,
307 GtkEntryIconPosition icon_pos,
311 TpawLiveSearch *self = TPAW_LIVE_SEARCH (user_data);
313 gtk_widget_hide (GTK_WIDGET (self));
317 live_search_key_press_event_cb (GtkWidget *widget,
321 TpawLiveSearch *self = TPAW_LIVE_SEARCH (user_data);
322 TpawLiveSearchPriv *priv = GET_PRIV (self);
326 /* dont forward this event to the entry, else the event is consumed by the
327 * entry and does not close the window */
328 if (!gtk_widget_get_visible (GTK_WIDGET (self)) &&
329 event->keyval == GDK_KEY_Escape)
332 /* do not show the search if CTRL and/or ALT are pressed with a key
333 * this is needed, because otherwise the CTRL + F accel would not work,
334 * because the entry consumes it */
335 if (event->state & (GDK_MOD1_MASK | GDK_CONTROL_MASK) ||
336 event->keyval == GDK_KEY_Control_L ||
337 event->keyval == GDK_KEY_Control_R)
340 /* dont forward the up/down and Page Up/Down arrow keys to the entry,
341 * they are needed for navigation in the treeview and are not needed in
342 * the search entry */
343 if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down ||
344 event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down ||
345 event->keyval == GDK_KEY_Menu)
348 if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End ||
349 event->keyval == GDK_KEY_space)
351 /* Home/End and space keys have to be forwarded to the entry only if
352 * the live search is visible (to move the cursor inside the entry). */
353 if (!gtk_widget_get_visible (GTK_WIDGET (self)))
357 /* Don't forward shift keys events as focusing the search entry would
358 * cancel an in-progress editing on a cell renderer (like when renaming a
359 * group). There is no point focusing it anyway as we don't display the
360 * search entry when only a shift key is pressed. */
361 if (event->keyval == GDK_KEY_Shift_L ||
362 event->keyval == GDK_KEY_Shift_R)
365 /* realize the widget if it is not realized yet */
366 gtk_widget_realize (priv->search_entry);
367 if (!gtk_widget_has_focus (priv->search_entry))
369 gtk_widget_grab_focus (priv->search_entry);
370 gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
373 /* forward the event to the search entry */
374 new_event = gdk_event_copy ((GdkEvent *) event);
375 ret = gtk_widget_event (priv->search_entry, new_event);
376 gdk_event_free (new_event);
382 live_search_entry_activate_cb (GtkEntry *entry,
383 TpawLiveSearch *self)
385 g_signal_emit (self, signals[ACTIVATE], 0);
389 live_search_release_hook_widget (TpawLiveSearch *self)
391 TpawLiveSearchPriv *priv = GET_PRIV (self);
393 /* remove old handlers if old source was not null */
394 if (priv->hook_widget != NULL)
396 g_signal_handlers_disconnect_by_func (priv->hook_widget,
397 live_search_key_press_event_cb, self);
398 g_signal_handlers_disconnect_by_func (priv->hook_widget,
399 live_search_hook_widget_destroy_cb, self);
400 g_object_unref (priv->hook_widget);
401 priv->hook_widget = NULL;
406 live_search_hook_widget_destroy_cb (GtkWidget *object,
409 TpawLiveSearch *self = TPAW_LIVE_SEARCH (user_data);
411 /* unref the hook widget and hide search */
412 gtk_widget_hide (GTK_WIDGET (self));
413 live_search_release_hook_widget (self);
417 live_search_dispose (GObject *obj)
419 TpawLiveSearch *self = TPAW_LIVE_SEARCH (obj);
421 live_search_release_hook_widget (self);
423 if (G_OBJECT_CLASS (tpaw_live_search_parent_class)->dispose != NULL)
424 G_OBJECT_CLASS (tpaw_live_search_parent_class)->dispose (obj);
428 live_search_finalize (GObject *obj)
430 TpawLiveSearch *self = TPAW_LIVE_SEARCH (obj);
431 TpawLiveSearchPriv *priv = GET_PRIV (self);
433 if (priv->stripped_words != NULL)
434 g_ptr_array_unref (priv->stripped_words);
436 if (G_OBJECT_CLASS (tpaw_live_search_parent_class)->finalize != NULL)
437 G_OBJECT_CLASS (tpaw_live_search_parent_class)->finalize (obj);
441 live_search_get_property (GObject *object,
446 TpawLiveSearch *self = TPAW_LIVE_SEARCH (object);
450 case PROP_HOOK_WIDGET:
451 g_value_set_object (value, tpaw_live_search_get_hook_widget (self));
454 g_value_set_string (value, tpaw_live_search_get_text (self));
457 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
463 live_search_set_property (GObject *object,
468 TpawLiveSearch *self = TPAW_LIVE_SEARCH (object);
471 case PROP_HOOK_WIDGET:
472 tpaw_live_search_set_hook_widget (self, g_value_get_object (value));
475 tpaw_live_search_set_text (self, g_value_get_string (value));
478 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
484 live_search_unmap (GtkWidget *widget)
486 TpawLiveSearch *self = TPAW_LIVE_SEARCH (widget);
487 TpawLiveSearchPriv *priv = GET_PRIV (self);
489 GTK_WIDGET_CLASS (tpaw_live_search_parent_class)->unmap (widget);
491 /* unmap can happen if a parent gets hidden, in that case we want to hide
492 * the live search as well, so when it gets mapped again, the live search
494 gtk_widget_hide (widget);
496 gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
498 if (priv->hook_widget != NULL)
499 gtk_widget_grab_focus (priv->hook_widget);
503 live_search_show (GtkWidget *widget)
505 TpawLiveSearch *self = TPAW_LIVE_SEARCH (widget);
506 TpawLiveSearchPriv *priv = GET_PRIV (self);
508 if (!gtk_widget_has_focus (priv->search_entry))
509 gtk_widget_grab_focus (priv->search_entry);
511 GTK_WIDGET_CLASS (tpaw_live_search_parent_class)->show (widget);
515 live_search_grab_focus (GtkWidget *widget)
517 TpawLiveSearch *self = TPAW_LIVE_SEARCH (widget);
518 TpawLiveSearchPriv *priv = GET_PRIV (self);
520 if (!gtk_widget_has_focus (priv->search_entry))
522 gtk_widget_grab_focus (priv->search_entry);
523 gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
528 tpaw_live_search_class_init (TpawLiveSearchClass *klass)
530 GObjectClass *object_class = (GObjectClass *) klass;
531 GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
532 GParamSpec *param_spec;
534 object_class->finalize = live_search_finalize;
535 object_class->dispose = live_search_dispose;
536 object_class->get_property = live_search_get_property;
537 object_class->set_property = live_search_set_property;
539 widget_class->unmap = live_search_unmap;
540 widget_class->show = live_search_show;
541 widget_class->grab_focus = live_search_grab_focus;
543 signals[ACTIVATE] = g_signal_new ("activate",
544 G_TYPE_FROM_CLASS (object_class),
548 g_cclosure_marshal_generic,
551 signals[KEYNAV] = g_signal_new ("key-navigation",
552 G_TYPE_FROM_CLASS (object_class),
555 g_signal_accumulator_true_handled, NULL,
556 g_cclosure_marshal_generic,
557 G_TYPE_BOOLEAN, 1, GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);
559 param_spec = g_param_spec_object ("hook-widget", "Live Search Hook Widget",
560 "The live search catches key-press-events on this widget",
561 GTK_TYPE_WIDGET, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
562 g_object_class_install_property (object_class, PROP_HOOK_WIDGET,
565 param_spec = g_param_spec_string ("text", "Live Search Text",
566 "The text of the live search entry",
567 "", G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
568 g_object_class_install_property (object_class, PROP_TEXT, param_spec);
570 g_type_class_add_private (klass, sizeof (TpawLiveSearchPriv));
574 tpaw_live_search_init (TpawLiveSearch *self)
576 TpawLiveSearchPriv *priv =
577 G_TYPE_INSTANCE_GET_PRIVATE ((self), TPAW_TYPE_LIVE_SEARCH,
580 gtk_widget_set_no_show_all (GTK_WIDGET (self), TRUE);
582 priv->search_entry = gtk_entry_new ();
583 gtk_entry_set_icon_from_stock (GTK_ENTRY (priv->search_entry),
584 GTK_ENTRY_ICON_SECONDARY, GTK_STOCK_CLOSE);
585 gtk_entry_set_icon_activatable (GTK_ENTRY (priv->search_entry),
586 GTK_ENTRY_ICON_SECONDARY, TRUE);
587 gtk_entry_set_icon_sensitive (GTK_ENTRY (priv->search_entry),
588 GTK_ENTRY_ICON_SECONDARY, TRUE);
589 gtk_widget_show (priv->search_entry);
591 gtk_box_pack_start (GTK_BOX (self), priv->search_entry, TRUE, TRUE, 0);
593 g_signal_connect (priv->search_entry, "icon_release",
594 G_CALLBACK (live_search_close_pressed), self);
595 g_signal_connect (priv->search_entry, "changed",
596 G_CALLBACK (live_search_text_changed), self);
597 g_signal_connect (priv->search_entry, "key-press-event",
598 G_CALLBACK (live_search_entry_key_pressed_cb), self);
599 g_signal_connect (priv->search_entry, "activate",
600 G_CALLBACK (live_search_entry_activate_cb), self);
602 priv->hook_widget = NULL;
608 tpaw_live_search_new (GtkWidget *hook)
610 g_return_val_if_fail (hook == NULL || GTK_IS_WIDGET (hook), NULL);
612 return g_object_new (TPAW_TYPE_LIVE_SEARCH,
620 tpaw_live_search_get_hook_widget (TpawLiveSearch *self)
622 TpawLiveSearchPriv *priv = GET_PRIV (self);
624 g_return_val_if_fail (TPAW_IS_LIVE_SEARCH (self), NULL);
626 return priv->hook_widget;
630 tpaw_live_search_set_hook_widget (TpawLiveSearch *self,
633 TpawLiveSearchPriv *priv;
635 g_return_if_fail (TPAW_IS_LIVE_SEARCH (self));
636 g_return_if_fail (hook == NULL || GTK_IS_WIDGET (hook));
638 priv = GET_PRIV (self);
640 /* release the actual widget */
641 live_search_release_hook_widget (self);
643 /* connect handlers if new source is not null */
646 priv->hook_widget = g_object_ref (hook);
647 g_signal_connect (priv->hook_widget, "key-press-event",
648 G_CALLBACK (live_search_key_press_event_cb),
650 g_signal_connect (priv->hook_widget, "destroy",
651 G_CALLBACK (live_search_hook_widget_destroy_cb),
657 tpaw_live_search_get_text (TpawLiveSearch *self)
659 TpawLiveSearchPriv *priv = GET_PRIV (self);
661 g_return_val_if_fail (TPAW_IS_LIVE_SEARCH (self), NULL);
663 return gtk_entry_get_text (GTK_ENTRY (priv->search_entry));
667 tpaw_live_search_set_text (TpawLiveSearch *self,
670 TpawLiveSearchPriv *priv = GET_PRIV (self);
672 g_return_if_fail (TPAW_IS_LIVE_SEARCH (self));
673 g_return_if_fail (text != NULL);
675 gtk_entry_set_text (GTK_ENTRY (priv->search_entry), text);
679 * tpaw_live_search_match:
680 * @self: a #TpawLiveSearch
681 * @string: a string where to search, must be valid UTF-8.
683 * Search if one of the words in @string string starts with the current text
686 * Searching for "aba" in "Abasto" will match, searching in "Moraba" will not,
687 * and searching in "A tool (abacus)" will do.
689 * The match is not case-sensitive, and regardless of the accentuation marks.
691 * Returns: %TRUE if a match is found, %FALSE otherwise.
695 tpaw_live_search_match (TpawLiveSearch *self,
698 TpawLiveSearchPriv *priv;
700 g_return_val_if_fail (TPAW_IS_LIVE_SEARCH (self), FALSE);
702 priv = GET_PRIV (self);
704 return tpaw_live_search_match_words (string, priv->stripped_words);
708 tpaw_live_search_match_string (const gchar *string,
714 words = tpaw_live_search_strip_utf8_string (prefix);
715 match = tpaw_live_search_match_words (string, words);
717 g_ptr_array_unref (words);
723 tpaw_live_search_get_words (TpawLiveSearch *self)
725 TpawLiveSearchPriv *priv = GET_PRIV (self);
727 return priv->stripped_words;