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