]> git.0d.be Git - empathy.git/blobdiff - libempathy-gtk/empathy-live-search.c
Use a flat namespace for internal includes
[empathy.git] / libempathy-gtk / empathy-live-search.c
index b1215bfd379be4f134a2e67c54c6391fa5fa0f47..3047612b03c7764fcaa767c9ee37ae289b3b2a82 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2010 Collabora Ltd.
+ * Copyright (C) 2007-2010 Nokia Corporation.
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * Authors: Felix Kaser <felix.kaser@collabora.co.uk>
+ *          Xavier Claessens <xavier.claessens@collabora.co.uk>
+ *          Claudio Saavedra <csaavedra@igalia.com>
  */
 
-#include <config.h>
-#include <string.h>
+#include "config.h"
 
-#include <gtk/gtk.h>
-#include <gdk/gdkkeysyms.h>
-
-#include <libempathy/empathy-utils.h>
+#include "empathy-utils.h"
 
 #include "empathy-live-search.h"
 
@@ -37,7 +36,7 @@ typedef struct
   GtkWidget *search_entry;
   GtkWidget *hook_widget;
 
-  gunichar *text_stripped;
+  GPtrArray *stripped_words;
 } EmpathyLiveSearchPriv;
 
 enum
@@ -47,7 +46,16 @@ enum
   PROP_TEXT
 };
 
-static void live_search_hook_widget_destroy_cb (GtkObject *object,
+enum
+{
+  ACTIVATE,
+  KEYNAV,
+  LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+static void live_search_hook_widget_destroy_cb (GtkWidget *object,
     gpointer user_data);
 
 /**
@@ -61,8 +69,6 @@ stripped_char (gunichar ch)
 {
   gunichar retval = 0;
   GUnicodeType utype;
-  gunichar *decomp;
-  gsize dlen;
 
   utype = g_unichar_type (ch);
 
@@ -71,46 +77,170 @@ stripped_char (gunichar ch)
     case G_UNICODE_CONTROL:
     case G_UNICODE_FORMAT:
     case G_UNICODE_UNASSIGNED:
+    case G_UNICODE_NON_SPACING_MARK:
     case G_UNICODE_COMBINING_MARK:
+    case G_UNICODE_ENCLOSING_MARK:
       /* Ignore those */
       break;
+    case G_UNICODE_PRIVATE_USE:
+    case G_UNICODE_SURROGATE:
+    case G_UNICODE_LOWERCASE_LETTER:
+    case G_UNICODE_MODIFIER_LETTER:
+    case G_UNICODE_OTHER_LETTER:
+    case G_UNICODE_TITLECASE_LETTER:
+    case G_UNICODE_UPPERCASE_LETTER:
+    case G_UNICODE_DECIMAL_NUMBER:
+    case G_UNICODE_LETTER_NUMBER:
+    case G_UNICODE_OTHER_NUMBER:
+    case G_UNICODE_CONNECT_PUNCTUATION:
+    case G_UNICODE_DASH_PUNCTUATION:
+    case G_UNICODE_CLOSE_PUNCTUATION:
+    case G_UNICODE_FINAL_PUNCTUATION:
+    case G_UNICODE_INITIAL_PUNCTUATION:
+    case G_UNICODE_OTHER_PUNCTUATION:
+    case G_UNICODE_OPEN_PUNCTUATION:
+    case G_UNICODE_CURRENCY_SYMBOL:
+    case G_UNICODE_MODIFIER_SYMBOL:
+    case G_UNICODE_MATH_SYMBOL:
+    case G_UNICODE_OTHER_SYMBOL:
+    case G_UNICODE_LINE_SEPARATOR:
+    case G_UNICODE_PARAGRAPH_SEPARATOR:
+    case G_UNICODE_SPACE_SEPARATOR:
     default:
       ch = g_unichar_tolower (ch);
-      decomp = g_unicode_canonical_decomposition (ch, &dlen);
-      if (decomp != NULL)
-        {
-          retval = decomp[0];
-          g_free (decomp);
-        }
+      g_unichar_fully_decompose (ch, FALSE, &retval, 1);
     }
 
   return retval;
 }
 
