aboutsummaryrefslogblamecommitdiffstats
path: root/libempathy-gtk/empathy-live-search.c
blob: 8a627ead14f5d471427c890b029b0acc11b2be0b (plain) (tree)
1
2
3

                                    
                                             















                                                                             

                                                               










                                     
                                









                                                                     
                            








                        


           
         




                                  























                                                                     
                                    
                                  
                                  














                                                             
                  

                                       
                          




                             

                                                        
                          
 


                                                                    
 


                           
                      
 
                                                   

                      


                                          



















                                                              







                                                            
               







                                           



                                                                               

                                                             

      












                                                            
                           


                                        
 




                                                  
                                            




































                                                                            

                                                                              
                                                            
                  
 
















                                                                        






                                               






















                                                              
                                      
                                         


















                                                                         

                                             














































































                                                                             



                                                                        

















                                                              







                                               



                                                   


                                                                      
 







































                                                                              

                                                        













































































                                                                    

                                              










                                                        
                                     













                                                                    
                                                   


                                                                        

                                                     
















                                                                         















                                                                         

























                                                                              
                                                                





                                                      
                   

                 



                                                  



               
/*
 * 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
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * 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 <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

#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)

#define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyLiveSearch)

typedef struct
{
  GtkWidget *search_entry;
  GtkWidget *hook_widget;

  GPtrArray *stripped_words;
} EmpathyLiveSearchPriv;

enum
{
  PROP_0,
  PROP_HOOK_WIDGET,
  PROP_TEXT
};

enum
{
  ACTIVATE,
  KEYNAV,
  LAST_SIGNAL
};

static guint signals[LAST_SIGNAL];

static void live_search_hook_widget_destroy_cb (GtkObject *object,
    gpointer user_data);

/**
 * stripped_char:
 *
 * Returns a stripped version of @ch, removing any case, accentuation
 * mark, or any special mark on it.
 **/
static gunichar
stripped_char (gunichar ch)
{
  gunichar retval = 0;
  GUnicodeType utype;
  gunichar *decomp;
  gsize dlen;

  utype = g_unichar_type (ch);

  switch (utype)
    {
    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;
    default:
      ch = g_unichar_tolower (ch);
      decomp = g_unicode_canonical_decomposition (ch, &dlen);
      if (decomp != NULL)
        {
          retval = decomp[0];
          g_free (decomp);
        }
    }

  return retval;
}

static GPtrArray *
strip_utf8_string (const gchar *string)
{
  GPtrArray *words = NULL;
  const gchar *p;

  if (EMP_STR_EMPTY (string))
    return NULL;

  for (p = string; *p != '\0'; p = g_utf8_next_char (p))
    {
      GString *str = NULL;

      /* 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);

      /* Strip this word */
      while (*p != '\0')
        {
          gunichar sc;

          sc = stripped_char (g_utf8_get_char (p));
          if (sc != 0)
            {
              if (!g_unichar_isalnum (sc))
                break;

              if (str == NULL)
                str = g_string_new (NULL);
              g_string_append_unichar (str, sc);
            }

          p = g_utf8_next_char (p);
        }

      if (str != NULL)
        {
          if (words == NULL)
            words = g_ptr_array_new_with_free_func (g_free);
          g_ptr_array_add (words, g_string_free (str, FALSE));
        }

      if (*p == '\0')
        break;
    }

  return words;
}

static gboolean
live_search_entry_key_pressed_cb (GtkEntry *entry,
    GdkEventKey *event,
    gpointer user_data)
{
  EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
  gboolean ret;

  /* if esc key pressed, hide the search */
  if (event->keyval == GDK_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)
     {
       g_signal_emit (self, signals[KEYNAV], 0, event, &ret);
       return ret;
     }

  return FALSE;
}

static void
live_search_text_changed (GtkEntry *entry,
    gpointer user_data)
{
  EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
  EmpathyLiveSearchPriv *priv = GET_PRIV (self);
  const gchar *text;

  text = gtk_entry_get_text (entry);

  if (EMP_STR_EMPTY (text))
    gtk_widget_hide (GTK_WIDGET (self));
  else
    gtk_widget_show (GTK_WIDGET (self));

  if (priv->stripped_words != NULL)
    g_ptr_array_unref (priv->stripped_words);

  priv->stripped_words = strip_utf8_string (text);

  g_object_notify (G_OBJECT (self), "text");
}

static void
live_search_close_pressed (GtkEntry *entry,
    GtkEntryIconPosition icon_pos,
    GdkEvent *event,
    gpointer user_data)
{
  EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);

  gtk_widget_hide (GTK_WIDGET (self));
}

