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