-static gunichar *
-strip_utf8_string (const gchar *string)
+static void
+append_word (GPtrArray **word_array,
+    GString **word)
+{
+  if (*word != NULL)
+    {
+      if (*word_array == NULL)
+        *word_array = g_ptr_array_new_with_free_func (g_free);
+      g_ptr_array_add (*word_array, g_string_free (*word, FALSE));
+      *word = NULL;
+    }
+}
+
+GPtrArray *
+empathy_live_search_strip_utf8_string (const gchar *string)
 {
-  gunichar *ret;
-  gint ret_len;
+  GPtrArray *word_array = NULL;
+  GString *word = NULL;
   const gchar *p;
 
   if (EMP_STR_EMPTY (string))
     return NULL;
 
-  ret = g_malloc (sizeof (gunichar) * (strlen (string) + 1));
-  ret_len = 0;
+  for (p = string; *p != '\0'; p = g_utf8_next_char (p))
+    {
+      gunichar sc;
+
+      /* Make the char lower-case, remove its accentuation marks, and ignore it
+       * if it is just unicode marks */
+      sc = stripped_char (g_utf8_get_char (p));
+      if (sc == 0)
+        continue;
+
+      /* If it is not alpha-num, it is separator between words */
+      if (!g_unichar_isalnum (sc))
+        {
+          append_word (&word_array, &word);
+          continue;
+        }
+
+      /* It is alpha-num, append this char to current word, or start new word */
+      if (word == NULL)
+        word = g_string_new (NULL);
+      g_string_append_unichar (word, sc);
+    }
+
+  append_word (&word_array, &word);
+
+  return word_array;
+}
+
+static gboolean
+live_search_match_prefix (const gchar *string,
+    const gchar *prefix)
+{
+  const gchar *p;
+  const gchar *prefix_p;
+  gboolean next_word = FALSE;
+
+  if (prefix == NULL || prefix[0] == 0)
+    return TRUE;
 
+  if (EMP_STR_EMPTY (string))
+    return FALSE;
+
+  prefix_p = prefix;
   for (p = string; *p != '\0'; p = g_utf8_next_char (p))
     {
       gunichar sc;
 
+      /* Make the char lower-case, remove its accentuation marks, and ignore it
+       * if it is just unicode marks */
       sc = stripped_char (g_utf8_get_char (p));
-      if (sc != 0)
-        ret[ret_len++] = sc;
+      if (sc == 0)
+        continue;
+
+      /* If we want to go to next word, ignore alpha-num chars */
+      if (next_word && g_unichar_isalnum (sc))
+        continue;
+      next_word = FALSE;
+
+      /* Ignore word separators */
+      if (!g_unichar_isalnum (sc))
+        continue;
+
+      /* If this char does not match prefix_p, go to next word and start again
+       * from the beginning of prefix */
+      if (sc != g_utf8_get_char (prefix_p))
+        {
+          next_word = TRUE;
+          prefix_p = prefix;
+          continue;
+        }
+
+      /* prefix_p match, verify to next char. If this was the last of prefix,
+       * it means it completely machted and we are done. */
+      prefix_p = g_utf8_next_char (prefix_p);
+      if (*prefix_p == '\0')
+        return TRUE;
     }
 
-  ret[ret_len] = 0;
+  return FALSE;
+}
+
+gboolean
+empathy_live_search_match_words (const gchar *string,
+    GPtrArray *words)
+{
+  guint i;
+
+  if (words == NULL)
+    return TRUE;
+
+  for (i = 0; i < words->len; i++)
+    if (!live_search_match_prefix (string, g_ptr_array_index (words, i)))
+      return FALSE;
 
+  return TRUE;
+}
+
+static gboolean
+fire_key_navigation_sig (EmpathyLiveSearch *self,
+    GdkEventKey *event)
+{
+  gboolean ret;
+
+  g_signal_emit (self, signals[KEYNAV], 0, event, &ret);
   return ret;
 }
 
@@ -122,12 +252,31 @@ live_search_entry_key_pressed_cb (GtkEntry *entry,
   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
 
   /* if esc key pressed, hide the search */
-  if (event->keyval == GDK_Escape)
+  if (event->keyval == GDK_KEY_Escape)
     {
       gtk_widget_hide (GTK_WIDGET (self));
       return TRUE;
     }
 
+  /* emit key navigation signal, so other widgets can respond to it properly */
+  if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down
+      || event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down
+      || event->keyval == GDK_KEY_Menu)
+     {
+       return fire_key_navigation_sig (self, event);
+     }
+
+  if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End ||
+      event->keyval == GDK_KEY_space)
+    {
+      /* If the live search is visible, the entry should catch the Home/End
+       * and space events */
+      if (!gtk_widget_get_visible (GTK_WIDGET (self)))
+        {
+          return fire_key_navigation_sig (self, event);
+        }
+    }
+
   return FALSE;
 }
 
@@ -146,8 +295,11 @@ live_search_text_changed (GtkEntry *entry,
   else
     gtk_widget_show (GTK_WIDGET (self));
 
-  g_free (priv->text_stripped);
-  priv->text_stripped = strip_utf8_string (text);
+  if (priv->stripped_words != NULL)
+    g_ptr_array_unref (priv->stripped_words);
+
+  priv->stripped_words = empathy_live_search_strip_utf8_string (text);
+
   g_object_notify (G_OBJECT (self), "text");
 }
 
