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 GPtrArray
*stripped_words
;
45 } EmpathyLiveSearchPriv
;
61 static guint signals
[LAST_SIGNAL
];
63 static void live_search_hook_widget_destroy_cb (GtkWidget
*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_NON_SPACING_MARK
:
88 case G_UNICODE_COMBINING_MARK
:
89 case G_UNICODE_ENCLOSING_MARK
:
92 case G_UNICODE_PRIVATE_USE
:
93 case G_UNICODE_SURROGATE
:
94 case G_UNICODE_LOWERCASE_LETTER
:
95 case G_UNICODE_MODIFIER_LETTER
:
96 case G_UNICODE_OTHER_LETTER
:
97 case G_UNICODE_TITLECASE_LETTER
:
98 case G_UNICODE_UPPERCASE_LETTER
:
99 case G_UNICODE_DECIMAL_NUMBER
:
100 case G_UNICODE_LETTER_NUMBER
:
101 case G_UNICODE_OTHER_NUMBER
:
102 case G_UNICODE_CONNECT_PUNCTUATION
:
103 case G_UNICODE_DASH_PUNCTUATION
:
104 case G_UNICODE_CLOSE_PUNCTUATION
:
105 case G_UNICODE_FINAL_PUNCTUATION
:
106 case G_UNICODE_INITIAL_PUNCTUATION
:
107 case G_UNICODE_OTHER_PUNCTUATION
:
108 case G_UNICODE_OPEN_PUNCTUATION
:
109 case G_UNICODE_CURRENCY_SYMBOL
:
110 case G_UNICODE_MODIFIER_SYMBOL
:
111 case G_UNICODE_MATH_SYMBOL
:
112 case G_UNICODE_OTHER_SYMBOL
:
113 case G_UNICODE_LINE_SEPARATOR
:
114 case G_UNICODE_PARAGRAPH_SEPARATOR
:
115 case G_UNICODE_SPACE_SEPARATOR
:
117 ch
= g_unichar_tolower (ch
);
118 decomp
= g_unicode_canonical_decomposition (ch
, &dlen
);
130 append_word (GPtrArray
**word_array
,
135 if (*word_array
== NULL
)
136 *word_array
= g_ptr_array_new_with_free_func (g_free
);
137 g_ptr_array_add (*word_array
, g_string_free (*word
, FALSE
));
143 empathy_live_search_strip_utf8_string (const gchar
*string
)
145 GPtrArray
*word_array
= NULL
;
146 GString
*word
= NULL
;
149 if (EMP_STR_EMPTY (string
))
152 for (p
= string
; *p
!= '\0'; p
= g_utf8_next_char (p
))
156 /* Make the char lower-case, remove its accentuation marks, and ignore it
157 * if it is just unicode marks */
158 sc
= stripped_char (g_utf8_get_char (p
));
162 /* If it is not alpha-num, it is separator between words */
163 if (!g_unichar_isalnum (sc
))
165 append_word (&word_array
, &word
);
169 /* It is alpha-num, append this char to current word, or start new word */
171 word
= g_string_new (NULL
);
172 g_string_append_unichar (word
, sc
);
175 append_word (&word_array
, &word
);
181 live_search_match_prefix (const gchar
*string
,
185 const gchar
*prefix_p
;
186 gboolean next_word
= FALSE
;
188 if (prefix
== NULL
|| prefix
[0] == 0)
191 if (EMP_STR_EMPTY (string
))
195 for (p
= string
; *p
!= '\0'; p
= g_utf8_next_char (p
))
199 /* Make the char lower-case, remove its accentuation marks, and ignore it
200 * if it is just unicode marks */
201 sc
= stripped_char (g_utf8_get_char (p
));
205 /* If we want to go to next word, ignore alpha-num chars */
206 if (next_word
&& g_unichar_isalnum (sc
))
210 /* Ignore word separators */
211 if (!g_unichar_isalnum (sc
))
214 /* If this char does not match prefix_p, go to next word and start again
215 * from the beginning of prefix */
216 if (sc
!= g_utf8_get_char (prefix_p
))
223 /* prefix_p match, verify to next char. If this was the last of prefix,
224 * it means it completely machted and we are done. */
225 prefix_p
= g_utf8_next_char (prefix_p
);
226 if (*prefix_p
== '\0')
234 empathy_live_search_match_words (const gchar
*string
,
242 for (i
= 0; i
< words
->len
; i
++)
243 if (!live_search_match_prefix (string
, g_ptr_array_index (words
, i
)))
250 fire_key_navigation_sig (EmpathyLiveSearch
*self
,
255 g_signal_emit (self
, signals
[KEYNAV
], 0, event
, &ret
);
260 live_search_entry_key_pressed_cb (GtkEntry
*entry
,
264 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (user_data
);
266 /* if esc key pressed, hide the search */
267 if (event
->keyval
== GDK_KEY_Escape
)
269 gtk_widget_hide (GTK_WIDGET (self
));
273 /* emit key navigation signal, so other widgets can respond to it properly */
274 if (event
->keyval
== GDK_KEY_Up
|| event
->keyval
== GDK_KEY_Down
275 || event
->keyval
== GDK_KEY_Page_Up
|| event
->keyval
== GDK_KEY_Page_Down
)
277 return fire_key_navigation_sig (self
, event
);
280 if (event
->keyval
== GDK_KEY_Home
|| event
->keyval
== GDK_KEY_End
||
281 event
->keyval
== GDK_KEY_space
)
283 /* If the live search is visible, the entry should catch the Home/End
284 * and space events */
285 if (!gtk_widget_get_visible (GTK_WIDGET (self
)))
287 return fire_key_navigation_sig (self
, event
);
295 live_search_text_changed (GtkEntry
*entry
,
298 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (user_data
);
299 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
302 text
= gtk_entry_get_text (entry
);
304 if (EMP_STR_EMPTY (text
))
305 gtk_widget_hide (GTK_WIDGET (self
));
307 gtk_widget_show (GTK_WIDGET (self
));
309 if (priv
->stripped_words
!= NULL
)
310 g_ptr_array_unref (priv
->stripped_words
);
312 priv
->stripped_words
= empathy_live_search_strip_utf8_string (text
);
314 g_object_notify (G_OBJECT (self
), "text");
318 live_search_close_pressed (GtkEntry
*entry
,
319 GtkEntryIconPosition icon_pos
,
323 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (user_data
);
325 gtk_widget_hide (GTK_WIDGET (self
));
329 live_search_key_press_event_cb (GtkWidget
*widget
,
333 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (user_data
);
334 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
338 /* dont forward this event to the entry, else the event is consumed by the
339 * entry and does not close the window */
340 if (!gtk_widget_get_visible (GTK_WIDGET (self
)) &&
341 event
->keyval
== GDK_KEY_Escape
)
344 /* do not show the search if CTRL and/or ALT are pressed with a key
345 * this is needed, because otherwise the CTRL + F accel would not work,
346 * because the entry consumes it */
347 if (event
->state
& (GDK_MOD1_MASK
| GDK_CONTROL_MASK
) ||
348 event
->keyval
== GDK_KEY_Control_L
||
349 event
->keyval
== GDK_KEY_Control_R
)
352 /* dont forward the up/down and Page Up/Down arrow keys to the entry,
353 * they are needed for navigation in the treeview and are not needed in
354 * the search entry */
355 if (event
->keyval
== GDK_KEY_Up
|| event
->keyval
== GDK_KEY_Down
||
356 event
->keyval
== GDK_KEY_Page_Up
|| event
->keyval
== GDK_KEY_Page_Down
)
359 if (event
->keyval
== GDK_KEY_Home
|| event
->keyval
== GDK_KEY_End
||
360 event
->keyval
== GDK_KEY_space
)
362 /* Home/End and space keys have to be forwarded to the entry only if
363 * the live search is visible (to move the cursor inside the entry). */
364 if (!gtk_widget_get_visible (GTK_WIDGET (self
)))
368 /* realize the widget if it is not realized yet */
369 gtk_widget_realize (priv
->search_entry
);
370 if (!gtk_widget_has_focus (priv
->search_entry
))
372 gtk_widget_grab_focus (priv
->search_entry
);
373 gtk_editable_set_position (GTK_EDITABLE (priv
->search_entry
), -1);
376 /* forward the event to the search entry */
377 new_event
= gdk_event_copy ((GdkEvent
*) event
);
378 ret
= gtk_widget_event (priv
->search_entry
, new_event
);
379 gdk_event_free (new_event
);
385 live_search_entry_activate_cb (GtkEntry
*entry
,
386 EmpathyLiveSearch
*self
)
388 g_signal_emit (self
, signals
[ACTIVATE
], 0);
392 live_search_release_hook_widget (EmpathyLiveSearch
*self
)
394 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
396 /* remove old handlers if old source was not null */
397 if (priv
->hook_widget
!= NULL
)
399 g_signal_handlers_disconnect_by_func (priv
->hook_widget
,
400 live_search_key_press_event_cb
, self
);
401 g_signal_handlers_disconnect_by_func (priv
->hook_widget
,
402 live_search_hook_widget_destroy_cb
, self
);
403 g_object_unref (priv
->hook_widget
);
404 priv
->hook_widget
= NULL
;
409 live_search_hook_widget_destroy_cb (GtkWidget
*object
,
412 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (user_data
);
414 /* unref the hook widget and hide search */
415 gtk_widget_hide (GTK_WIDGET (self
));
416 live_search_release_hook_widget (self
);
420 live_search_dispose (GObject
*obj
)
422 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (obj
);
424 live_search_release_hook_widget (self
);
426 if (G_OBJECT_CLASS (empathy_live_search_parent_class
)->dispose
!= NULL
)
427 G_OBJECT_CLASS (empathy_live_search_parent_class
)->dispose (obj
);
431 live_search_finalize (GObject
*obj
)
433 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (obj
);
434 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
436 if (priv
->stripped_words
!= NULL
)
437 g_ptr_array_unref (priv
->stripped_words
);
439 if (G_OBJECT_CLASS (empathy_live_search_parent_class
)->finalize
!= NULL
)
440 G_OBJECT_CLASS (empathy_live_search_parent_class
)->finalize (obj
);
444 live_search_get_property (GObject
*object
,
449 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (object
);
453 case PROP_HOOK_WIDGET
:
454 g_value_set_object (value
, empathy_live_search_get_hook_widget (self
));
457 g_value_set_string (value
, empathy_live_search_get_text (self
));
460 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
466 live_search_set_property (GObject
*object
,
471 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (object
);
474 case PROP_HOOK_WIDGET
:
475 empathy_live_search_set_hook_widget (self
, g_value_get_object (value
));
478 empathy_live_search_set_text (self
, g_value_get_string (value
));
481 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
487 live_search_unmap (GtkWidget
*widget
)
489 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (widget
);
490 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
492 GTK_WIDGET_CLASS (empathy_live_search_parent_class
)->unmap (widget
);
494 /* unmap can happen if a parent gets hidden, in that case we want to hide
495 * the live search as well, so when it gets mapped again, the live search
497 gtk_widget_hide (widget
);
499 gtk_entry_set_text (GTK_ENTRY (priv
->search_entry
), "");
500 gtk_widget_grab_focus (priv
->hook_widget
);
504 live_search_show (GtkWidget
*widget
)
506 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (widget
);
507 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
509 if (!gtk_widget_has_focus (priv
->search_entry
))
510 gtk_widget_grab_focus (priv
->search_entry
);
512 GTK_WIDGET_CLASS (empathy_live_search_parent_class
)->show (widget
);
516 live_search_grab_focus (GtkWidget
*widget
)
518 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (widget
);
519 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
521 if (!gtk_widget_has_focus (priv
->search_entry
))
523 gtk_widget_grab_focus (priv
->search_entry
);
524 gtk_editable_set_position (GTK_EDITABLE (priv
->search_entry
), -1);
529 empathy_live_search_class_init (EmpathyLiveSearchClass
*klass
)
531 GObjectClass
*object_class
= (GObjectClass
*) klass
;
532 GtkWidgetClass
*widget_class
= (GtkWidgetClass
*) klass
;
533 GParamSpec
*param_spec
;
535 object_class
->finalize
= live_search_finalize
;
536 object_class
->dispose
= live_search_dispose
;
537 object_class
->get_property
= live_search_get_property
;
538 object_class
->set_property
= live_search_set_property
;
540 widget_class
->unmap
= live_search_unmap
;
541 widget_class
->show
= live_search_show
;
542 widget_class
->grab_focus
= live_search_grab_focus
;
544 signals
[ACTIVATE
] = g_signal_new ("activate",
545 G_TYPE_FROM_CLASS (object_class
),
549 g_cclosure_marshal_VOID__VOID
,
552 signals
[KEYNAV
] = g_signal_new ("key-navigation",
553 G_TYPE_FROM_CLASS (object_class
),
556 g_signal_accumulator_true_handled
, NULL
,
557 _empathy_gtk_marshal_BOOLEAN__BOXED
,
558 G_TYPE_BOOLEAN
, 1, GDK_TYPE_EVENT
| G_SIGNAL_TYPE_STATIC_SCOPE
);
560 param_spec
= g_param_spec_object ("hook-widget", "Live Search Hook Widget",
561 "The live search catches key-press-events on this widget",
562 GTK_TYPE_WIDGET
, G_PARAM_READWRITE
| G_PARAM_STATIC_STRINGS
);
563 g_object_class_install_property (object_class
, PROP_HOOK_WIDGET
,
566 param_spec
= g_param_spec_string ("text", "Live Search Text",
567 "The text of the live search entry",
568 "", G_PARAM_READWRITE
| G_PARAM_STATIC_STRINGS
);
569 g_object_class_install_property (object_class
, PROP_TEXT
, param_spec
);
571 g_type_class_add_private (klass
, sizeof (EmpathyLiveSearchPriv
));
575 empathy_live_search_init (EmpathyLiveSearch
*self
)
577 EmpathyLiveSearchPriv
*priv
=
578 G_TYPE_INSTANCE_GET_PRIVATE ((self
), EMPATHY_TYPE_LIVE_SEARCH
,
579 EmpathyLiveSearchPriv
);
581 gtk_widget_set_no_show_all (GTK_WIDGET (self
), TRUE
);
583 priv
->search_entry
= gtk_entry_new ();
584 gtk_entry_set_icon_from_stock (GTK_ENTRY (priv
->search_entry
),
585 GTK_ENTRY_ICON_SECONDARY
, GTK_STOCK_CLOSE
);
586 gtk_entry_set_icon_activatable (GTK_ENTRY (priv
->search_entry
),
587 GTK_ENTRY_ICON_SECONDARY
, TRUE
);
588 gtk_entry_set_icon_sensitive (GTK_ENTRY (priv
->search_entry
),
589 GTK_ENTRY_ICON_SECONDARY
, TRUE
);
590 gtk_widget_show (priv
->search_entry
);
592 gtk_box_pack_start (GTK_BOX (self
), priv
->search_entry
, TRUE
, TRUE
, 0);
594 g_signal_connect (priv
->search_entry
, "icon_release",
595 G_CALLBACK (live_search_close_pressed
), self
);
596 g_signal_connect (priv
->search_entry
, "changed",
597 G_CALLBACK (live_search_text_changed
), self
);
598 g_signal_connect (priv
->search_entry
, "key-press-event",
599 G_CALLBACK (live_search_entry_key_pressed_cb
), self
);
600 g_signal_connect (priv
->search_entry
, "activate",
601 G_CALLBACK (live_search_entry_activate_cb
), self
);
603 priv
->hook_widget
= NULL
;
609 empathy_live_search_new (GtkWidget
*hook
)
611 g_return_val_if_fail (hook
== NULL
|| GTK_IS_WIDGET (hook
), NULL
);
613 return g_object_new (EMPATHY_TYPE_LIVE_SEARCH
,
621 empathy_live_search_get_hook_widget (EmpathyLiveSearch
*self
)
623 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
625 g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self
), NULL
);
627 return priv
->hook_widget
;
631 empathy_live_search_set_hook_widget (EmpathyLiveSearch
*self
,
634 EmpathyLiveSearchPriv
*priv
;
636 g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self
));
637 g_return_if_fail (hook
== NULL
|| GTK_IS_WIDGET (hook
));
639 priv
= GET_PRIV (self
);
641 /* release the actual widget */
642 live_search_release_hook_widget (self
);
644 /* connect handlers if new source is not null */
647 priv
->hook_widget
= g_object_ref (hook
);
648 g_signal_connect (priv
->hook_widget
, "key-press-event",
649 G_CALLBACK (live_search_key_press_event_cb
),
651 g_signal_connect (priv
->hook_widget
, "destroy",
652 G_CALLBACK (live_search_hook_widget_destroy_cb
),
658 empathy_live_search_get_text (EmpathyLiveSearch
*self
)
660 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
662 g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self
), NULL
);
664 return gtk_entry_get_text (GTK_ENTRY (priv
->search_entry
));
668 empathy_live_search_set_text (EmpathyLiveSearch
*self
,
671 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
673 g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self
));
674 g_return_if_fail (text
!= NULL
);
676 gtk_entry_set_text (GTK_ENTRY (priv
->search_entry
), text
);
680 * empathy_live_search_match:
681 * @self: a #EmpathyLiveSearch
682 * @string: a string where to search, must be valid UTF-8.
684 * Search if one of the words in @string string starts with the current text
687 * Searching for "aba" in "Abasto" will match, searching in "Moraba" will not,
688 * and searching in "A tool (abacus)" will do.
690 * The match is not case-sensitive, and regardless of the accentuation marks.
692 * Returns: %TRUE if a match is found, %FALSE otherwise.
696 empathy_live_search_match (EmpathyLiveSearch
*self
,
699 EmpathyLiveSearchPriv
*priv
;
701 g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self
), FALSE
);
703 priv
= GET_PRIV (self
);
705 return empathy_live_search_match_words (string
, priv
->stripped_words
);
709 empathy_live_search_match_string (const gchar
*string
,
715 words
= empathy_live_search_strip_utf8_string (prefix
);
716 match
= empathy_live_search_match_words (string
, words
);
718 g_ptr_array_unref (words
);
724 empathy_live_search_get_words (EmpathyLiveSearch
*self
)
726 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
728 return priv
->stripped_words
;