static gboolean
live_search_key_press_event_cb (GtkWidget *widget,
    GdkEventKey *event,
    gpointer user_data)
{
  EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);
  EmpathyLiveSearchPriv *priv = GET_PRIV (self);
  GdkEvent *new_event;
  gboolean ret;

  /* 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)
    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)
    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)
     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))
    {
      gtk_widget_grab_focus (priv->search_entry);
      gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
    }

  /* forward the event to the search entry */
  new_event = gdk_event_copy ((GdkEvent *) event);
  ret = gtk_widget_event (priv->search_entry, new_event);
  gdk_event_free (new_event);

  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)
{
  EmpathyLiveSearchPriv *priv = GET_PRIV (self);

  /* remove old handlers if old source was not null */
  if (priv->hook_widget != NULL)
    {
      g_signal_handlers_disconnect_by_func (priv->hook_widget,
          live_search_key_press_event_cb, self);
      g_signal_handlers_disconnect_by_func (priv->hook_widget,
          live_search_hook_widget_destroy_cb, self);
      g_object_unref (priv->hook_widget);
      priv->hook_widget = NULL;
    }
}

static void
live_search_hook_widget_destroy_cb (GtkObject *object,
    gpointer user_data)
{
  EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (user_data);

  /* unref the hook widget and hide search */
  gtk_widget_hide (GTK_WIDGET (self));
  live_search_release_hook_widget (self);
}

static void
live_search_dispose (GObject *obj)
{
  EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);

  live_search_release_hook_widget (self);

  if (G_OBJECT_CLASS (empathy_live_search_parent_class)->dispose != NULL)
    G_OBJECT_CLASS (empathy_live_search_parent_class)->dispose (obj);
}

static void
live_search_finalize (GObject *obj)
{
  EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (obj);
  EmpathyLiveSearchPriv *priv = GET_PRIV (self);

  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);
}

static void
live_search_get_property (GObject *object,
    guint param_id,
    GValue *value,
    GParamSpec *pspec)
{
  EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);

  switch (param_id)
    {
    case PROP_HOOK_WIDGET:
      g_value_set_object (value, empathy_live_search_get_hook_widget (self));
      break;
    case PROP_TEXT:
      g_value_set_string (value, empathy_live_search_get_text (self));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
      break;
    }
}

static void
live_search_set_property (GObject *object,
    guint param_id,
    const GValue *value,
    GParamSpec *pspec)
{
  EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (object);

  switch (param_id) {
  case PROP_HOOK_WIDGET:
    empathy_live_search_set_hook_widget (self, g_value_get_object (value));
    break;
  case PROP_TEXT:
    empathy_live_search_set_text (self, g_value_get_string (value));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
    break;
  };
}

static void
live_search_hide (GtkWidget *widget)
{
  EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
  EmpathyLiveSearchPriv *priv = GET_PRIV (self);

  GTK_WIDGET_CLASS (empathy_live_search_parent_class)->hide (widget);

  gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
  gtk_widget_grab_focus (priv->hook_widget);
}

static void
live_search_show (GtkWidget *widget)
{
  EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
  EmpathyLiveSearchPriv *priv = GET_PRIV (self);

  if (!gtk_widget_has_focus (priv->search_entry))
    gtk_widget_grab_focus (priv->search_entry);

  GTK_WIDGET_CLASS (empathy_live_search_parent_class)->show (widget);
}

static void
live_search_grab_focus (GtkWidget *widget)
{
  EmpathyLiveSearch *self = EMPATHY_LIVE_SEARCH (widget);
  EmpathyLiveSearchPriv *priv = GET_PRIV (self);

  if (!gtk_widget_has_focus (priv->search_entry))
    {
      gtk_widget_grab_focus (priv->search_entry);
      gtk_editable_set_position (GTK_EDITABLE (priv->search_entry), -1);
    }
}

