Clear audio_output when the call ends
[empathy-mirror.git] / libempathy-gtk / empathy-live-search.c
blob422bfcb73094765a0f7b6e61d61445d2a1886e88
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"
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)
39 typedef struct
41 GtkWidget *search_entry;
42 GtkWidget *hook_widget;
44 GPtrArray *stripped_words;
45 } EmpathyLiveSearchPriv;
47 enum
49 PROP_0,
50 PROP_HOOK_WIDGET,
51 PROP_TEXT
54 enum
56 ACTIVATE,
57 KEYNAV,
58 LAST_SIGNAL
61 static guint signals[LAST_SIGNAL];
63 static void live_search_hook_widget_destroy_cb (GtkWidget *object,
64 gpointer user_data);
66 /**
67 * stripped_char:
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)
75 gunichar retval = 0;
76 GUnicodeType utype;
77 gunichar *decomp;
78 gsize dlen;
80 utype = g_unichar_type (ch);
82 switch (utype)
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:
90 /* Ignore those */
91 break;
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:
116 default:
117 ch = g_unichar_tolower (ch);
118 decomp = g_unicode_canonical_decomposition (ch, &dlen);
119 if (decomp != NULL)
121 retval = decomp[0];
122 g_free (decomp);
126 return retval;
129 static void
130 append_word (GPtrArray **word_array,
131 GString **word)
133 if (*word != NULL)
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));
138 *word = NULL;
142 GPtrArray *
143 empathy_live_search_strip_utf8_string (const gchar *string)
145 GPtrArray *word_array = NULL;
146 GString *word = NULL;
147 const gchar *p;
149 if (EMP_STR_EMPTY (string))
150 return NULL;
152 for (p = string; *p != '\0'; p = g_utf8_next_char (p))
154 gunichar sc;
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));
159 if (sc == 0)
160 continue;
162 /* If it is not alpha-num, it is separator between words */
163 if (!g_unichar_isalnum (sc))
165 append_word (&word_array, &word);
166 continue;
169 /* It is alpha-num, append this char to current word, or start new word */
170 if (word == NULL)
171 word = g_string_new (NULL);
172 g_string_append_unichar (word, sc);
175 append_word (&word_array, &word);
177 return word_array;
180 static gboolean
181 live_search_match_prefix (const gchar *string,
182 const gchar *prefix)
184 const gchar *p;
185 const gchar *prefix_p;
186 gboolean next_word = FALSE;
188 if (prefix == NULL || prefix[0] == 0)
189 return TRUE;
191 if (EMP_STR_EMPTY (string))
192 return FALSE;
194 prefix_p = prefix;
195 for (p = string; *p != '\0'; p = g_utf8_next_char (p))
197 gunichar sc;
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));
202 if (sc == 0)
203 continue;
205 /* If we want to go to next word, ignore alpha-num chars */
206 if (next_word && g_unichar_isalnum (sc))
207 continue;
208 next_word = FALSE;
210 /* Ignore word separators */
211 if (!g_unichar_isalnum (sc))
212 continue;
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))
218 next_word = TRUE;
219 prefix_p = prefix;
220 continue;
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')
227 return TRUE;
230 return FALSE;
233 gboolean
234 empathy_live_search_match_words (const gchar *string,
235 GPtrArray *words)
237 guint i;
239 if (words == NULL)
240 return TRUE;
242 for (i = 0; i < words->len; i++)
243 if (!live_search_match_prefix (string, g_ptr_array_index (words, i)))
244 return FALSE;
246 return TRUE;
249 static gboolean
250 fire_key_navigation_sig (EmpathyLiveSearch *self,
251 GdkEventKey *event)
253 gboolean ret;
255 g_signal_emit (self, signals[KEYNAV], 0, event, &ret);
256 return ret;
259 static gboolean
260 live_search_entry_key_pressed_cb (GtkEntry *entry,
261 GdkEventKey *event,
262 gpointer user_data)
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));
270 return TRUE;
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);
291 return FALSE;
294 static void
295 live_search_text_changed (GtkEntry *entry,
296 gpointer user_data)
298 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
299 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
300 const gchar *text;
302 text = gtk_entry_get_text (entry);
304 if (EMP_STR_EMPTY (text))
305 gtk_widget_hide (GTK_WIDGET (self));
306 else
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");
317 static void
318 live_search_close_pressed (GtkEntry *entry,
319 GtkEntryIconPosition icon_pos,
320 GdkEvent *event,
321 gpointer user_data)
323 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
325 gtk_widget_hide (GTK_WIDGET (self));
328 static gboolean
329 live_search_key_press_event_cb (GtkWidget *widget,
330 GdkEventKey *event,
331 gpointer user_data)
333 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
334 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
335 GdkEvent *new_event;
336 gboolean ret;
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)
342 return FALSE;
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)
350 return FALSE;
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)
357 return FALSE;
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)))
365 return FALSE;
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);
381 return ret;
384 static void
385 live_search_entry_activate_cb (GtkEntry *entry,
386 EmpathyLiveSearch *self)
388 g_signal_emit (self, signals[ACTIVATE], 0);
391 static void
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;
408 static void
409 live_search_hook_widget_destroy_cb (GtkWidget *object,
410 gpointer user_data)
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);
419 static void
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);
430 static void
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);
443 static void
444 live_search_get_property (GObject *object,
445 guint param_id,
446 GValue *value,
447 GParamSpec *pspec)
449 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);
451 switch (param_id)
453 case PROP_HOOK_WIDGET:
454 g_value_set_object (value, empathy_live_search_get_hook_widget (self));
455 break;
456 case PROP_TEXT:
457 g_value_set_string (value, empathy_live_search_get_text (self));
458 break;
459 default:
460 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
461 break;
465 static void
466 live_search_set_property (GObject *object,
467 guint param_id,
468 const GValue *value,
469 GParamSpec *pspec)
471 EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);
473 switch (param_id) {
474 case PROP_HOOK_WIDGET:
475 empathy_live_search_set_hook_widget (self, g_value_get_object (value));
476 break;
477 case PROP_TEXT:
478 empathy_live_search_set_text (self, g_value_get_string (value));
479 break;
480 default:
481 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
482 break;
486 static void
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
496 * won't be shown. */
497 gtk_widget_hide (widget);
499 gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
500 gtk_widget_grab_focus (priv->hook_widget);
503 static void
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);
515 static void
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);
528 static void
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),
546 G_SIGNAL_RUN_LAST,
548 NULL, NULL,
549 g_cclosure_marshal_VOID__VOID,
550 G_TYPE_NONE, 0);
552 signals[KEYNAV] = g_signal_new ("key-navigation",
553 G_TYPE_FROM_CLASS (object_class),
554 G_SIGNAL_RUN_LAST,
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,
564 param_spec);
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));
574 static void
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;
605 self->priv = priv;
608 GtkWidget *
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,
614 "hook-widget", hook,
615 NULL);
618 /* public methods */
620 GtkWidget *
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;
630 void
631 empathy_live_search_set_hook_widget (EmpathyLiveSearch *self,
632 GtkWidget *hook)
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 */
645 if (hook != 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),
650 self);
651 g_signal_connect (priv->hook_widget, "destroy",
652 G_CALLBACK (live_search_hook_widget_destroy_cb),
653 self);
657 const gchar *
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));
667 void
668 empathy_live_search_set_text (EmpathyLiveSearch *self,
669 const gchar *text)
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
685 * of @self.
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.
695 gboolean
696 empathy_live_search_match (EmpathyLiveSearch *self,
697 const gchar *string)
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);
708 gboolean
709 empathy_live_search_match_string (const gchar *string,
710 const gchar *prefix)
712 GPtrArray *words;
713 gboolean match;
715 words = empathy_live_search_strip_utf8_string (prefix);
716 match = empathy_live_search_match_words (string, words);
717 if (words != NULL)
718 g_ptr_array_unref (words);
720 return match;
723 GPtrArray *
724 empathy_live_search_get_words (EmpathyLiveSearch *self)
726 EmpathyLiveSearchPriv *priv = GET_PRIV (self);
728 return priv->stripped_words;