]> git.0d.be Git - empathy.git/blobdiff - libempathy-gtk/empathy-live-search.c
individual_view_drag_end: remove the auto scroll
[empathy.git] / libempathy-gtk / empathy-live-search.c
index c029d0f5b41600f40ede5c4d20623dc1f9dbc377..3dc77998c39d7d24d9f644a5098ea8f0cfe45db7 100644 (file)
@@ -30,7 +30,6 @@
 #include <libempathy/empathy-utils.h>
 
 #include "empathy-live-search.h"
-#include "empathy-gtk-marshal.h"
 
 G_DEFINE_TYPE (EmpathyLiveSearch, empathy_live_search, GTK_TYPE_HBOX)
 
@@ -41,7 +40,7 @@ typedef struct
   GtkWidget *search_entry;
   GtkWidget *hook_widget;
 
-  gunichar *text_stripped;
+  GPtrArray *stripped_words;
 } EmpathyLiveSearchPriv;
 
 enum
@@ -60,7 +59,7 @@ enum
 
 static guint signals[LAST_SIGNAL];
 
-static void live_search_hook_widget_destroy_cb (GtkObject *object,
+static void live_search_hook_widget_destroy_cb (GtkWidget *object,
     gpointer user_data);
 
 /**
@@ -74,8 +73,6 @@ stripped_char (gunichar ch)
 {
   gunichar retval = 0;
   GUnicodeType utype;
-  gunichar *decomp;
-  gsize dlen;
 
   utype = g_unichar_type (ch);
 
@@ -84,46 +81,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;
 }
 
@@ -133,23 +254,32 @@ live_search_entry_key_pressed_cb (GtkEntry *entry,
     gpointer user_data)
 {
   EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
-  gboolean ret;
 
   /* 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_Up || event->keyval == GDK_Down
-      || event->keyval == GDK_Left || event->keyval == GDK_Right)
+  if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down
+      || event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down)
      {
-       g_signal_emit (self, signals[KEYNAV], 0, event, &ret);
-       return ret;
+       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;
 }
 
@@ -168,8 +298,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");
 }
 
@@ -197,22 +330,33 @@ 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 and 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_Up || event->keyval == GDK_Down)
+  /* 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)
      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;
+     }
+
   /* realize the widget if it is not realized yet */
   gtk_widget_realize (priv->search_entry);
   if (!gtk_widget_has_focus (priv->search_entry))
@@ -254,7 +398,7 @@ 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);
@@ -281,7 +425,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);
@@ -331,12 +476,17 @@ 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);
@@ -379,7 +529,7 @@ 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;
 
@@ -388,7 +538,7 @@ empathy_live_search_class_init (EmpathyLiveSearchClass *klass)
       G_SIGNAL_RUN_LAST,
       0,
       NULL, NULL,
-      g_cclosure_marshal_VOID__VOID,
+      g_cclosure_marshal_generic,
       G_TYPE_NONE, 0);
 
   signals[KEYNAV] = g_signal_new ("key-navigation",
@@ -396,10 +546,10 @@ empathy_live_search_class_init (EmpathyLiveSearchClass *klass)
       G_SIGNAL_RUN_LAST,
       0,
       g_signal_accumulator_true_handled, NULL,
-      _empathy_gtk_marshal_BOOLEAN__BOXED,
+      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 Searchs Hook Widget",
+  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,
@@ -518,57 +668,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
@@ -595,20 +694,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;
+}