@@ -175,17 +327,42 @@ live_search_key_press_event_cb (GtkWidget *widget,
   /* dont forward this event to the entry, else the event is consumed by the
    * entry and does not close the window */
   if (!gtk_widget_get_visible (GTK_WIDGET (self)) &&
-      event->keyval == GDK_Escape)
+      event->keyval == GDK_KEY_Escape)
     return FALSE;
 
   /* do not show the search if CTRL and/or ALT are pressed with a key
    * this is needed, because otherwise the CTRL + F accel would not work,
    * because the entry consumes it */
   if (event->state & (GDK_MOD1_MASK | GDK_CONTROL_MASK) ||
-      event->keyval == GDK_Control_L ||
-      event->keyval == GDK_Control_R)
+      event->keyval == GDK_KEY_Control_L ||
+      event->keyval == GDK_KEY_Control_R)
     return FALSE;
 
+  /* dont forward the up/down and Page Up/Down arrow keys to the entry,
+   * they are needed for navigation in the treeview and are not needed in
+   * the search entry */
+   if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down ||
+       event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down ||
+       event->keyval == GDK_KEY_Menu)
+     return FALSE;
+
+   if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End ||
+       event->keyval == GDK_KEY_space)
+     {
+       /* Home/End and space keys have to be forwarded to the entry only if
+        * the live search is visible (to move the cursor inside the entry). */
+       if (!gtk_widget_get_visible (GTK_WIDGET (self)))
+         return FALSE;
+     }
+
+   /* Don't forward shift keys events as focusing the search entry would
+    * cancel an in-progress editing on a cell renderer (like when renaming a
+    * group). There is no point focusing it anyway as we don't display the
+    * search entry when only a shift key is pressed. */
+   if (event->keyval == GDK_KEY_Shift_L ||
+       event->keyval == GDK_KEY_Shift_R)
+       return FALSE;
+
   /* realize the widget if it is not realized yet */
   gtk_widget_realize (priv->search_entry);
   if (!gtk_widget_has_focus (priv->search_entry))
@@ -202,6 +379,13 @@ live_search_key_press_event_cb (GtkWidget *widget,
   return ret;
 }
 
+static void
+live_search_entry_activate_cb (GtkEntry *entry,
+    EmpathyLiveSearch *self)
+{
+  g_signal_emit (self, signals[ACTIVATE], 0);
+}
+
 static void
 live_search_release_hook_widget (EmpathyLiveSearch *self)
 {
@@ -220,14 +404,14 @@ live_search_release_hook_widget (EmpathyLiveSearch *self)
 }
 
 static void
-live_search_hook_widget_destroy_cb (GtkObject *object,
+live_search_hook_widget_destroy_cb (GtkWidget *object,
     gpointer user_data)
 {
   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
 
   /* unref the hook widget and hide search */
-  live_search_release_hook_widget (self);
   gtk_widget_hide (GTK_WIDGET (self));
+  live_search_release_hook_widget (self);
 }
 
 static void
@@ -247,7 +431,8 @@ live_search_finalize (GObject *obj)
   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);
   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
 
-  g_free (priv->text_stripped);
+  if (priv->stripped_words != NULL)
+    g_ptr_array_unref (priv->stripped_words);
 
   if (G_OBJECT_CLASS (empathy_live_search_parent_class)->finalize != NULL)
     G_OBJECT_CLASS (empathy_live_search_parent_class)->finalize (obj);
@@ -297,15 +482,22 @@ live_search_set_property (GObject *object,
 }
 
 static void
-live_search_hide (GtkWidget *widget)
+live_search_unmap (GtkWidget *widget)
 {
   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
 
-  GTK_WIDGET_CLASS (empathy_live_search_parent_class)->hide (widget);
+  GTK_WIDGET_CLASS (empathy_live_search_parent_class)->unmap (widget);
+
+  /* unmap can happen if a parent gets hidden, in that case we want to hide
+   * the live search as well, so when it gets mapped again, the live search
+   * won't be shown. */
+  gtk_widget_hide (widget);
 
   gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
-  gtk_widget_grab_focus (priv->hook_widget);
+
+  if (priv->hook_widget != NULL)
+    gtk_widget_grab_focus (priv->hook_widget);
 }
 
 static void
@@ -327,7 +519,10 @@ live_search_grab_focus (GtkWidget *widget)
   EmpathyLiveSearchPriv *priv = GET_PRIV (self);
 
   if (!gtk_widget_has_focus (priv->search_entry))
-    gtk_widget_grab_focus (priv->search_entry);
+    {
+      gtk_widget_grab_focus (priv->search_entry);
+      gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
+    }
 }
 
 static void
