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