Merge branch 'gnome-3-6'
[empathy-mirror.git] / libempathy-gtk / empathy-live-search.c
blobbb1881fe6f6c50164f07bf534f255925d603857e
1 /*
2 * Copyright (C) 2010 Collabora Ltd.
3 * Copyright (C) 2007-2010 Nokia Corporation.
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.
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.
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
19 * Authors: Felix Kaser <felix.kaser@collabora.co.uk>
20 * Xavier Claessens <xavier.claessens@collabora.co.uk>
21 * Claudio Saavedra <csaavedra@igalia.com>
24 #include <config.h>
25 #include <string.h>
27 #include <gtk/gtk.h>
28 #include <gdk/gdkkeysyms.h>
30 #include <libempathy/empathy-utils.h>
32 #include "empathy-live-search.h"
34 G_DEFINE_TYPE (EmpathyLiveSearch, empathy_live_search, GTK_TYPE_HBOX)
36 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyLiveSearch)
38 typedef struct
40 GtkWidget *search_entry;
41 GtkWidget *hook_widget;
43 GPtrArray *stripped_words;
44 } EmpathyLiveSearchPriv;
46 enum
48 PROP_0,
49 PROP_HOOK_WIDGET,
50 PROP_TEXT
53 enum
55 ACTIVATE,
56 KEYNAV,
57 LAST_SIGNAL
60 static guint signals[LAST_SIGNAL];
62 static void live_search_hook_widget_destroy_cb (GtkWidget *object,
63 gpointer user_data);
65 /**
66 * stripped_char:
68 * Returns a stripped version of @ch, removing any case, accentuation
69 * mark, or any special mark on it.
70 **/
71 static gunichar
72 stripped_char (gunichar ch)
74 gunichar retval = 0;
75 GUnicodeType utype;
77 utype = g_unichar_type (ch);
79 switch (utype)
81 case G_UNICODE_CONTROL:
82 case G_UNICODE_FORMAT:
83 case G_UNICODE_UNASSIGNED:
84 case G_UNICODE_NON_SPACING_MARK:
85 case G_UNICODE_COMBINING_MARK:
86 case G_UNICODE_ENCLOSING_MARK:
87 /* Ignore those */
88 break;
89 case G_UNICODE_PRIVATE_USE:
90 case G_UNICODE_SURROGATE:
91 case G_UNICODE_LOWERCASE_LETTER:
92 case G_UNICODE_MODIFIER_LETTER:
93 case G_UNICODE_OTHER_LETTER:
94 case G_UNICODE_TITLECASE_LETTER:
95 case G_UNICODE_UPPERCASE_LETTER:
96 case G_UNICODE_DECIMAL_NUMBER:
97 case G_UNICODE_LETTER_NUMBER:
98 case G_UNICODE_OTHER_NUMBER:
99 case G_UNICODE_CONNECT_PUNCTUATION:
100 case G_UNICODE_DASH_PUNCTUATION:
101 case G_UNICODE_CLOSE_PUNCTUATION:
102 case G_UNICODE_FINAL_PUNCTUATION:
103 case G_UNICODE_INITIAL_PUNCTUATION:
104 case G_UNICODE_OTHER_PUNCTUATION:
105 case G_UNICODE_OPEN_PUNCTUATION:
106 case G_UNICODE_CURRENCY_SYMBOL:
107 case G_UNICODE_MODIFIER_SYMBOL:
108 case G_UNICODE_MATH_SYMBOL:
109 case G_UNICODE_OTHER_SYMBOL:
110 case G_UNICODE_LINE_SEPARATOR:
111 case G_UNICODE_PARAGRAPH_SEPARATOR:
112 case G_UNICODE_SPACE_SEPARATOR:
113 default:
114 ch = g_unichar_tolower (ch);
115 g_unichar_fully_decompose (ch, FALSE, &retval, 1);
118 return retval;
121 static void
122 append_word (GPtrArray **word_array,
123 GString **word)
125 if (*word != NULL)
127 if (*word_array == NULL)
128 *word_array = g_ptr_array_new_with_free_func (g_free);
129 g_ptr_array_add (*word_array, g_string_free (*word, FALSE));
130 *word = NULL;
134 GPtrArray *
135 empathy_live_search_strip_utf8_string (const gchar *string)
137 GPtrArray *word_array = NULL;
138 GString *word = NULL;
139 const gchar *p;
141 if (EMP_STR_EMPTY (string))
142 return NULL;
144 for (p = string; *p != '\0'; p = g_utf8_next_char (p))
146 gunichar sc;
148 /* Make the char lower-case, remove its accentuation marks, and ignore it
149 * if it is just unicode marks */
150 sc = stripped_char (g_utf8_get_char (p));
151 if (sc == 0)
152 continue;
154 /* If it is not alpha-num, it is separator between words */
155 if (!g_unichar_isalnum (sc))
157 append_word (&word_array, &word);
158 continue;
161 /* It is alpha-num, append this char to current word, or start new word */
162 if (word == NULL)
163 word = g_string_new (NULL);
164 g_string_append_unichar (word, sc);
167 append_word (&word_array, &word);
169 return word_array;
172 static gboolean
173 live_search_match_prefix (const gchar *string,
174 const gchar *prefix)
176 const gchar *p;
177 const gchar *prefix_p;
178 gboolean next_word = FALSE;
180 if (prefix == NULL || prefix[0] == 0)
181 return TRUE;
183 if (EMP_STR_EMPTY (string))
184 return FALSE;
186 prefix_p = prefix;
187 for (p = string; *p != '\0'; p = g_utf8_next_char (p))
189 gunichar sc;
191 /* Make the char lower-case, remove its accentuation marks, and ignore it
192 * if it is just unicode marks */
193 sc = stripped_char (g_utf8_get_char (p));
194 if (sc == 0)
195 continue;
197 /* If we want to go to next word, ignore alpha-num chars */
198 if (next_word && g_unichar_isalnum (sc))
199 continue;
200 next_word = FALSE;
202 /* Ignore word separators */
203 if (!g_unichar_isalnum (sc))
204 continue;
206 /* If this char does not match prefix_p, go to next word and start again
207 * from the beginning of prefix */
208 if (sc != g_utf8_get_char (prefix_p))
210 next_word = TRUE;
211 prefix_p = prefix;
212 continue;
215 /* prefix_p match, verify to next char. If this was the last of prefix,
216 * it means it completely machted and we are done. */
217 prefix_p = g_utf8_next_char (prefix_p);
218 if (*prefix_p == '\0')
219 return TRUE;
222 return FALSE;
225 gboolean
226 empathy_live_search_match_words (const gchar *string,
227 GPtrArray *words)
229 guint i;
231 if (words == NULL)
232 return TRUE;
234 for (i = 0; i < words->len; i++)
235 if (!live_search_match_prefix (string, g_ptr_array_index (words, i)))
236 return FALSE;
238 return TRUE;
241 static gboolean
242 fire_key_navigation_sig (EmpathyLiveSearch *self,
243 GdkEventKey *event)
245 gboolean ret;
247 g_signal_emit (self, signals[KEYNAV], 0, event, &ret);
248 return ret;
251 static gboolean
252 live_search_entry_key_pressed_cb (GtkEntry *entry,
253 GdkEventKey *event,
254 gpointer user_data)
256 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
258 /* if esc key pressed, hide the search */
259 if (event->keyval == GDK_KEY_Escape)
261 gtk_widget_hide (GTK_WIDGET (self));
262 return TRUE;
265 /* emit key navigation signal, so other widgets can respond to it properly */
266 if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down
267 || event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down
268 || event->keyval == GDK_KEY_Menu)
270 return fire_key_navigation_sig (self, event);
273 if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End ||
274 event->keyval == GDK_KEY_space)
276 /* If the live search is visible, the entry should catch the Home/End
277 * and space events */
278 if (!gtk_widget_get_visible (GTK_WIDGET (self)))
280 return fire_key_navigation_sig (self, event);
284 return FALSE;
287 static void
288 live_search_text_changed (GtkEntry *entry,
289 gpointer user_data)
291 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
292 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
293 const gchar *text;
295 text = gtk_entry_get_text (entry);
297 if (EMP_STR_EMPTY (text))
298 gtk_widget_hide (GTK_WIDGET (self));
299 else
300 gtk_widget_show (GTK_WIDGET (self));
302 if (priv->stripped_words != NULL)
303 g_ptr_array_unref (priv->stripped_words);
305 priv->stripped_words = empathy_live_search_strip_utf8_string (text);
307 g_object_notify (G_OBJECT (self), "text");
310 static void
311 live_search_close_pressed (GtkEntry *entry,
312 GtkEntryIconPosition icon_pos,
313 GdkEvent *event,
314 gpointer user_data)
316 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
318 gtk_widget_hide (GTK_WIDGET (self));
321 static gboolean
322 live_search_key_press_event_cb (GtkWidget *widget,
323 GdkEventKey *event,
324 gpointer user_data)
326 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
327 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
328 GdkEvent *new_event;
329 gboolean ret;
331 /* dont forward this event to the entry, else the event is consumed by the
332 * entry and does not close the window */
333 if (!gtk_widget_get_visible (GTK_WIDGET (self)) &&
334 event->keyval == GDK_KEY_Escape)
335 return FALSE;
337 /* do not show the search if CTRL and/or ALT are pressed with a key
338 * this is needed, because otherwise the CTRL + F accel would not work,
339 * because the entry consumes it */
340 if (event->state & (GDK_MOD1_MASK | GDK_CONTROL_MASK) ||
341 event->keyval == GDK_KEY_Control_L ||
342 event->keyval == GDK_KEY_Control_R)
343 return FALSE;
345 /* dont forward the up/down and Page Up/Down arrow keys to the entry,
346 * they are needed for navigation in the treeview and are not needed in
347 * the search entry */
348 if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down ||
349 event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down ||
350 event->keyval == GDK_KEY_Menu)
351 return FALSE;
353 if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End ||
354 event->keyval == GDK_KEY_space)
356 /* Home/End and space keys have to be forwarded to the entry only if
357 * the live search is visible (to move the cursor inside the entry). */
358 if (!gtk_widget_get_visible (GTK_WIDGET (self)))
359 return FALSE;
362 /* Don't forward shift keys events as focusing the search entry would
363 * cancel an in-progress editing on a cell renderer (like when renaming a
364 * group). There is no point focusing it anyway as we don't display the
365 * search entry when only a shift key is pressed. */
366 if (event->keyval == GDK_KEY_Shift_L ||
367 event->keyval == GDK_KEY_Shift_R)
368 return FALSE;
370 /* realize the widget if it is not realized yet */
371 gtk_widget_realize (priv->search_entry);
372 if (!gtk_widget_has_focus (priv->search_entry))
374 gtk_widget_grab_focus (priv->search_entry);
375 gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
378 /* forward the event to the search entry */
379 new_event = gdk_event_copy ((GdkEvent *) event);
380 ret = gtk_widget_event (priv->search_entry, new_event);
381 gdk_event_free (new_event);
383 return ret;
386 static void
387 live_search_entry_activate_cb (GtkEntry *entry,
388 EmpathyLiveSearch *self)
390 g_signal_emit (self, signals[ACTIVATE], 0);
393 static void
394 live_search_release_hook_widget (EmpathyLiveSearch *self)
396 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
398 /* remove old handlers if old source was not null */
399 if (priv->hook_widget != NULL)
401 g_signal_handlers_disconnect_by_func (priv->hook_widget,
402 live_search_key_press_event_cb, self);
403 g_signal_handlers_disconnect_by_func (priv->hook_widget,
404 live_search_hook_widget_destroy_cb, self);
405 g_object_unref (priv->hook_widget);
406 priv->hook_widget = NULL;
410 static void
411 live_search_hook_widget_destroy_cb (GtkWidget *object,
412 gpointer user_data)
414 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
416 /* unref the hook widget and hide search */
417 gtk_widget_hide (GTK_WIDGET (self));
418 live_search_release_hook_widget (self);
421 static void
422 live_search_dispose (GObject *obj)
424 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);
426 live_search_release_hook_widget (self);
428 if (G_OBJECT_CLASS (empathy_live_search_parent_class)->dispose != NULL)
429 G_OBJECT_CLASS (empathy_live_search_parent_class)->dispose (obj);
432 static void
433 live_search_finalize (GObject *obj)
435 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);
436 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
438 if (priv->stripped_words != NULL)
439 g_ptr_array_unref (priv->stripped_words);
441 if (G_OBJECT_CLASS (empathy_live_search_parent_class)->finalize != NULL)
442 G_OBJECT_CLASS (empathy_live_search_parent_class)->finalize (obj);
445 static void
446 live_search_get_property (GObject *object,
447 guint param_id,
448 GValue *value,
449 GParamSpec *pspec)
451 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);
453 switch (param_id)
455 case PROP_HOOK_WIDGET:
456 g_value_set_object (value, empathy_live_search_get_hook_widget (self));
457 break;
458 case PROP_TEXT:
459 g_value_set_string (value, empathy_live_search_get_text (self));
460 break;
461 default:
462 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
463 break;
467 static void
468 live_search_set_property (GObject *object,
469 guint param_id,
470 const GValue *value,
471 GParamSpec *pspec)
473 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);
475 switch (param_id) {
476 case PROP_HOOK_WIDGET:
477 empathy_live_search_set_hook_widget (self, g_value_get_object (value));
478 break;
479 case PROP_TEXT:
480 empathy_live_search_set_text (self, g_value_get_string (value));
481 break;
482 default:
483 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
484 break;
488 static void
489 live_search_unmap (GtkWidget *widget)
491 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
492 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
494 GTK_WIDGET_CLASS (empathy_live_search_parent_class)->unmap (widget);
496 /* unmap can happen if a parent gets hidden, in that case we want to hide
497 * the live search as well, so when it gets mapped again, the live search
498 * won't be shown. */
499 gtk_widget_hide (widget);
501 gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
502 gtk_widget_grab_focus (priv->hook_widget);
505 static void
506 live_search_show (GtkWidget *widget)
508 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
509 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
511 if (!gtk_widget_has_focus (priv->search_entry))
512 gtk_widget_grab_focus (priv->search_entry);
514 GTK_WIDGET_CLASS (empathy_live_search_parent_class)->show (widget);
517 static void
518 live_search_grab_focus (GtkWidget *widget)
520 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
521 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
523 if (!gtk_widget_has_focus (priv->search_entry))
525 gtk_widget_grab_focus (priv->search_entry);
526 gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
530 static void
531 empathy_live_search_class_init (EmpathyLiveSearchClass *klass)
533 GObjectClass *object_class = (GObjectClass *) klass;
534 GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
535 GParamSpec *param_spec;
537 object_class->finalize = live_search_finalize;
538 object_class->dispose = live_search_dispose;
539 object_class->get_property = live_search_get_property;
540 object_class->set_property = live_search_set_property;
542 widget_class->unmap = live_search_unmap;
543 widget_class->show = live_search_show;
544 widget_class->grab_focus = live_search_grab_focus;
546 signals[ACTIVATE] = g_signal_new ("activate",
547 G_TYPE_FROM_CLASS (object_class),
548 G_SIGNAL_RUN_LAST,
550 NULL, NULL,
551 g_cclosure_marshal_generic,
552 G_TYPE_NONE, 0);
554 signals[KEYNAV] = g_signal_new ("key-navigation",
555 G_TYPE_FROM_CLASS (object_class),
556 G_SIGNAL_RUN_LAST,
558 g_signal_accumulator_true_handled, NULL,
559 g_cclosure_marshal_generic,
560 G_TYPE_BOOLEAN, 1, GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);
562 param_spec = g_param_spec_object ("hook-widget", "Live Search Hook Widget",
563 "The live search catches key-press-events on this widget",
564 GTK_TYPE_WIDGET, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
565 g_object_class_install_property (object_class, PROP_HOOK_WIDGET,
566 param_spec);
568 param_spec = g_param_spec_string ("text", "Live Search Text",
569 "The text of the live search entry",
570 "", G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
571 g_object_class_install_property (object_class, PROP_TEXT, param_spec);
573 g_type_class_add_private (klass, sizeof (EmpathyLiveSearchPriv));
576 static void
577 empathy_live_search_init (EmpathyLiveSearch *self)
579 EmpathyLiveSearchPriv *priv =
580 G_TYPE_INSTANCE_GET_PRIVATE ((self), EMPATHY_TYPE_LIVE_SEARCH,
581 EmpathyLiveSearchPriv);
583 gtk_widget_set_no_show_all (GTK_WIDGET (self), TRUE);
585 priv->search_entry = gtk_entry_new ();
586 gtk_entry_set_icon_from_stock (GTK_ENTRY (priv->search_entry),
587 GTK_ENTRY_ICON_SECONDARY, GTK_STOCK_CLOSE);
588 gtk_entry_set_icon_activatable (GTK_ENTRY (priv->search_entry),
589 GTK_ENTRY_ICON_SECONDARY, TRUE);
590 gtk_entry_set_icon_sensitive (GTK_ENTRY (priv->search_entry),
591 GTK_ENTRY_ICON_SECONDARY, TRUE);
592 gtk_widget_show (priv->search_entry);
594 gtk_box_pack_start (GTK_BOX (self), priv->search_entry, TRUE, TRUE, 0);
596 g_signal_connect (priv->search_entry, "icon_release",
597 G_CALLBACK (live_search_close_pressed), self);
598 g_signal_connect (priv->search_entry, "changed",
599 G_CALLBACK (live_search_text_changed), self);
600 g_signal_connect (priv->search_entry, "key-press-event",
601 G_CALLBACK (live_search_entry_key_pressed_cb), self);
602 g_signal_connect (priv->search_entry, "activate",
603 G_CALLBACK (live_search_entry_activate_cb), self);
605 priv->hook_widget = NULL;
607 self->priv = priv;
610 GtkWidget *
611 empathy_live_search_new (GtkWidget *hook)
613 g_return_val_if_fail (hook == NULL || GTK_IS_WIDGET (hook), NULL);
615 return g_object_new (EMPATHY_TYPE_LIVE_SEARCH,
616 "hook-widget", hook,
617 NULL);
620 /* public methods */
622 GtkWidget *
623 empathy_live_search_get_hook_widget (EmpathyLiveSearch *self)
625 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
627 g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), NULL);
629 return priv->hook_widget;
632 void
633 empathy_live_search_set_hook_widget (EmpathyLiveSearch *self,
634 GtkWidget *hook)
636 EmpathyLiveSearchPriv *priv;
638 g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self));
639 g_return_if_fail (hook == NULL || GTK_IS_WIDGET (hook));
641 priv = GET_PRIV (self);
643 /* release the actual widget */
644 live_search_release_hook_widget (self);
646 /* connect handlers if new source is not null */
647 if (hook != NULL)
649 priv->hook_widget = g_object_ref (hook);
650 g_signal_connect (priv->hook_widget, "key-press-event",
651 G_CALLBACK (live_search_key_press_event_cb),
652 self);
653 g_signal_connect (priv->hook_widget, "destroy",
654 G_CALLBACK (live_search_hook_widget_destroy_cb),
655 self);
659 const gchar *
660 empathy_live_search_get_text (EmpathyLiveSearch *self)
662 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
664 g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), NULL);
666 return gtk_entry_get_text (GTK_ENTRY (priv->search_entry));
669 void
670 empathy_live_search_set_text (EmpathyLiveSearch *self,
671 const gchar *text)
673 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
675 g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self));
676 g_return_if_fail (text != NULL);
678 gtk_entry_set_text (GTK_ENTRY (priv->search_entry), text);
682 * empathy_live_search_match:
683 * @self: a #EmpathyLiveSearch
684 * @string: a string where to search, must be valid UTF-8.
686 * Search if one of the words in @string string starts with the current text
687 * of @self.
689 * Searching for "aba" in "Abasto" will match, searching in "Moraba" will not,
690 * and searching in "A tool (abacus)" will do.
692 * The match is not case-sensitive, and regardless of the accentuation marks.
694 * Returns: %TRUE if a match is found, %FALSE otherwise.
697 gboolean
698 empathy_live_search_match (EmpathyLiveSearch *self,
699 const gchar *string)
701 EmpathyLiveSearchPriv *priv;
703 g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), FALSE);
705 priv = GET_PRIV (self);
707 return empathy_live_search_match_words (string, priv->stripped_words);
710 gboolean
711 empathy_live_search_match_string (const gchar *string,
712 const gchar *prefix)
714 GPtrArray *words;
715 gboolean match;
717 words = empathy_live_search_strip_utf8_string (prefix);
718 match = empathy_live_search_match_words (string, words);
719 if (words != NULL)
720 g_ptr_array_unref (words);
722 return match;
725 GPtrArray *
726 empathy_live_search_get_words (EmpathyLiveSearch *self)
728 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
730 return priv->stripped_words;