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"
34 G_DEFINE_TYPE (EmpathyLiveSearch
, empathy_live_search
, GTK_TYPE_HBOX
)
36 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyLiveSearch)
40 GtkWidget
*search_entry
;
41 GtkWidget
*hook_widget
;
43 GPtrArray
*stripped_words
;
44 } EmpathyLiveSearchPriv
;
60 static guint signals
[LAST_SIGNAL
];
62 static void live_search_hook_widget_destroy_cb (GtkWidget
*object
,
68 * Returns a stripped version of @ch, removing any case, accentuation
69 * mark, or any special mark on it.
72 stripped_char (gunichar ch
)
77 utype
= g_unichar_type (ch
);
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
:
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
:
114 ch
= g_unichar_tolower (ch
);
115 g_unichar_fully_decompose (ch
, FALSE
, &retval
, 1);
122 append_word (GPtrArray
**word_array
,
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
));
135 empathy_live_search_strip_utf8_string (const gchar
*string
)
137 GPtrArray
*word_array
= NULL
;
138 GString
*word
= NULL
;
141 if (EMP_STR_EMPTY (string
))
144 for (p
= string
; *p
!= '\0'; p
= g_utf8_next_char (p
))
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
));
154 /* If it is not alpha-num, it is separator between words */
155 if (!g_unichar_isalnum (sc
))
157 append_word (&word_array
, &word
);
161 /* It is alpha-num, append this char to current word, or start new word */
163 word
= g_string_new (NULL
);
164 g_string_append_unichar (word
, sc
);
167 append_word (&word_array
, &word
);
173 live_search_match_prefix (const gchar
*string
,
177 const gchar
*prefix_p
;
178 gboolean next_word
= FALSE
;
180 if (prefix
== NULL
|| prefix
[0] == 0)
183 if (EMP_STR_EMPTY (string
))
187 for (p
= string
; *p
!= '\0'; p
= g_utf8_next_char (p
))
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
));
197 /* If we want to go to next word, ignore alpha-num chars */
198 if (next_word
&& g_unichar_isalnum (sc
))
202 /* Ignore word separators */
203 if (!g_unichar_isalnum (sc
))
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
))
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')
226 empathy_live_search_match_words (const gchar
*string
,
234 for (i
= 0; i
< words
->len
; i
++)
235 if (!live_search_match_prefix (string
, g_ptr_array_index (words
, i
)))
242 fire_key_navigation_sig (EmpathyLiveSearch
*self
,
247 g_signal_emit (self
, signals
[KEYNAV
], 0, event
, &ret
);
252 live_search_entry_key_pressed_cb (GtkEntry
*entry
,
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
));
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
);
288 live_search_text_changed (GtkEntry
*entry
,
291 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (user_data
);
292 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
295 text
= gtk_entry_get_text (entry
);
297 if (EMP_STR_EMPTY (text
))
298 gtk_widget_hide (GTK_WIDGET (self
));
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");
311 live_search_close_pressed (GtkEntry
*entry
,
312 GtkEntryIconPosition icon_pos
,
316 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (user_data
);
318 gtk_widget_hide (GTK_WIDGET (self
));
322 live_search_key_press_event_cb (GtkWidget
*widget
,
326 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (user_data
);
327 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
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
)
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
)
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
)
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
)))
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
)
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
);
387 live_search_entry_activate_cb (GtkEntry
*entry
,
388 EmpathyLiveSearch
*self
)
390 g_signal_emit (self
, signals
[ACTIVATE
], 0);
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
;
411 live_search_hook_widget_destroy_cb (GtkWidget
*object
,
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
);
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
);
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
);
446 live_search_get_property (GObject
*object
,
451 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (object
);
455 case PROP_HOOK_WIDGET
:
456 g_value_set_object (value
, empathy_live_search_get_hook_widget (self
));
459 g_value_set_string (value
, empathy_live_search_get_text (self
));
462 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
468 live_search_set_property (GObject
*object
,
473 EmpathyLiveSearch
*self
= EMPATHY_LIVE_SEARCH (object
);
476 case PROP_HOOK_WIDGET
:
477 empathy_live_search_set_hook_widget (self
, g_value_get_object (value
));
480 empathy_live_search_set_text (self
, g_value_get_string (value
));
483 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
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
499 gtk_widget_hide (widget
);
501 gtk_entry_set_text (GTK_ENTRY (priv
->search_entry
), "");
502 gtk_widget_grab_focus (priv
->hook_widget
);
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
);
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);
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
),
551 g_cclosure_marshal_generic
,
554 signals
[KEYNAV
] = g_signal_new ("key-navigation",
555 G_TYPE_FROM_CLASS (object_class
),
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
,
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
));
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
;
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
,
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
;
633 empathy_live_search_set_hook_widget (EmpathyLiveSearch
*self
,
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 */
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
),
653 g_signal_connect (priv
->hook_widget
, "destroy",
654 G_CALLBACK (live_search_hook_widget_destroy_cb
),
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
));
670 empathy_live_search_set_text (EmpathyLiveSearch
*self
,
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
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.
698 empathy_live_search_match (EmpathyLiveSearch
*self
,
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
);
711 empathy_live_search_match_string (const gchar
*string
,
717 words
= empathy_live_search_strip_utf8_string (prefix
);
718 match
= empathy_live_search_match_words (string
, words
);
720 g_ptr_array_unref (words
);
726 empathy_live_search_get_words (EmpathyLiveSearch
*self
)
728 EmpathyLiveSearchPriv
*priv
= GET_PRIV (self
);
730 return priv
->stripped_words
;