static void
empathy_live_search_class_init (EmpathyLiveSearchClass *klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
  GParamSpec *param_spec;

  object_class->finalize = live_search_finalize;
  object_class->dispose = live_search_dispose;
  object_class->get_property = live_search_get_property;
  object_class->set_property = live_search_set_property;

  widget_class->hide = live_search_hide;
  widget_class->show = live_search_show;
  widget_class->grab_focus = live_search_grab_focus;

  signals[ACTIVATE] = g_signal_new ("activate",
      G_TYPE_FROM_CLASS (object_class),
      G_SIGNAL_RUN_LAST,
      0,
      NULL, NULL,
      g_cclosure_marshal_VOID__VOID,
      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,
      _empathy_gtk_marshal_BOOLEAN__BOXED,
      G_TYPE_BOOLEAN, 1, GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);

  param_spec = g_param_spec_object ("hook-widget", "Live Searchs 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,
      param_spec);

  param_spec = g_param_spec_string ("text", "Live Search Text",
      "The text of the live search entry",
      "", G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
  g_object_class_install_property (object_class, PROP_TEXT, param_spec);

  g_type_class_add_private (klass, sizeof (EmpathyLiveSearchPriv));
}

static void
empathy_live_search_init (EmpathyLiveSearch *self)
{
  EmpathyLiveSearchPriv *priv =
    G_TYPE_INSTANCE_GET_PRIVATE ((self), EMPATHY_TYPE_LIVE_SEARCH,
        EmpathyLiveSearchPriv);

  gtk_widget_set_no_show_all (GTK_WIDGET (self), TRUE);

  priv->search_entry = gtk_entry_new ();
  gtk_entry_set_icon_from_stock (GTK_ENTRY (priv->search_entry),
      GTK_ENTRY_ICON_SECONDARY, GTK_STOCK_CLOSE);
  gtk_entry_set_icon_activatable (GTK_ENTRY (priv->search_entry),
      GTK_ENTRY_ICON_SECONDARY, TRUE);
  gtk_entry_set_icon_sensitive (GTK_ENTRY (priv->search_entry),
      GTK_ENTRY_ICON_SECONDARY, TRUE);
  gtk_widget_show (priv->search_entry);

  gtk_box_pack_start (GTK_BOX (self), priv->search_entry, TRUE, TRUE, 0);

  g_signal_connect (priv->search_entry, "icon_release",
      G_CALLBACK (live_search_close_pressed), self);
  g_signal_connect (priv->search_entry, "changed",
      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;

  self->priv = priv;
}

GtkWidget *
empathy_live_search_new (GtkWidget *hook)
{
  g_return_val_if_fail (hook == NULL || GTK_IS_WIDGET (hook), NULL);

  return g_object_new (EMPATHY_TYPE_LIVE_SEARCH,
      "hook-widget", hook,
      NULL);
}

/* public methods */

GtkWidget *
empathy_live_search_get_hook_widget (EmpathyLiveSearch *self)
{
  EmpathyLiveSearchPriv *priv = GET_PRIV (self);

  g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), NULL);

  return priv->hook_widget;
}

void
empathy_live_search_set_hook_widget (EmpathyLiveSearch *self,
    GtkWidget *hook)
{
  EmpathyLiveSearchPriv *priv;

  g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self));
  g_return_if_fail (hook == NULL || GTK_IS_WIDGET (hook));

  priv = GET_PRIV (self);

  /* release the actual widget */
  live_search_release_hook_widget (self);

  /* connect handlers if new source is not null */
  if (hook != NULL)
    {
      priv->hook_widget = g_object_ref (hook);
      g_signal_connect (priv->hook_widget, "key-press-event",
          G_CALLBACK (live_search_key_press_event_cb),
          self);
      g_signal_connect (priv->hook_widget, "destroy",
          G_CALLBACK (live_search_hook_widget_destroy_cb),
          self);
    }
}

const gchar *
empathy_live_search_get_text (EmpathyLiveSearch *self)
{
  EmpathyLiveSearchPriv *priv = GET_PRIV (self);

  g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), NULL);

  return gtk_entry_get_text (GTK_ENTRY (priv->search_entry));
}

void
empathy_live_search_set_text (EmpathyLiveSearch *self,
    const gchar *text)
{
  EmpathyLiveSearchPriv *priv = GET_PRIV (self);

  g_return_if_fail (EMPATHY_IS_LIVE_SEARCH (self));
  g_return_if_fail (text != NULL);

  gtk_entry_set_text (GTK_ENTRY (priv->search_entry), text);
}

static gboolean
live_search_match_prefix (const gchar *string,
    const gchar *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))
    {
      const gchar *prefix_p = prefix;

      /* 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 != g_utf8_get_char (prefix_p))
                break;

              /* The char matched. If it was the last of prefix, stop */
              prefix_p = g_utf8_next_char (prefix_p);
              if (*prefix_p == '\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;
}

static gboolean
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;
}

/**
 * empathy_live_search_match:
 * @self: a #EmpathyLiveSearch
 * @string: a string where to search, must be valid UTF-8.
 *
 * Search if one of the words in @string string starts with the current text
 * of @self.
 *
 * Searching for "aba" in "Abasto" will match, searching in "Moraba" will not,
 * and searching in "A tool (abacus)" will do.
 *
 * The match is not case-sensitive, and regardless of the accentuation marks.
 *
 * Returns: %TRUE if a match is found, %FALSE otherwise.
 *
 **/
gboolean
empathy_live_search_match (EmpathyLiveSearch *self,
    const gchar *string)
{
  EmpathyLiveSearchPriv *priv;

  g_return_val_if_fail (EMPATHY_IS_LIVE_SEARCH (self), FALSE);

  priv = GET_PRIV (self);

  return live_search_match_words (string, priv->stripped_words);
}

gboolean
empathy_live_search_match_string (const gchar *string,
    const gchar *prefix)
{
  GPtrArray *words;
  gboolean match;

  words = strip_utf8_string (prefix);
  match = live_search_match_words (string, words);
  if (words != NULL)
    g_ptr_array_unref (words);

  return match;
}