]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-live-search.c
a98811d0b9b3e954dbc13fded4031076c61d2b8f
[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 <string.h>
26
27 #include <gtk/gtk.h>
28 #include <gdk/gdkkeysyms.h>
29
30 #include <libempathy/empathy-utils.h>
31
32 #include "empathy-live-search.h"
33 #include "empathy-gtk-marshal.h"
34
35 G_DEFINE_TYPE (EmpathyLiveSearch, empathy_live_search, GTK_TYPE_HBOX)
36
37 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyLiveSearch)
38
39 typedef struct
40 {
41   GtkWidget *search_entry;
42   GtkWidget *hook_widget;
43
44   GPtrArray *stripped_words;
45 } EmpathyLiveSearchPriv;
46
47 enum
48 {
49   PROP_0,
50   PROP_HOOK_WIDGET,
51   PROP_TEXT
52 };
53
54 enum
55 {
56   ACTIVATE,
57   KEYNAV,
58   LAST_SIGNAL
59 };
60
61 static guint signals[LAST_SIGNAL];
62
63 static void live_search_hook_widget_destroy_cb (GtkWidget *object,
64     gpointer user_data);
65
66 /**
67  * stripped_char:
68  *
69  * Returns a stripped version of @ch, removing any case, accentuation
70  * mark, or any special mark on it.
71  **/
72 static gunichar
73 stripped_char (gunichar ch)
74 {
75   gunichar retval = 0;
76   GUnicodeType utype;
77   gunichar *decomp;
78   gsize dlen;
79
80   utype = g_unichar_type (ch);
81
82   switch (utype)
83     {
84     case G_UNICODE_CONTROL:
85     case G_UNICODE_FORMAT:
86     case G_UNICODE_UNASSIGNED:
87     case G_UNICODE_NON_SPACING_MARK:
88     case G_UNICODE_COMBINING_MARK:
89     case G_UNICODE_ENCLOSING_MARK:
90       /* Ignore those */
91       break;
92     case G_UNICODE_PRIVATE_USE:
93     case G_UNICODE_SURROGATE:
94     case G_UNICODE_LOWERCASE_LETTER:
95     case G_UNICODE_MODIFIER_LETTER:
96     case G_UNICODE_OTHER_LETTER:
97     case G_UNICODE_TITLECASE_LETTER:
98     case G_UNICODE_UPPERCASE_LETTER:
99     case G_UNICODE_DECIMAL_NUMBER:
100     case G_UNICODE_LETTER_NUMBER:
101     case G_UNICODE_OTHER_NUMBER:
102     case G_UNICODE_CONNECT_PUNCTUATION:
103     case G_UNICODE_DASH_PUNCTUATION:
104     case G_UNICODE_CLOSE_PUNCTUATION:
105     case G_UNICODE_FINAL_PUNCTUATION:
106     case G_UNICODE_INITIAL_PUNCTUATION:
107     case G_UNICODE_OTHER_PUNCTUATION:
108     case G_UNICODE_OPEN_PUNCTUATION:
109     case G_UNICODE_CURRENCY_SYMBOL:
110     case G_UNICODE_MODIFIER_SYMBOL:
111     case G_UNICODE_MATH_SYMBOL:
112     case G_UNICODE_OTHER_SYMBOL:
113     case G_UNICODE_LINE_SEPARATOR:
114     case G_UNICODE_PARAGRAPH_SEPARATOR:
115     case G_UNICODE_SPACE_SEPARATOR:
116     default:
117       ch = g_unichar_tolower (ch);
118       decomp = g_unicode_canonical_decomposition (ch, &dlen);
119       if (decomp != NULL)
120         {
121           retval = decomp[0];
122           g_free (decomp);
123         }
124     }
125
126   return retval;
127 }
128
129 static void
130 append_word (GPtrArray **word_array,
131     GString **word)
132 {
133   if (*word != NULL)
134     {
135       if (*word_array == NULL)
136         *word_array = g_ptr_array_new_with_free_func (g_free);
137       g_ptr_array_add (*word_array, g_string_free (*word, FALSE));
138       *word = NULL;
139     }
140 }
141
142 static GPtrArray *
143 strip_utf8_string (const gchar *string)
144 {
145   GPtrArray *word_array = NULL;
146   GString *word = NULL;
147   const gchar *p;
148
149   if (EMP_STR_EMPTY (string))
150     return NULL;
151
152   for (p = string; *p != '\0'; p = g_utf8_next_char (p))
153     {
154       gunichar sc;
155
156       /* Make the char lower-case, remove its accentuation marks, and ignore it
157        * if it is just unicode marks */
158       sc = stripped_char (g_utf8_get_char (p));
159       if (sc == 0)
160         continue;
161
162       /* If it is not alpha-num, it is separator between words */
163       if (!g_unichar_isalnum (sc))
164         {
165           append_word (&word_array, &word);
166           continue;
167         }
168
169       /* It is alpha-num, append this char to current word, or start new word */
170       if (word == NULL)
171         word = g_string_new (NULL);
172       g_string_append_unichar (word, sc);
173     }
174
175   append_word (&word_array, &word);
176
177   return word_array;
178 }
179
180 static gboolean
181 live_search_match_prefix (const gchar *string,
182     const gchar *prefix)
183 {
184   const gchar *p;
185   const gchar *prefix_p;
186   gboolean next_word = FALSE;
187
188   if (prefix == NULL || prefix[0] == 0)
189     return TRUE;
190
191   if (EMP_STR_EMPTY (string))
192     return FALSE;
193
194   prefix_p = prefix;
195   for (p = string; *p != '\0'; p = g_utf8_next_char (p))
196     {
197       gunichar sc;
198
199       /* Make the char lower-case, remove its accentuation marks, and ignore it
200        * if it is just unicode marks */
201       sc = stripped_char (g_utf8_get_char (p));
202       if (sc == 0)
203         continue;
204
205       /* If we want to go to next word, ignore alpha-num chars */
206       if (next_word && g_unichar_isalnum (sc))
207         continue;
208       next_word = FALSE;
209
210       /* Ignore word separators */
211       if (!g_unichar_isalnum (sc))
212         continue;
213
214       /* If this char does not match prefix_p, go to next word and start again
215        * from the beginning of prefix */
216       if (sc != g_utf8_get_char (prefix_p))
217         {
218           next_word = TRUE;
219           prefix_p = prefix;
220           continue;
221         }
222
223       /* prefix_p match, verify to next char. If this was the last of prefix,
224        * it means it completely machted and we are done. */
225       prefix_p = g_utf8_next_char (prefix_p);
226       if (*prefix_p == '\0')
227         return TRUE;
228     }
229
230   return FALSE;
231 }
232
233 static gboolean
234 live_search_match_words (const gchar *string,
235     GPtrArray *words)
236 {
237   guint i;
238
239   if (words == NULL)
240     return TRUE;
241
242   for (i = 0; i < words->len; i++)
243     if (!live_search_match_prefix (string, g_ptr_array_index (words, i)))
244       return FALSE;
245
246   return TRUE;
247 }
248
249 static gboolean
250 fire_key_navigation_sig (EmpathyLiveSearch *self,
251     GdkEventKey *event)
252 {
253   gboolean ret;
254
255   g_signal_emit (self, signals[KEYNAV], 0, event, &ret);
256   return ret;
257 }
258
259 static gboolean
260 live_search_entry_key_pressed_cb (GtkEntry *entry,
261     GdkEventKey *event,
262     gpointer user_data)
263 {
264   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
265
266   /* if esc key pressed, hide the search */
267   if (event->keyval == GDK_KEY_Escape)
268     {
269       gtk_widget_hide (GTK_WIDGET (self));
270       return TRUE;
271     }
272
273   /* emit key navigation signal, so other widgets can respond to it properly */
274   if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down
275       || event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down)
276      {
277        return fire_key_navigation_sig (self, event);
278      }
279
280   if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End)
281     {
282       /* If the live search is visible, the entry should catch the Home/End
283        * events */
284       if (!gtk_widget_get_visible (GTK_WIDGET (self)))
285         {
286           return fire_key_navigation_sig (self, event);
287         }
288     }
289
290   return FALSE;
291 }
292
293 static void
294 live_search_text_changed (GtkEntry *entry,
295     gpointer user_data)
296 {
297   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
298   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
299   const gchar *text;
300
301   text = gtk_entry_get_text (entry);
302
303   if (EMP_STR_EMPTY (text))
304     gtk_widget_hide (GTK_WIDGET (self));
305   else
306     gtk_widget_show (GTK_WIDGET (self));
307
308   if (priv->stripped_words != NULL)
309     g_ptr_array_unref (priv->stripped_words);
310
311   priv->stripped_words = strip_utf8_string (text);
312
313   g_object_notify (G_OBJECT (self), "text");
314 }
315
316 static void
317 live_search_close_pressed (GtkEntry *entry,
318     GtkEntryIconPosition icon_pos,
319     GdkEvent *event,
320     gpointer user_data)
321 {
322   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
323
324   gtk_widget_hide (GTK_WIDGET (self));
325 }
326
327 static gboolean
328 live_search_key_press_event_cb (GtkWidget *widget,
329     GdkEventKey *event,
330     gpointer user_data)
331 {
332   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
333   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
334   GdkEvent *new_event;
335   gboolean ret;
336
337   /* dont forward this event to the entry, else the event is consumed by the
338    * entry and does not close the window */
339   if (!gtk_widget_get_visible (GTK_WIDGET (self)) &&
340       event->keyval == GDK_KEY_Escape)
341     return FALSE;
342
343   /* do not show the search if CTRL and/or ALT are pressed with a key
344    * this is needed, because otherwise the CTRL + F accel would not work,
345    * because the entry consumes it */
346   if (event->state & (GDK_MOD1_MASK | GDK_CONTROL_MASK) ||
347       event->keyval == GDK_KEY_Control_L ||
348       event->keyval == GDK_KEY_Control_R)
349     return FALSE;
350
351   /* dont forward the up/down and Page Up/Down arrow keys to the entry,
352    * they are needed for navigation in the treeview and are not needed in
353    * the search entry */
354    if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down ||
355        event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down)
356      return FALSE;
357
358    if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End)
359      {
360        /* Home/End keys have to be forwarded to the entry only if the live
361         * search is visible (to move the cursor inside the entry). */
362        if (!gtk_widget_get_visible (GTK_WIDGET (self)))
363          return FALSE;
364      }
365
366   /* realize the widget if it is not realized yet */
367   gtk_widget_realize (priv->search_entry);
368   if (!gtk_widget_has_focus (priv->search_entry))
369     {
370       gtk_widget_grab_focus (priv->search_entry);
371       gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
372     }
373
374   /* forward the event to the search entry */
375   new_event = gdk_event_copy ((GdkEvent *) event);
376   ret = gtk_widget_event (priv->search_entry, new_event);
377   gdk_event_free (new_event);
378
379   return ret;
380 }
381
382 static void
383 live_search_entry_activate_cb (GtkEntry *entry,
384     EmpathyLiveSearch *self)
385 {
386   g_signal_emit (self, signals[ACTIVATE], 0);
387 }
388
389 static void
390 live_search_release_hook_widget (EmpathyLiveSearch *self)
391 {
392   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
393
394   /* remove old handlers if old source was not null */
395   if (priv->hook_widget != NULL)
396     {
397       g_signal_handlers_disconnect_by_func (priv->hook_widget,
398           live_search_key_press_event_cb, self);
399       g_signal_handlers_disconnect_by_func (priv->hook_widget,
400           live_search_hook_widget_destroy_cb, self);
401       g_object_unref (priv->hook_widget);
402       priv->hook_widget = NULL;
403     }
404 }
405
406 static void
407 live_search_hook_widget_destroy_cb (GtkWidget *object,
408     gpointer user_data)
409 {
410   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
411
412   /* unref the hook widget and hide search */
413   gtk_widget_hide (GTK_WIDGET (self));
414   live_search_release_hook_widget (self);
415 }
416
417 static void
418 live_search_dispose (GObject *obj)
419 {
420   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);
421
422   live_search_release_hook_widget (self);
423
424   if (G_OBJECT_CLASS (empathy_live_search_parent_class)->dispose != NULL)
425     G_OBJECT_CLASS (empathy_live_search_parent_class)->dispose (obj);
426 }
427
428 static void
429 live_search_finalize (GObject *obj)
430 {
431   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);
432   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
433
434   if (priv->stripped_words != NULL)
435     g_ptr_array_unref (priv->stripped_words);
436
437   if (G_OBJECT_CLASS (empathy_live_search_parent_class)->finalize != NULL)
438     G_OBJECT_CLASS (empathy_live_search_parent_class)->finalize (obj);
439 }
440
441 static void
442 live_search_get_property (GObject *object,
443     guint param_id,
444     GValue *value,
445     GParamSpec *pspec)
446 {
447   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);
448
449   switch (param_id)
450     {
451     case PROP_HOOK_WIDGET:
452       g_value_set_object (value, empathy_live_search_get_hook_widget (self));
453       break;
454     case PROP_TEXT:
455       g_value_set_string (value, empathy_live_search_get_text (self));
456       break;
457     default:
458       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
459       break;
460     }
461 }
462
463 static void
464 live_search_set_property (GObject *object,
465     guint param_id,
466     const GValue *value,
467     GParamSpec *pspec)
468 {
469   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);
470
471   switch (param_id) {
472   case PROP_HOOK_WIDGET:
473     empathy_live_search_set_hook_widget (self, g_value_get_object (value));
474     break;
475   case PROP_TEXT:
476     empathy_live_search_set_text (self, g_value_get_string (value));
477     break;
478   default:
479     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
480     break;
481   };
482 }
483
484 static void
485 live_search_hide (GtkWidget *widget)
486 {
487   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
488   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
489
490   GTK_WIDGET_CLASS (empathy_live_search_parent_class)->hide (widget);
491
492   gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
493   gtk_widget_grab_focus (priv->hook_widget);
494 }
495
496 static void
497 live_search_show (GtkWidget *widget)
498 {
499   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
500   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
501
502   if (!gtk_widget_has_focus (priv->search_entry))
503     gtk_widget_grab_focus (priv->search_entry);
504
505   GTK_WIDGET_CLASS (empathy_live_search_parent_class)->show (widget);
506 }
507
508 static void
509 live_search_grab_focus (GtkWidget *widget)
510 {
511   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
512   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
513
514   if (!gtk_widget_has_focus (priv->search_entry))
515     {
516       gtk_widget_grab_focus (priv->search_entry);
517       gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
518     }
519 }
520
521 static void
522 empathy_live_search_class_init (EmpathyLiveSearchClass *klass)
523 {
524   GObjectClass *object_class = (GObjectClass *) klass;
525   GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
526   GParamSpec *param_spec;
527
528   object_class->finalize = live_search_finalize;
529   object_class->dispose = live_search_dispose;
530   object_class->get_property = live_search_get_property;
531   object_class->set_property = live_search_set_property;
532
533   widget_class->hide = live_search_hide;
534   widget_class->show = live_search_show;
535   widget_class->grab_focus = live_search_grab_focus;
536
537   signals[ACTIVATE] = g_signal_new ("activate",
538       G_TYPE_FROM_CLASS (object_class),
539       G_SIGNAL_RUN_LAST,
540       0,
541       NULL, NULL,
542       g_cclosure_marshal_VOID__VOID,
543       G_TYPE_NONE, 0);
544
545   signals[KEYNAV] = g_signal_new ("key-navigation",
546       G_TYPE_FROM_CLASS (object_class),
547       G_SIGNAL_RUN_LAST,
548       0,
549       g_signal_accumulator_true_handled, NULL,
550       _empathy_gtk_marshal_BOOLEAN__BOXED,
551       G_TYPE_BOOLEAN, 1, GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);
552
553   param_spec = g_param_spec_object ("hook-widget", "Live Search Hook Widget",
554       "The live search catches key-press-events on this widget",
555       GTK_TYPE_WIDGET, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
556   g_object_class_install_property (object_class, PROP_HOOK_WIDGET,
557       param_spec);
558
559   param_spec = g_param_spec_string ("text", "Live Search Text",
560       "The text of the live search entry",
561       "", G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
562   g_object_class_install_property (object_class, PROP_TEXT, param_spec);
563
564   g_type_class_add_private (klass, sizeof (EmpathyLiveSearchPriv));
565 }
566
567 static void
568 empathy_live_search_init (EmpathyLiveSearch *self)
569 {
570   EmpathyLiveSearchPriv *priv =
571     G_TYPE_INSTANCE_GET_PRIVATE ((self), EMPATHY_TYPE_LIVE_SEARCH,
572         EmpathyLiveSearchPriv);
573
574   gtk_widget_set_no_show_all (GTK_WIDGET (self), TRUE);
575
576   priv->search_entry = gtk_entry_new ();
577   gtk_entry_set_icon_from_stock (GTK_ENTRY (priv->search_entry),
578       GTK_ENTRY_ICON_SECONDARY, GTK_STOCK_CLOSE);
579   gtk_entry_set_icon_activatable (GTK_ENTRY (priv->search_entry),
580       GTK_ENTRY_ICON_SECONDARY, TRUE);
581   gtk_entry_set_icon_sensitive (GTK_ENTRY (priv->search_entry),
582       GTK_ENTRY_ICON_SECONDARY, TRUE);
583   gtk_widget_show (priv->search_entry);
584
585   gtk_box_pack_start (GTK_BOX (self), priv->search_entry, TRUE, TRUE, 0);
586
587   g_signal_connect (priv->search_entry, "icon_release",
588       G_CALLBACK (live_search_close_pressed), self);
589   g_signal_connect (priv->search_entry, "changed",
590       G_CALLBACK (live_search_text_changed), self);
591   g_signal_connect (priv->search_entry, "key-press-event",
592       G_CALLBACK (live_search_entry_key_pressed_cb), self);
593   g_signal_connect (priv->search_entry, "activate",
594       G_CALLBACK (live_search_entry_activate_cb), self);
595
596   priv->hook_widget = NULL;
597
598   self->priv = priv;
599 }
600
601 GtkWidget *
602 empathy_live_search_new (GtkWidget *hook)
603 {
604   g_return_val_if_fail (hook == NULL || GTK_IS_WIDGET (hook), NULL);
605
606   return g_object_new (EMPATHY_TYPE_LIVE_SEARCH,
607       "hook-widget", hook,
608       NULL);
609 }
610
611 /* public methods */
612
613 GtkWidget *
614 empathy_live_search_get_hook_widget (EmpathyLiveSearch *self)
615 {
616   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
617
618   g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), NULL);
619
620   return priv->hook_widget;
621 }
622
623 void
624 empathy_live_search_set_hook_widget (EmpathyLiveSearch *self,
625     GtkWidget *hook)
626 {
627   EmpathyLiveSearchPriv *priv;
628
629   g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self));
630   g_return_if_fail (hook == NULL || GTK_IS_WIDGET (hook));
631
632   priv = GET_PRIV (self);
633
634   /* release the actual widget */
635   live_search_release_hook_widget (self);
636
637   /* connect handlers if new source is not null */
638   if (hook != NULL)
639     {
640       priv->hook_widget = g_object_ref (hook);
641       g_signal_connect (priv->hook_widget, "key-press-event",
642           G_CALLBACK (live_search_key_press_event_cb),
643           self);
644       g_signal_connect (priv->hook_widget, "destroy",
645           G_CALLBACK (live_search_hook_widget_destroy_cb),
646           self);
647     }
648 }
649
650 const gchar *
651 empathy_live_search_get_text (EmpathyLiveSearch *self)
652 {
653   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
654
655   g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), NULL);
656
657   return gtk_entry_get_text (GTK_ENTRY (priv->search_entry));
658 }
659
660 void
661 empathy_live_search_set_text (EmpathyLiveSearch *self,
662     const gchar *text)
663 {
664   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
665
666   g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self));
667   g_return_if_fail (text != NULL);
668
669   gtk_entry_set_text (GTK_ENTRY (priv->search_entry), text);
670 }
671
672 /**
673  * empathy_live_search_match:
674  * @self: a #EmpathyLiveSearch
675  * @string: a string where to search, must be valid UTF-8.
676  *
677  * Search if one of the words in @string string starts with the current text
678  * of @self.
679  *
680  * Searching for "aba" in "Abasto" will match, searching in "Moraba" will not,
681  * and searching in "A tool (abacus)" will do.
682  *
683  * The match is not case-sensitive, and regardless of the accentuation marks.
684  *
685  * Returns: %TRUE if a match is found, %FALSE otherwise.
686  *
687  **/
688 gboolean
689 empathy_live_search_match (EmpathyLiveSearch *self,
690     const gchar *string)
691 {
692   EmpathyLiveSearchPriv *priv;
693
694   g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), FALSE);
695
696   priv = GET_PRIV (self);
697
698   return live_search_match_words (string, priv->stripped_words);
699 }
700
701 gboolean
702 empathy_live_search_match_string (const gchar *string,
703     const gchar *prefix)
704 {
705   GPtrArray *words;
706   gboolean match;
707
708   words = strip_utf8_string (prefix);
709   match = live_search_match_words (string, words);
710   if (words != NULL)
711     g_ptr_array_unref (words);
712
713   return match;
714 }
715