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>
28 #include <gdk/gdkkeysyms.h>
30 #include <libempathy/empathy-utils.h>
32 #include "empathy-live-search.h"
33 #include "empathy-gtk-marshal.h"
35 G_DEFINE_TYPE (EmpathyLiveSearch, empathy_live_search, GTK_TYPE_HBOX)
37 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyLiveSearch)
41 GtkWidget *search_entry;
42 GtkWidget *hook_widget;
44 gunichar *text_stripped;
45 } EmpathyLiveSearchPriv;
61 static guint signals[LAST_SIGNAL];
63 static void live_search_hook_widget_destroy_cb (GtkObject *object,
69 * Returns a stripped version of @ch, removing any case, accentuation
70 * mark, or any special mark on it.
73 stripped_char (gunichar ch)
80 utype = g_unichar_type (ch);
84 case G_UNICODE_CONTROL:
85 case G_UNICODE_FORMAT:
86 case G_UNICODE_UNASSIGNED:
87 case G_UNICODE_COMBINING_MARK:
91 ch = g_unichar_tolower (ch);
92 decomp = g_unicode_canonical_decomposition (ch, &dlen);
104 strip_utf8_string (const gchar *string)
110 if (EMP_STR_EMPTY (string))
113 ret = g_malloc (sizeof (gunichar) * (strlen (string) + 1));
116 for (p = string; *p != '\0'; p = g_utf8_next_char (p))
120 sc = stripped_char (g_utf8_get_char (p));
131 live_search_entry_key_pressed_cb (GtkEntry *entry,
135 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
138 /* if esc key pressed, hide the search */
139 if (event->keyval == GDK_Escape)
141 gtk_widget_hide (GTK_WIDGET (self));
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)
149 g_signal_emit (self, signals[KEYNAV], 0, event, &ret);
157 live_search_text_changed (GtkEntry *entry,
160 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
161 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
164 text = gtk_entry_get_text (entry);
166 if (EMP_STR_EMPTY (text))
167 gtk_widget_hide (GTK_WIDGET (self));
169 gtk_widget_show (GTK_WIDGET (self));
171 g_free (priv->text_stripped);
172 priv->text_stripped = strip_utf8_string (text);
173 g_object_notify (G_OBJECT (self), "text");
177 live_search_close_pressed (GtkEntry *entry,
178 GtkEntryIconPosition icon_pos,
182 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
184 gtk_widget_hide (GTK_WIDGET (self));
188 live_search_key_press_event_cb (GtkWidget *widget,
192 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
193 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
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)
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)
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)
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))
220 gtk_widget_grab_focus (priv->search_entry);
221 gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
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);
233 live_search_entry_activate_cb (GtkEntry *entry,
234 EmpathyLiveSearch *self)
236 g_signal_emit (self, signals[ACTIVATE], 0);
240 live_search_release_hook_widget (EmpathyLiveSearch *self)
242 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
244 /* remove old handlers if old source was not null */
245 if (priv->hook_widget != NULL)
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;
257 live_search_hook_widget_destroy_cb (GtkObject *object,
260 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
262 /* unref the hook widget and hide search */
263 gtk_widget_hide (GTK_WIDGET (self));
264 live_search_release_hook_widget (self);
268 live_search_dispose (GObject *obj)
270 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);
272 live_search_release_hook_widget (self);
274 if (G_OBJECT_CLASS (empathy_live_search_parent_class)->dispose != NULL)
275 G_OBJECT_CLASS (empathy_live_search_parent_class)->dispose (obj);
279 live_search_finalize (GObject *obj)
281 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);
282 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
284 g_free (priv->text_stripped);
286 if (G_OBJECT_CLASS (empathy_live_search_parent_class)->finalize != NULL)
287 G_OBJECT_CLASS (empathy_live_search_parent_class)->finalize (obj);
291 live_search_get_property (GObject *object,
296 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);
300 case PROP_HOOK_WIDGET:
301 g_value_set_object (value, empathy_live_search_get_hook_widget (self));
304 g_value_set_string (value, empathy_live_search_get_text (self));
307 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
313 live_search_set_property (GObject *object,
318 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);
321 case PROP_HOOK_WIDGET:
322 empathy_live_search_set_hook_widget (self, g_value_get_object (value));
325 empathy_live_search_set_text (self, g_value_get_string (value));
328 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
334 live_search_hide (GtkWidget *widget)
336 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
337 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
339 GTK_WIDGET_CLASS (empathy_live_search_parent_class)->hide (widget);
341 gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
342 gtk_widget_grab_focus (priv->hook_widget);
346 live_search_show (GtkWidget *widget)
348 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
349 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
351 if (!gtk_widget_has_focus (priv->search_entry))
352 gtk_widget_grab_focus (priv->search_entry);
354 GTK_WIDGET_CLASS (empathy_live_search_parent_class)->show (widget);
358 live_search_grab_focus (GtkWidget *widget)
360 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
361 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
363 if (!gtk_widget_has_focus (priv->search_entry))
365 gtk_widget_grab_focus (priv->search_entry);
366 gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
371 empathy_live_search_class_init (EmpathyLiveSearchClass *klass)
373 GObjectClass *object_class = (GObjectClass *) klass;
374 GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
375 GParamSpec *param_spec;
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;
382 widget_class->hide = live_search_hide;
383 widget_class->show = live_search_show;
384 widget_class->grab_focus = live_search_grab_focus;
386 signals[ACTIVATE] = g_signal_new ("activate",
387 G_TYPE_FROM_CLASS (object_class),
391 g_cclosure_marshal_VOID__VOID,
394 signals[KEYNAV] = g_signal_new ("key-navigation",
395 G_TYPE_FROM_CLASS (object_class),
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);
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,
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);
413 g_type_class_add_private (klass, sizeof (EmpathyLiveSearchPriv));
417 empathy_live_search_init (EmpathyLiveSearch *self)
419 EmpathyLiveSearchPriv *priv =
420 G_TYPE_INSTANCE_GET_PRIVATE ((self), EMPATHY_TYPE_LIVE_SEARCH,
421 EmpathyLiveSearchPriv);
423 gtk_widget_set_no_show_all (GTK_WIDGET (self), TRUE);
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);
434 gtk_box_pack_start (GTK_BOX (self), priv->search_entry, TRUE, TRUE, 0);
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);
445 priv->hook_widget = NULL;
451 empathy_live_search_new (GtkWidget *hook)
453 g_return_val_if_fail (hook == NULL || GTK_IS_WIDGET (hook), NULL);
455 return g_object_new (EMPATHY_TYPE_LIVE_SEARCH,
463 empathy_live_search_get_hook_widget (EmpathyLiveSearch *self)
465 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
467 g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), NULL);
469 return priv->hook_widget;
473 empathy_live_search_set_hook_widget (EmpathyLiveSearch *self,
476 EmpathyLiveSearchPriv *priv;
478 g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self));
479 g_return_if_fail (hook == NULL || GTK_IS_WIDGET (hook));
481 priv = GET_PRIV (self);
483 /* release the actual widget */
484 live_search_release_hook_widget (self);
486 /* connect handlers if new source is not null */
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),
493 g_signal_connect (priv->hook_widget, "destroy",
494 G_CALLBACK (live_search_hook_widget_destroy_cb),
500 empathy_live_search_get_text (EmpathyLiveSearch *self)
502 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
504 g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), NULL);
506 return gtk_entry_get_text (GTK_ENTRY (priv->search_entry));
510 empathy_live_search_set_text (EmpathyLiveSearch *self,
513 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
515 g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self));
516 g_return_if_fail (text != NULL);
518 gtk_entry_set_text (GTK_ENTRY (priv->search_entry), text);
522 live_search_match_string (const gchar *string,
523 const gunichar *prefix)
527 if (prefix == NULL || prefix[0] == 0)
530 if (EMP_STR_EMPTY (string))
533 for (p = string; *p != '\0'; p = g_utf8_next_char (p))
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);
541 /* Check if this word match prefix */
546 sc = stripped_char (g_utf8_get_char (p));
549 /* If the char does not match, stop */
553 /* The char matched. If it was the last of prefix, stop */
554 if (prefix[++i] == 0)
558 p = g_utf8_next_char (p);
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);
573 * empathy_live_search_match:
574 * @self: a #EmpathyLiveSearch
575 * @string: a string where to search, must be valid UTF-8.
577 * Search if one of the words in @string string starts with the current text
580 * Searching for "aba" in "Abasto" will match, searching in "Moraba" will not,
581 * and searching in "A tool (abacus)" will do.
583 * The match is not case-sensitive, and regardless of the accentuation marks.
585 * Returns: %TRUE if a match is found, %FALSE otherwise.
589 empathy_live_search_match (EmpathyLiveSearch *self,
592 EmpathyLiveSearchPriv *priv;
594 g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), FALSE);
596 priv = GET_PRIV (self);
598 return live_search_match_string (string, priv->text_stripped);
602 empathy_live_search_match_string (const gchar *string,
608 stripped = strip_utf8_string (prefix);
609 match = live_search_match_string (string, stripped);