]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-live-search.c
Merge branch 'gseal'
[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   gunichar *text_stripped;
44 } EmpathyLiveSearchPriv;
45
46 enum
47 {
48   PROP_0,
49   PROP_HOOK_WIDGET,
50   PROP_TEXT
51 };
52
53 static void live_search_hook_widget_destroy_cb (GtkObject *object,
54     gpointer user_data);
55
56 /**
57  * stripped_char:
58  *
59  * Returns a stripped version of @ch, removing any case, accentuation
60  * mark, or any special mark on it.
61  **/
62 static gunichar
63 stripped_char (gunichar ch)
64 {
65   gunichar retval = 0;
66   GUnicodeType utype;
67   gunichar *decomp;
68   gsize dlen;
69
70   utype = g_unichar_type (ch);
71
72   switch (utype)
73     {
74     case G_UNICODE_CONTROL:
75     case G_UNICODE_FORMAT:
76     case G_UNICODE_UNASSIGNED:
77     case G_UNICODE_COMBINING_MARK:
78       /* Ignore those */
79       break;
80     default:
81       ch = g_unichar_tolower (ch);
82       decomp = g_unicode_canonical_decomposition (ch, &dlen);
83       if (decomp != NULL)
84         {
85           retval = decomp[0];
86           g_free (decomp);
87         }
88     }
89
90   return retval;
91 }
92
93 static gunichar *
94 strip_utf8_string (const gchar *string)
95 {
96   gunichar *ret;
97   gint ret_len;
98   const gchar *p;
99
100   if (EMP_STR_EMPTY (string))
101     return NULL;
102
103   ret = g_malloc (sizeof (gunichar) * (strlen (string) + 1));
104   ret_len = 0;
105
106   for (p = string; *p != '\0'; p = g_utf8_next_char (p))
107     {
108       gunichar sc;
109
110       sc = stripped_char (g_utf8_get_char (p));
111       if (sc != 0)
112         ret[ret_len++] = sc;
113     }
114
115   ret[ret_len] = 0;
116
117   return ret;
118 }
119
120 static gboolean
121 live_search_entry_key_pressed_cb (GtkEntry *entry,
122     GdkEventKey *event,
123     gpointer user_data)
124 {
125   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
126
127   /* if esc key pressed, hide the search */
128   if (event->keyval == GDK_Escape)
129     {
130       gtk_widget_hide (GTK_WIDGET (self));
131       return TRUE;
132     }
133
134   return FALSE;
135 }
136
137 static void
138 live_search_text_changed (GtkEntry *entry,
139     gpointer user_data)
140 {
141   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
142   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
143   const gchar *text;
144
145   text = gtk_entry_get_text (entry);
146
147   if (EMP_STR_EMPTY (text))
148     gtk_widget_hide (GTK_WIDGET (self));
149   else
150     gtk_widget_show (GTK_WIDGET (self));
151
152   g_free (priv->text_stripped);
153   priv->text_stripped = strip_utf8_string (text);
154   g_object_notify (G_OBJECT (self), "text");
155 }
156
157 static void
158 live_search_close_pressed (GtkEntry *entry,
159     GtkEntryIconPosition icon_pos,
160     GdkEvent *event,
161     gpointer user_data)
162 {
163   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
164
165   gtk_widget_hide (GTK_WIDGET (self));
166 }
167
168 static gboolean
169 live_search_key_press_event_cb (GtkWidget *widget,
170     GdkEventKey *event,
171     gpointer user_data)
172 {
173   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
174   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
175   GdkEvent *new_event;
176   gboolean ret;
177
178   /* dont forward this event to the entry, else the event is consumed by the
179    * entry and does not close the window */
180   if (!gtk_widget_get_visible (GTK_WIDGET (self)) &&
181       event->keyval == GDK_Escape)
182     return FALSE;
183
184   /* do not show the search if CTRL and/or ALT are pressed with a key
185    * this is needed, because otherwise the CTRL + F accel would not work,
186    * because the entry consumes it */
187   if (event->state & (GDK_MOD1_MASK | GDK_CONTROL_MASK) ||
188       event->keyval == GDK_Control_L ||
189       event->keyval == GDK_Control_R)
190     return FALSE;
191
192   /* realize the widget if it is not realized yet */
193   gtk_widget_realize (priv->search_entry);
194   if (!gtk_widget_has_focus (priv->search_entry))
195     {
196       gtk_widget_grab_focus (priv->search_entry);
197       gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
198     }
199
200   /* forward the event to the search entry */
201   new_event = gdk_event_copy ((GdkEvent *) event);
202   ret = gtk_widget_event (priv->search_entry, new_event);
203   gdk_event_free (new_event);
204
205   return ret;
206 }
207
208 static void
209 live_search_release_hook_widget (EmpathyLiveSearch *self)
210 {
211   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
212
213   /* remove old handlers if old source was not null */
214   if (priv->hook_widget != NULL)
215     {
216       g_signal_handlers_disconnect_by_func (priv->hook_widget,
217           live_search_key_press_event_cb, self);
218       g_signal_handlers_disconnect_by_func (priv->hook_widget,
219           live_search_hook_widget_destroy_cb, self);
220       g_object_unref (priv->hook_widget);
221       priv->hook_widget = NULL;
222     }
223 }
224
225 static void
226 live_search_hook_widget_destroy_cb (GtkObject *object,
227     gpointer user_data)
228 {
229   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
230
231   /* unref the hook widget and hide search */
232   live_search_release_hook_widget (self);
233   gtk_widget_hide (GTK_WIDGET (self));
234 }
235
236 static void
237 live_search_dispose (GObject *obj)
238 {
239   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);
240
241   live_search_release_hook_widget (self);
242
243   if (G_OBJECT_CLASS (empathy_live_search_parent_class)->dispose != NULL)
244     G_OBJECT_CLASS (empathy_live_search_parent_class)->dispose (obj);
245 }
246
247 static void
248 live_search_finalize (GObject *obj)
249 {
250   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);
251   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
252
253   g_free (priv->text_stripped);
254
255   if (G_OBJECT_CLASS (empathy_live_search_parent_class)->finalize != NULL)
256     G_OBJECT_CLASS (empathy_live_search_parent_class)->finalize (obj);
257 }
258
259 static void
260 live_search_get_property (GObject *object,
261     guint param_id,
262     GValue *value,
263     GParamSpec *pspec)
264 {
265   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);
266
267   switch (param_id)
268     {
269     case PROP_HOOK_WIDGET:
270       g_value_set_object (value, empathy_live_search_get_hook_widget (self));
271       break;
272     case PROP_TEXT:
273       g_value_set_string (value, empathy_live_search_get_text (self));
274       break;
275     default:
276       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
277       break;
278     }
279 }
280
281 static void
282 live_search_set_property (GObject *object,
283     guint param_id,
284     const GValue *value,
285     GParamSpec *pspec)
286 {
287   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);
288
289   switch (param_id) {
290   case PROP_HOOK_WIDGET:
291     empathy_live_search_set_hook_widget (self, g_value_get_object (value));
292     break;
293   case PROP_TEXT:
294     empathy_live_search_set_text (self, g_value_get_string (value));
295     break;
296   default:
297     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
298     break;
299   };
300 }
301
302 static void
303 live_search_hide (GtkWidget *widget)
304 {
305   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
306   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
307
308   GTK_WIDGET_CLASS (empathy_live_search_parent_class)->hide (widget);
309
310   gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
311   gtk_widget_grab_focus (priv->hook_widget);
312 }
313
314 static void
315 live_search_show (GtkWidget *widget)
316 {
317   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
318   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
319
320   if (!gtk_widget_has_focus (priv->search_entry))
321     gtk_widget_grab_focus (priv->search_entry);
322
323   GTK_WIDGET_CLASS (empathy_live_search_parent_class)->show (widget);
324 }
325
326 static void
327 live_search_grab_focus (GtkWidget *widget)
328 {
329   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
330   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
331
332   if (!gtk_widget_has_focus (priv->search_entry))
333     gtk_widget_grab_focus (priv->search_entry);
334 }
335
336 static void
337 empathy_live_search_class_init (EmpathyLiveSearchClass *klass)
338 {
339   GObjectClass *object_class = (GObjectClass *) klass;
340   GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
341   GParamSpec *param_spec;
342
343   object_class->finalize = live_search_finalize;
344   object_class->dispose = live_search_dispose;
345   object_class->get_property = live_search_get_property;
346   object_class->set_property = live_search_set_property;
347
348   widget_class->hide = live_search_hide;
349   widget_class->show = live_search_show;
350   widget_class->grab_focus = live_search_grab_focus;
351
352   param_spec = g_param_spec_object ("hook-widget", "Live Searchs Hook Widget",
353       "The live search catches key-press-events on this widget",
354       GTK_TYPE_WIDGET, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
355   g_object_class_install_property (object_class, PROP_HOOK_WIDGET,
356       param_spec);
357
358   param_spec = g_param_spec_string ("text", "Live Search Text",
359       "The text of the live search entry",
360       "", G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
361   g_object_class_install_property (object_class, PROP_TEXT, param_spec);
362
363   g_type_class_add_private (klass, sizeof (EmpathyLiveSearchPriv));
364 }
365
366 static void
367 empathy_live_search_init (EmpathyLiveSearch *self)
368 {
369   EmpathyLiveSearchPriv *priv =
370     G_TYPE_INSTANCE_GET_PRIVATE ((self), EMPATHY_TYPE_LIVE_SEARCH,
371         EmpathyLiveSearchPriv);
372
373   gtk_widget_set_no_show_all (GTK_WIDGET (self), TRUE);
374
375   priv->search_entry = gtk_entry_new ();
376   gtk_entry_set_icon_from_stock (GTK_ENTRY (priv->search_entry),
377       GTK_ENTRY_ICON_SECONDARY, GTK_STOCK_CLOSE);
378   gtk_entry_set_icon_activatable (GTK_ENTRY (priv->search_entry),
379       GTK_ENTRY_ICON_SECONDARY, TRUE);
380   gtk_entry_set_icon_sensitive (GTK_ENTRY (priv->search_entry),
381       GTK_ENTRY_ICON_SECONDARY, TRUE);
382   gtk_widget_show (priv->search_entry);
383
384   gtk_box_pack_start (GTK_BOX (self), priv->search_entry, TRUE, TRUE, 0);
385
386   g_signal_connect (priv->search_entry, "icon_release",
387       G_CALLBACK (live_search_close_pressed), self);
388   g_signal_connect (priv->search_entry, "changed",
389       G_CALLBACK (live_search_text_changed), self);
390   g_signal_connect (priv->search_entry, "key-press-event",
391       G_CALLBACK (live_search_entry_key_pressed_cb), self);
392
393   priv->hook_widget = NULL;
394
395   self->priv = priv;
396 }
397
398 GtkWidget *
399 empathy_live_search_new (GtkWidget *hook)
400 {
401   g_return_val_if_fail (hook == NULL || GTK_IS_WIDGET (hook), NULL);
402
403   return g_object_new (EMPATHY_TYPE_LIVE_SEARCH,
404       "hook-widget", hook,
405       NULL);
406 }
407
408 /* public methods */
409
410 GtkWidget *
411 empathy_live_search_get_hook_widget (EmpathyLiveSearch *self)
412 {
413   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
414
415   g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), NULL);
416
417   return priv->hook_widget;
418 }
419
420 void
421 empathy_live_search_set_hook_widget (EmpathyLiveSearch *self,
422     GtkWidget *hook)
423 {
424   EmpathyLiveSearchPriv *priv;
425
426   g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self));
427   g_return_if_fail (hook == NULL || GTK_IS_WIDGET (hook));
428
429   priv = GET_PRIV (self);
430
431   /* release the actual widget */
432   live_search_release_hook_widget (self);
433
434   /* connect handlers if new source is not null */
435   if (hook != NULL)
436     {
437       priv->hook_widget = g_object_ref (hook);
438       g_signal_connect (priv->hook_widget, "key-press-event",
439           G_CALLBACK (live_search_key_press_event_cb),
440           self);
441       g_signal_connect (priv->hook_widget, "destroy",
442           G_CALLBACK (live_search_hook_widget_destroy_cb),
443           self);
444     }
445 }
446
447 const gchar *
448 empathy_live_search_get_text (EmpathyLiveSearch *self)
449 {
450   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
451
452   g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), NULL);
453
454   return gtk_entry_get_text (GTK_ENTRY (priv->search_entry));
455 }
456
457 void
458 empathy_live_search_set_text (EmpathyLiveSearch *self,
459     const gchar *text)
460 {
461   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
462
463   g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self));
464   g_return_if_fail (text != NULL);
465
466   gtk_entry_set_text (GTK_ENTRY (priv->search_entry), text);
467 }
468
469 static gboolean
470 live_search_match_string (const gchar *string,
471     const gunichar *prefix)
472 {
473   const gchar *p;
474
475   if (prefix == NULL || prefix[0] == 0)
476     return TRUE;
477
478   if (EMP_STR_EMPTY (string))
479     return FALSE;
480
481   for (p = string; *p != '\0'; p = g_utf8_next_char (p))
482     {
483       guint i = 0;
484
485       /* Search the start of the word (skip non alpha-num chars) */
486       while (*p != '\0' && !g_unichar_isalnum (g_utf8_get_char (p)))
487         p = g_utf8_next_char (p);
488
489       /* Check if this word match prefix */
490       while (*p != '\0')
491         {
492           gunichar sc;
493
494           sc = stripped_char (g_utf8_get_char (p));
495           if (sc != 0)
496             {
497               /* If the char does not match, stop */
498               if (sc != prefix[i])
499                 break;
500
501               /* The char matched. If it was the last of prefix, stop */
502               if (prefix[++i] == 0)
503                 return TRUE;
504             }
505
506           p = g_utf8_next_char (p);
507         }
508
509       /* This word didn't match, go to next one (skip alpha-num chars) */
510       while (*p != '\0' && g_unichar_isalnum (g_utf8_get_char (p)))
511         p = g_utf8_next_char (p);
512
513       if (*p == '\0')
514         break;
515     }
516
517   return FALSE;
518 }
519
520 /**
521  * empathy_live_search_match:
522  * @self: a #EmpathyLiveSearch
523  * @string: a string where to search, must be valid UTF-8.
524  *
525  * Search if one of the words in @string string starts with the current text
526  * of @self.
527  *
528  * Searching for "aba" in "Abasto" will match, searching in "Moraba" will not,
529  * and searching in "A tool (abacus)" will do.
530  *
531  * The match is not case-sensitive, and regardless of the accentuation marks.
532  *
533  * Returns: %TRUE if a match is found, %FALSE otherwise.
534  *
535  **/
536 gboolean
537 empathy_live_search_match (EmpathyLiveSearch *self,
538     const gchar *string)
539 {
540   EmpathyLiveSearchPriv *priv;
541
542   g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), FALSE);
543
544   priv = GET_PRIV (self);
545
546   return live_search_match_string (string, priv->text_stripped);
547 }
548
549 gboolean
550 empathy_live_search_match_string (const gchar *string,
551     const gchar *prefix)
552 {
553   gunichar *stripped;
554   gboolean match;
555
556   stripped = strip_utf8_string (prefix);
557   match = live_search_match_string (string, stripped);
558   g_free (stripped);
559
560   return match;
561 }
562