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