@@ -342,11 +537,27 @@ empathy_live_search_class_init (EmpathyLiveSearchClass *klass)
   object_class->get_property = live_search_get_property;
   object_class->set_property = live_search_set_property;
 
-  widget_class->hide = live_search_hide;
+  widget_class->unmap = live_search_unmap;
   widget_class->show = live_search_show;
   widget_class->grab_focus = live_search_grab_focus;
 
-  param_spec = g_param_spec_object ("hook-widget", "Live Searchs Hook Widget",
+  signals[ACTIVATE] = g_signal_new ("activate",
+      G_TYPE_FROM_CLASS (object_class),
+      G_SIGNAL_RUN_LAST,
+      0,
+      NULL, NULL,
+      g_cclosure_marshal_generic,
+      G_TYPE_NONE, 0);
+
+  signals[KEYNAV] = g_signal_new ("key-navigation",
+      G_TYPE_FROM_CLASS (object_class),
+      G_SIGNAL_RUN_LAST,
+      0,
+      g_signal_accumulator_true_handled, NULL,
+      g_cclosure_marshal_generic,
+      G_TYPE_BOOLEAN, 1, GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+  param_spec = g_param_spec_object ("hook-widget", "Live Search Hook Widget",
       "The live search catches key-press-events on this widget",
       GTK_TYPE_WIDGET, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
   g_object_class_install_property (object_class, PROP_HOOK_WIDGET,
@@ -386,6 +597,8 @@ empathy_live_search_init (EmpathyLiveSearch *self)
       G_CALLBACK (live_search_text_changed), self);
   g_signal_connect (priv->search_entry, "key-press-event",
       G_CALLBACK (live_search_entry_key_pressed_cb), self);
+  g_signal_connect (priv->search_entry, "activate",
+      G_CALLBACK (live_search_entry_activate_cb), self);
 
   priv->hook_widget = NULL;
 
@@ -463,57 +676,6 @@ empathy_live_search_set_text (EmpathyLiveSearch *self,
   gtk_entry_set_text (GTK_ENTRY (priv->search_entry), text);
 }
 
-static gboolean
-live_search_match_string (const gchar *string,
-    const gunichar *prefix)
-{
-  const gchar *p;
-
-  if (prefix == NULL || prefix[0] == 0)
-    return TRUE;
-
-  if (EMP_STR_EMPTY (string))
-    return FALSE;
-
-  for (p = string; *p != '\0'; p = g_utf8_next_char (p))
-    {
-      guint i = 0;
-
-      /* Search the start of the word (skip non alpha-num chars) */
-      while (*p != '\0' && !g_unichar_isalnum (g_utf8_get_char (p)))
-        p = g_utf8_next_char (p);
-
-      /* Check if this word match prefix */
-      while (*p != '\0')
-        {
-          gunichar sc;
-
-          sc = stripped_char (g_utf8_get_char (p));
-          if (sc != 0)
-            {
-              /* If the char does not match, stop */
-              if (sc != prefix[i])
-                break;
-
-              /* The char matched. If it was the last of prefix, stop */
-              if (prefix[++i] == 0)
-                return TRUE;
-            }
-
-          p = g_utf8_next_char (p);
-        }
-
-      /* This word didn't match, go to next one (skip alpha-num chars) */
-      while (*p != '\0' && g_unichar_isalnum (g_utf8_get_char (p)))
-        p = g_utf8_next_char (p);
-
-      if (*p == '\0')
-        break;
-    }
-
-  return FALSE;
-}
-
 /**
  * empathy_live_search_match:
  * @self: a #EmpathyLiveSearch
@@ -540,20 +702,28 @@ empathy_live_search_match (EmpathyLiveSearch *self,
 
   priv = GET_PRIV (self);
 
-  return live_search_match_string (string, priv->text_stripped);
+  return empathy_live_search_match_words (string, priv->stripped_words);
 }
 
 gboolean
 empathy_live_search_match_string (const gchar *string,
     const gchar *prefix)
 {
-  gunichar *stripped;
+  GPtrArray *words;
   gboolean match;
 
-  stripped = strip_utf8_string (prefix);
-  match = live_search_match_string (string, stripped);
-  g_free (stripped);
+  words = empathy_live_search_strip_utf8_string (prefix);
+  match = empathy_live_search_match_words (string, words);
+  if (words != NULL)
+    g_ptr_array_unref (words);
 
   return match;
 }
 
+GPtrArray *
+empathy_live_search_get_words (EmpathyLiveSearch *self)
+{
+  EmpathyLiveSearchPriv *priv = GET_PRIV (self);
+
+  return priv->stripped_words;
+}