]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-live-search.c
Merge remote-tracking branch 'origin/gnome-3-8'
[empathy.git] / libempathy-gtk / empathy-live-search.c
1 /*
2  * Copyright (C) 2010 Collabora Ltd.
3  * Copyright (C) 2007-2010 Nokia Corporation.
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.1 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 Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18  *
19  * Authors: Felix Kaser <felix.kaser@collabora.co.uk>
20  *          Xavier Claessens <xavier.claessens@collabora.co.uk>
21  *          Claudio Saavedra <csaavedra@igalia.com>
22  */
23
24 #include "config.h"
25 #include "empathy-live-search.h"
26
27 #include "empathy-utils.h"
28
29 G_DEFINE_TYPE (EmpathyLiveSearch, empathy_live_search, GTK_TYPE_HBOX)
30
31 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyLiveSearch)
32
33 typedef struct
34 {
35   GtkWidget *search_entry;
36   GtkWidget *hook_widget;
37
38   GPtrArray *stripped_words;
39 } EmpathyLiveSearchPriv;
40
41 enum
42 {
43   PROP_0,
44   PROP_HOOK_WIDGET,
45   PROP_TEXT
46 };
47
48 enum
49 {
50   ACTIVATE,
51   KEYNAV,
52   LAST_SIGNAL
53 };
54
55 static guint signals[LAST_SIGNAL];
56
57 static void live_search_hook_widget_destroy_cb (GtkWidget *object,
58     gpointer user_data);
59
60 /**
61  * stripped_char:
62  *
63  * Returns a stripped version of @ch, removing any case, accentuation
64  * mark, or any special mark on it.
65  **/
66 static gunichar
67 stripped_char (gunichar ch)
68 {
69   gunichar retval = 0;
70   GUnicodeType utype;
71
72   utype = g_unichar_type (ch);
73
74   switch (utype)
75     {
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:
82       /* Ignore those */
83       break;
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:
108     default:
109       ch = g_unichar_tolower (ch);
110       g_unichar_fully_decompose (ch, FALSE, &retval, 1);
111     }
112
113   return retval;
114 }
115
116 static void
117 append_word (GPtrArray **word_array,
118     GString **word)
119 {
120   if (*word != NULL)
121     {
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));
125       *word = NULL;
126     }
127 }
128
129 GPtrArray *
130 empathy_live_search_strip_utf8_string (const gchar *string)
131 {
132   GPtrArray *word_array = NULL;
133   GString *word = NULL;
134   const gchar *p;
135
136   if (EMP_STR_EMPTY (string))
137     return NULL;
138
139   for (p = string; *p != '\0'; p = g_utf8_next_char (p))
140     {
141       gunichar sc;
142
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));
146       if (sc == 0)
147         continue;
148
149       /* If it is not alpha-num, it is separator between words */
150       if (!g_unichar_isalnum (sc))
151         {
152           append_word (&word_array, &word);
153           continue;
154         }
155
156       /* It is alpha-num, append this char to current word, or start new word */
157       if (word == NULL)
158         word = g_string_new (NULL);
159       g_string_append_unichar (word, sc);
160     }
161
162   append_word (&word_array, &word);
163
164   return word_array;
165 }
166
167 static gboolean
168 live_search_match_prefix (const gchar *string,
169     const gchar *prefix)
170 {
171   const gchar *p;
172   const gchar *prefix_p;
173   gboolean next_word = FALSE;
174
175   if (prefix == NULL || prefix[0] == 0)
176     return TRUE;
177
178   if (EMP_STR_EMPTY (string))
179     return FALSE;
180
181   prefix_p = prefix;
182   for (p = string; *p != '\0'; p = g_utf8_next_char (p))
183     {
184       gunichar sc;
185
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));
189       if (sc == 0)
190         continue;
191
192       /* If we want to go to next word, ignore alpha-num chars */
193       if (next_word && g_unichar_isalnum (sc))
194         continue;
195       next_word = FALSE;
196
197       /* Ignore word separators */
198       if (!g_unichar_isalnum (sc))
199         continue;
200
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))
204         {
205           next_word = TRUE;
206           prefix_p = prefix;
207           continue;
208         }
209
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')
214         return TRUE;
215     }
216
217   return FALSE;
218 }
219
220 gboolean
221 empathy_live_search_match_words (const gchar *string,
222     GPtrArray *words)
223 {
224   guint i;
225
226   if (words == NULL)
227     return TRUE;
228
229   for (i = 0; i < words->len; i++)
230     if (!live_search_match_prefix (string, g_ptr_array_index (words, i)))
231       return FALSE;
232
233   return TRUE;
234 }
235
236 static gboolean
237 fire_key_navigation_sig (EmpathyLiveSearch *self,
238     GdkEventKey *event)
239 {
240   gboolean ret;
241
242   g_signal_emit (self, signals[KEYNAV], 0, event, &ret);
243   return ret;
244 }
245
246 static gboolean
247 live_search_entry_key_pressed_cb (GtkEntry *entry,
248     GdkEventKey *event,
249     gpointer user_data)
250 {
251   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
252
253   /* if esc key pressed, hide the search */
254   if (event->keyval == GDK_KEY_Escape)
255     {
256       gtk_widget_hide (GTK_WIDGET (self));
257       return TRUE;
258     }
259
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)
264      {
265        return fire_key_navigation_sig (self, event);
266      }
267
268   if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End ||
269       event->keyval == GDK_KEY_space)
270     {
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)))
274         {
275           return fire_key_navigation_sig (self, event);
276         }
277     }
278
279   return FALSE;
280 }
281
282 static void
283 live_search_text_changed (GtkEntry *entry,
284     gpointer user_data)
285 {
286   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
287   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
288   const gchar *text;
289
290   text = gtk_entry_get_text (entry);
291
292   if (EMP_STR_EMPTY (text))
293     gtk_widget_hide (GTK_WIDGET (self));
294   else
295     gtk_widget_show (GTK_WIDGET (self));
296
297   if (priv->stripped_words != NULL)
298     g_ptr_array_unref (priv->stripped_words);
299
300   priv->stripped_words = empathy_live_search_strip_utf8_string (text);
301
302   g_object_notify (G_OBJECT (self), "text");
303 }
304
305 static void
306 live_search_close_pressed (GtkEntry *entry,
307     GtkEntryIconPosition icon_pos,
308     GdkEvent *event,
309     gpointer user_data)
310 {
311   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
312
313   gtk_widget_hide (GTK_WIDGET (self));
314 }
315
316 static gboolean
317 live_search_key_press_event_cb (GtkWidget *widget,
318     GdkEventKey *event,
319     gpointer user_data)
320 {
321   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
322   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
323   GdkEvent *new_event;
324   gboolean ret;
325
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)
330     return FALSE;
331
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)
338     return FALSE;
339
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)
346      return FALSE;
347
348    if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End ||
349        event->keyval == GDK_KEY_space)
350      {
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)))
354          return FALSE;
355      }
356
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)
363        return FALSE;
364
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))
368     {
369       gtk_widget_grab_focus (priv->search_entry);
370       gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
371     }
372
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);
377
378   return ret;
379 }
380
381 static void
382 live_search_entry_activate_cb (GtkEntry *entry,
383     EmpathyLiveSearch *self)
384 {
385   g_signal_emit (self, signals[ACTIVATE], 0);
386 }
387
388 static void
389 live_search_release_hook_widget (EmpathyLiveSearch *self)
390 {
391   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
392
393   /* remove old handlers if old source was not null */
394   if (priv->hook_widget != NULL)
395     {
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;
402     }
403 }
404
405 static void
406 live_search_hook_widget_destroy_cb (GtkWidget *object,
407     gpointer user_data)
408 {
409   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
410
411   /* unref the hook widget and hide search */
412   gtk_widget_hide (GTK_WIDGET (self));
413   live_search_release_hook_widget (self);
414 }
415
416 static void
417 live_search_dispose (GObject *obj)
418 {
419   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);
420
421   live_search_release_hook_widget (self);
422
423   if (G_OBJECT_CLASS (empathy_live_search_parent_class)->dispose != NULL)
424     G_OBJECT_CLASS (empathy_live_search_parent_class)->dispose (obj);
425 }
426
427 static void
428 live_search_finalize (GObject *obj)
429 {
430   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);
431   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
432
433   if (priv->stripped_words != NULL)
434     g_ptr_array_unref (priv->stripped_words);
435
436   if (G_OBJECT_CLASS (empathy_live_search_parent_class)->finalize != NULL)
437     G_OBJECT_CLASS (empathy_live_search_parent_class)->finalize (obj);
438 }
439
440 static void
441 live_search_get_property (GObject *object,
442     guint param_id,
443     GValue *value,
444     GParamSpec *pspec)
445 {
446   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);
447
448   switch (param_id)
449     {
450     case PROP_HOOK_WIDGET:
451       g_value_set_object (value, empathy_live_search_get_hook_widget (self));
452       break;
453     case PROP_TEXT:
454       g_value_set_string (value, empathy_live_search_get_text (self));
455       break;
456     default:
457       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
458       break;
459     }
460 }
461
462 static void
463 live_search_set_property (GObject *object,
464     guint param_id,
465     const GValue *value,
466     GParamSpec *pspec)
467 {
468   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);
469
470   switch (param_id) {
471   case PROP_HOOK_WIDGET:
472     empathy_live_search_set_hook_widget (self, g_value_get_object (value));
473     break;
474   case PROP_TEXT:
475     empathy_live_search_set_text (self, g_value_get_string (value));
476     break;
477   default:
478     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
479     break;
480   };
481 }
482
483 static void
484 live_search_unmap (GtkWidget *widget)
485 {
486   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
487   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
488
489   GTK_WIDGET_CLASS (empathy_live_search_parent_class)->unmap (widget);
490
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
493    * won't be shown. */
494   gtk_widget_hide (widget);
495
496   gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
497
498   if (priv->hook_widget != NULL)
499     gtk_widget_grab_focus (priv->hook_widget);
500 }
501
502 static void
503 live_search_show (GtkWidget *widget)
504 {
505   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
506   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
507
508   if (!gtk_widget_has_focus (priv->search_entry))
509     gtk_widget_grab_focus (priv->search_entry);
510
511   GTK_WIDGET_CLASS (empathy_live_search_parent_class)->show (widget);
512 }
513
514 static void
515 live_search_grab_focus (GtkWidget *widget)
516 {
517   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
518   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
519
520   if (!gtk_widget_has_focus (priv->search_entry))
521     {
522       gtk_widget_grab_focus (priv->search_entry);
523       gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
524     }
525 }
526
527 static void
528 empathy_live_search_class_init (EmpathyLiveSearchClass *klass)
529 {
530   GObjectClass *object_class = (GObjectClass *) klass;
531   GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
532   GParamSpec *param_spec;
533
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;
538
539   widget_class->unmap = live_search_unmap;
540   widget_class->show = live_search_show;
541   widget_class->grab_focus = live_search_grab_focus;
542
543   signals[ACTIVATE] = g_signal_new ("activate",
544       G_TYPE_FROM_CLASS (object_class),
545       G_SIGNAL_RUN_LAST,
546       0,
547       NULL, NULL,
548       g_cclosure_marshal_generic,
549       G_TYPE_NONE, 0);
550
551   signals[KEYNAV] = g_signal_new ("key-navigation",
552       G_TYPE_FROM_CLASS (object_class),
553       G_SIGNAL_RUN_LAST,
554       0,
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);
558
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,
563       param_spec);
564
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);
569
570   g_type_class_add_private (klass, sizeof (EmpathyLiveSearchPriv));
571 }
572
573 static void
574 empathy_live_search_init (EmpathyLiveSearch *self)
575 {
576   EmpathyLiveSearchPriv *priv =
577     G_TYPE_INSTANCE_GET_PRIVATE ((self), EMPATHY_TYPE_LIVE_SEARCH,
578         EmpathyLiveSearchPriv);
579
580   gtk_widget_set_no_show_all (GTK_WIDGET (self), TRUE);
581
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);
590
591   gtk_box_pack_start (GTK_BOX (self), priv->search_entry, TRUE, TRUE, 0);
592
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);
601
602   priv->hook_widget = NULL;
603
604   self->priv = priv;
605 }
606
607 GtkWidget *
608 empathy_live_search_new (GtkWidget *hook)
609 {
610   g_return_val_if_fail (hook == NULL || GTK_IS_WIDGET (hook), NULL);
611
612   return g_object_new (EMPATHY_TYPE_LIVE_SEARCH,
613       "hook-widget", hook,
614       NULL);
615 }
616
617 /* public methods */
618
619 GtkWidget *
620 empathy_live_search_get_hook_widget (EmpathyLiveSearch *self)
621 {
622   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
623
624   g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), NULL);
625
626   return priv->hook_widget;
627 }
628
629 void
630 empathy_live_search_set_hook_widget (EmpathyLiveSearch *self,
631     GtkWidget *hook)
632 {
633   EmpathyLiveSearchPriv *priv;
634
635   g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self));
636   g_return_if_fail (hook == NULL || GTK_IS_WIDGET (hook));
637
638   priv = GET_PRIV (self);
639
640   /* release the actual widget */
641   live_search_release_hook_widget (self);
642
643   /* connect handlers if new source is not null */
644   if (hook != NULL)
645     {
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),
649           self);
650       g_signal_connect (priv->hook_widget, "destroy",
651           G_CALLBACK (live_search_hook_widget_destroy_cb),
652           self);
653     }
654 }
655
656 const gchar *
657 empathy_live_search_get_text (EmpathyLiveSearch *self)
658 {
659   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
660
661   g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), NULL);
662
663   return gtk_entry_get_text (GTK_ENTRY (priv->search_entry));
664 }
665
666 void
667 empathy_live_search_set_text (EmpathyLiveSearch *self,
668     const gchar *text)
669 {
670   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
671
672   g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self));
673   g_return_if_fail (text != NULL);
674
675   gtk_entry_set_text (GTK_ENTRY (priv->search_entry), text);
676 }
677
678 /**
679  * empathy_live_search_match:
680  * @self: a #EmpathyLiveSearch
681  * @string: a string where to search, must be valid UTF-8.
682  *
683  * Search if one of the words in @string string starts with the current text
684  * of @self.
685  *
686  * Searching for "aba" in "Abasto" will match, searching in "Moraba" will not,
687  * and searching in "A tool (abacus)" will do.
688  *
689  * The match is not case-sensitive, and regardless of the accentuation marks.
690  *
691  * Returns: %TRUE if a match is found, %FALSE otherwise.
692  *
693  **/
694 gboolean
695 empathy_live_search_match (EmpathyLiveSearch *self,
696     const gchar *string)
697 {
698   EmpathyLiveSearchPriv *priv;
699
700   g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), FALSE);
701
702   priv = GET_PRIV (self);
703
704   return empathy_live_search_match_words (string, priv->stripped_words);
705 }
706
707 gboolean
708 empathy_live_search_match_string (const gchar *string,
709     const gchar *prefix)
710 {
711   GPtrArray *words;
712   gboolean match;
713
714   words = empathy_live_search_strip_utf8_string (prefix);
715   match = empathy_live_search_match_words (string, words);
716   if (words != NULL)
717     g_ptr_array_unref (words);
718
719   return match;
720 }
721
722 GPtrArray *
723 empathy_live_search_get_words (EmpathyLiveSearch *self)
724 {
725   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
726
727   return priv->stripped_words;
728 }