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