/*
 * Copyright (C) 2010 Intel, Inc
 * Copyright (C) 2014 Red Hat, Inc
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Thomas Wood <thomas.wood@intel.com>
 *          Rodrigo Moya <rodrigo@gnome.org>
 *          Christophe Fergeau <cfergeau@redhat.com>
 */

#include <config.h>

#include <glib/gi18n.h>

#include "keyboard-shortcuts.h"
#include "cc-keyboard-item.h"
#include "cc-keyboard-option.h"
#include "wm-common.h"

#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif

#define BINDINGS_SCHEMA "org.gnome.settings-daemon.plugins.media-keys"
#define CUSTOM_KEYS_BASENAME "/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings"
#define CUSTOM_SHORTCUTS_ID "custom"
#define WID(builder, name) (GTK_WIDGET (gtk_builder_get_object (builder, name)))

typedef struct {
  /* The untranslated name, combine with ->package to translate */
  char *name;
  /* The group of keybindings (system or application) */
  char *group;
  /* The gettext package to use to translate the section title */
  char *package;
  /* Name of the window manager the keys would apply to */
  char *wm_name;
  /* The GSettings schema for the whole file, if any */
  char *schema;
  /* an array of KeyListEntry */
  GArray *entries;
} KeyList;

typedef struct
{
  CcKeyboardItemType type;
  char *schema; /* GSettings schema name, if any */
  char *description; /* description for GSettings types */
  char *name; /* GSettings schema path, or GSettings key name depending on type */
  char *reverse_entry;
  gboolean is_reversed;
  gboolean hidden;
} KeyListEntry;

typedef enum
{
  SHORTCUT_TYPE_KEY_ENTRY,
  SHORTCUT_TYPE_XKB_OPTION,
} ShortcutType;

enum
{
  DETAIL_DESCRIPTION_COLUMN,
  DETAIL_KEYENTRY_COLUMN,
  DETAIL_TYPE_COLUMN,
  DETAIL_N_COLUMNS
};

enum
{
  SECTION_DESCRIPTION_COLUMN,
  SECTION_ID_COLUMN,
  SECTION_GROUP_COLUMN,
  SECTION_N_COLUMNS
};

static GRegex *pictures_regex = NULL;
static GSettings *binding_settings = NULL;
static GtkWidget *custom_shortcut_dialog = NULL;
static GtkWidget *custom_shortcut_name_entry = NULL;
static GtkWidget *custom_shortcut_command_entry = NULL;
static GtkWidget *custom_shortcut_ok_button = NULL;
static GHashTable *kb_system_sections = NULL;
static GHashTable *kb_apps_sections = NULL;
static GHashTable *kb_user_sections = NULL;
static gpointer wm_changed_id = NULL;

static void
free_key_array (GPtrArray *keys)
{
  if (keys != NULL)
    {
      gint i;

      for (i = 0; i < keys->len; i++)
        {
          CcKeyboardItem *item;

          item = g_ptr_array_index (keys, i);

          g_object_unref (item);
        }

      g_ptr_array_free (keys, TRUE);
    }
}

static GHashTable *
get_hash_for_group (BindingGroupType group)
{
  GHashTable *hash;

  switch (group)
    {
    case BINDING_GROUP_SYSTEM:
      hash = kb_system_sections;
      break;
    case BINDING_GROUP_APPS:
      hash = kb_apps_sections;
      break;
    case BINDING_GROUP_USER:
      hash = kb_user_sections;
      break;
    default:
      hash = NULL;
    }
  return hash;
}

static gboolean
have_key_for_group (int group, const gchar *name)
{
  GHashTableIter iter;
  GPtrArray *keys;
  gint i;

  g_hash_table_iter_init (&iter, get_hash_for_group (group));
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&keys))
    {
      for (i = 0; i < keys->len; i++)
        {
          CcKeyboardItem *item = g_ptr_array_index (keys, i);

	  if (item->type == CC_KEYBOARD_ITEM_TYPE_GSETTINGS &&
	      g_strcmp0 (name, item->key) == 0)
	    {
	          return TRUE;
	    }

	  return FALSE;
        }
    }

  return FALSE;
}

static gboolean
keybinding_key_changed_foreach (GtkTreeModel   *model,
                                GtkTreePath    *path,
                                GtkTreeIter    *iter,
                                CcKeyboardItem *item)
{
  CcKeyboardItem *tmp_item;

  gtk_tree_model_get (item->model, iter,
                      DETAIL_KEYENTRY_COLUMN, &tmp_item,
                      -1);

  if (item == tmp_item)
    {
      gtk_tree_model_row_changed (item->model, path, iter);
      return TRUE;
    }
  return FALSE;
}

static void
item_changed (CcKeyboardItem *item,
	      GParamSpec     *pspec,
	      gpointer        user_data)
{
  /* update the model */
  gtk_tree_model_foreach (item->model, (GtkTreeModelForeachFunc) keybinding_key_changed_foreach, item);
}


static void
append_section (GtkBuilder         *builder,
                const gchar        *title,
                const gchar        *id,
                BindingGroupType    group,
                const KeyListEntry *keys_list)
{
  GPtrArray *keys_array;
  GtkTreeModel *sort_model;
  GtkTreeModel *model, *shortcut_model;
  GtkTreeIter iter;
  gint i;
  GHashTable *hash;
  gboolean is_new;

  hash = get_hash_for_group (group);
  if (!hash)
    return;

  sort_model = gtk_tree_view_get_model (GTK_TREE_VIEW (gtk_builder_get_object (builder, "section_treeview")));
  model = gtk_tree_model_sort_get_model (GTK_TREE_MODEL_SORT (sort_model));

  shortcut_model = gtk_tree_view_get_model (GTK_TREE_VIEW (gtk_builder_get_object (builder, "shortcut_treeview")));

  /* Add all CcKeyboardItems for this section */
  is_new = FALSE;
  keys_array = g_hash_table_lookup (hash, id);
  if (keys_array == NULL)
    {
      keys_array = g_ptr_array_new ();
      is_new = TRUE;
    }

  GHashTable *reverse_items = g_hash_table_new (g_str_hash, g_str_equal);

  for (i = 0; keys_list != NULL && keys_list[i].name != NULL; i++)
    {
      CcKeyboardItem *item;
      gboolean ret;

      if (have_key_for_group (group, keys_list[i].name))
        continue;

      item = cc_keyboard_item_new (keys_list[i].type);
      switch (keys_list[i].type)
        {
	case CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH:
          ret = cc_keyboard_item_load_from_gsettings_path (item, keys_list[i].name, FALSE);
          break;
	case CC_KEYBOARD_ITEM_TYPE_GSETTINGS:
	  ret = cc_keyboard_item_load_from_gsettings (item,
						      keys_list[i].description,
						      keys_list[i].schema,
						      keys_list[i].name);
          if (ret && keys_list[i].reverse_entry != NULL)
            {
              CcKeyboardItem *reverse_item;
              reverse_item = g_hash_table_lookup (reverse_items,
                                                  keys_list[i].reverse_entry);
              if (reverse_item != NULL)
                {
                  cc_keyboard_item_add_reverse_item (item,
                                                     reverse_item,
                                                     keys_list[i].is_reversed);
                }
              else
                {
                  g_hash_table_insert (reverse_items,
                                       keys_list[i].name,
                                       item);
                }
            }
	  break;
	default:
	  g_assert_not_reached ();
	}

      if (ret == FALSE)
        {
          /* We don't actually want to popup a dialog - just skip this one */
          g_object_unref (item);
          continue;
        }

      cc_keyboard_item_set_hidden (item, keys_list[i].hidden);
      item->model = shortcut_model;

      g_signal_connect (G_OBJECT (item), "notify",
			G_CALLBACK (item_changed), NULL);

      g_ptr_array_add (keys_array, item);
    }

  g_hash_table_destroy (reverse_items);

  /* Add the keys to the hash table */
  if (is_new)
    {
      g_hash_table_insert (hash, g_strdup (id), keys_array);

      /* Append the section to the left tree view */
      gtk_list_store_append (GTK_LIST_STORE (model), &iter);
      gtk_list_store_set (GTK_LIST_STORE (model), &iter,
                          SECTION_DESCRIPTION_COLUMN, title,
                          SECTION_ID_COLUMN, id,
                          SECTION_GROUP_COLUMN, group,
                          -1);
    }
}

static char *
replace_pictures_folder (const char *description)
{
  const char *path;
  char *dirname;
  char *ret;

  if (description == NULL)
    return NULL;

  if (strstr (description, "$PICTURES") == NULL)
    return g_strdup (description);

  path = g_get_user_special_dir (G_USER_DIRECTORY_PICTURES);
  dirname = g_filename_display_basename (path);
  ret = g_regex_replace (pictures_regex, description, -1,
                         0, dirname, 0, NULL);
  g_free (dirname);
  if (ret == NULL)
    return g_strdup (description);

  return ret;
}

static void
parse_start_tag (GMarkupParseContext *ctx,
                 const gchar         *element_name,
                 const gchar        **attr_names,
                 const gchar        **attr_values,
                 gpointer             user_data,
                 GError             **error)
{
  KeyList *keylist = (KeyList *) user_data;
  KeyListEntry key = { 0, };
  const char *name, *schema, *description, *package, *context, *orig_description, *reverse_entry;
  gboolean is_reversed, hidden;

  name = NULL;
  schema = NULL;
  package = NULL;
  context = NULL;

  /* The top-level element, names the section in the tree */
  if (g_str_equal (element_name, "KeyListEntries"))
    {
      const char *wm_name = NULL;
      const char *group = NULL;

      while (*attr_names && *attr_values)
        {
          if (g_str_equal (*attr_names, "name"))
            {
              if (**attr_values)
                name = *attr_values;
            } else if (g_str_equal (*attr_names, "group")) {
              if (**attr_values)
                group = *attr_values;
            } else if (g_str_equal (*attr_names, "wm_name")) {
              if (**attr_values)
                wm_name = *attr_values;
	    } else if (g_str_equal (*attr_names, "schema")) {
	      if (**attr_values)
	        schema = *attr_values;
            } else if (g_str_equal (*attr_names, "package")) {
              if (**attr_values)
                package = *attr_values;
            }
          ++attr_names;
          ++attr_values;
        }

      if (name)
        {
          if (keylist->name)
            g_warning ("Duplicate section name");
          g_free (keylist->name);
          keylist->name = g_strdup (name);
        }
      if (wm_name)
        {
          if (keylist->wm_name)
            g_warning ("Duplicate window manager name");
          g_free (keylist->wm_name);
          keylist->wm_name = g_strdup (wm_name);
        }
      if (package)
        {
          if (keylist->package)
            g_warning ("Duplicate gettext package name");
          g_free (keylist->package);
          keylist->package = g_strdup (package);
	  bind_textdomain_codeset (keylist->package, "UTF-8");
        }
      if (group)
        {
          if (keylist->group)
            g_warning ("Duplicate group");
          g_free (keylist->group);
          keylist->group = g_strdup (group);
        }
      if (schema)
        {
          if (keylist->schema)
            g_warning ("Duplicate schema");
          g_free (keylist->schema);
          keylist->schema = g_strdup (schema);
	}
      return;
    }

  if (!g_str_equal (element_name, "KeyListEntry")
      || attr_names == NULL
      || attr_values == NULL)
    return;

  schema = NULL;
  description = NULL;
  context = NULL;
  orig_description = NULL;
  reverse_entry = NULL;
  is_reversed = FALSE;
  hidden = FALSE;

  while (*attr_names && *attr_values)
    {
      if (g_str_equal (*attr_names, "name"))
        {
          /* skip if empty */
          if (**attr_values)
            name = *attr_values;
	} else if (g_str_equal (*attr_names, "schema")) {
	  if (**attr_values) {
	   schema = *attr_values;
	  }
	} else if (g_str_equal (*attr_names, "description")) {
          if (**attr_values)
	    orig_description = *attr_values;
        } else if (g_str_equal (*attr_names, "msgctxt")) {
          if (**attr_values)
            context = *attr_values;
	} else if (g_str_equal (*attr_names, "reverse-entry")) {
	  if (**attr_values)
	    reverse_entry = *attr_values;
	} else if (g_str_equal (*attr_names, "is-reversed")) {
	    if (g_str_equal (*attr_values, "true"))
	      is_reversed = TRUE;
	} else if (g_str_equal (*attr_names, "hidden")) {
	    if (g_str_equal (*attr_values, "true"))
	      hidden = TRUE;
	}

      ++attr_names;
      ++attr_values;
    }

  if (name == NULL)
    return;

  if (schema == NULL &&
      keylist->schema == NULL) {
    g_debug ("Ignored GConf keyboard shortcut '%s'", name);
    return;
  }

  if (context != NULL)
    description = g_dpgettext2 (keylist->package, context, orig_description);
  else
    description = dgettext (keylist->package, orig_description);

  key.name = g_strdup (name);
  key.type = CC_KEYBOARD_ITEM_TYPE_GSETTINGS;
  key.description = replace_pictures_folder (description);
  key.schema = schema ? g_strdup (schema) : g_strdup (keylist->schema);
  key.reverse_entry = g_strdup (reverse_entry);
  key.is_reversed = is_reversed;
  key.hidden = hidden;
  g_array_append_val (keylist->entries, key);
}

static gboolean
strv_contains (char **strv,
               char  *str)
{
  char **p = strv;
  for (p = strv; *p; p++)
    if (strcmp (*p, str) == 0)
      return TRUE;

  return FALSE;
}

static void
append_sections_from_file (GtkBuilder *builder, const gchar *path, const char *datadir, gchar **wm_keybindings)
{
  GError *err = NULL;
  char *buf;
  gsize buf_len;
  KeyList *keylist;
  KeyListEntry *keys;
  KeyListEntry key = { 0, };
  const char *title;
  int group;
  guint i;
  GMarkupParseContext *ctx;
  GMarkupParser parser = { parse_start_tag, NULL, NULL, NULL, NULL };

  /* Parse file */
  if (!g_file_get_contents (path, &buf, &buf_len, &err))
    return;

  keylist = g_new0 (KeyList, 1);
  keylist->entries = g_array_new (FALSE, TRUE, sizeof (KeyListEntry));
  ctx = g_markup_parse_context_new (&parser, 0, keylist, NULL);

  if (!g_markup_parse_context_parse (ctx, buf, buf_len, &err))
    {
      g_warning ("Failed to parse '%s': '%s'", path, err->message);
      g_error_free (err);
      g_free (keylist->name);
      g_free (keylist->package);
      g_free (keylist->wm_name);
      for (i = 0; i < keylist->entries->len; i++)
        g_free (((KeyListEntry *) &(keylist->entries->data[i]))->name);
      g_array_free (keylist->entries, TRUE);
      g_free (keylist);
      keylist = NULL;
    }
  g_markup_parse_context_free (ctx);
  g_free (buf);

  if (keylist == NULL)
    return;

  /* If there's no keys to add, or the settings apply to a window manager
   * that's not the one we're running */
  if (keylist->entries->len == 0
      || (keylist->wm_name != NULL && !strv_contains (wm_keybindings, keylist->wm_name))
      || keylist->name == NULL)
    {
      g_free (keylist->name);
      g_free (keylist->package);
      g_free (keylist->wm_name);
      g_array_free (keylist->entries, TRUE);
      g_free (keylist);
      return;
    }

  /* Empty KeyListEntry to end the array */
  key.name = NULL;
  g_array_append_val (keylist->entries, key);

  keys = (KeyListEntry *) g_array_free (keylist->entries, FALSE);
  if (keylist->package)
    {
      char *localedir;

      localedir = g_build_filename (datadir, "locale", NULL);
      bindtextdomain (keylist->package, localedir);
      g_free (localedir);

      title = dgettext (keylist->package, keylist->name);
    } else {
      title = _(keylist->name);
    }
  if (keylist->group && strcmp (keylist->group, "system") == 0)
    group = BINDING_GROUP_SYSTEM;
  else
    group = BINDING_GROUP_APPS;

  append_section (builder, title, keylist->name, group, keys);

  g_free (keylist->name);
  g_free (keylist->package);
  g_free (keylist->wm_name);
  g_free (keylist->schema);
  g_free (keylist->group);

  for (i = 0; keys[i].name != NULL; i++) {
    KeyListEntry *entry = &keys[i];
    g_free (entry->schema);
    g_free (entry->description);
    g_free (entry->name);
    g_free (entry->reverse_entry);
  }

  g_free (keylist);
}

static void
append_sections_from_gsettings (GtkBuilder *builder)
{
  char **custom_paths;
  GArray *entries;
  KeyListEntry key = { 0, };
  int i;

  /* load custom shortcuts from GSettings */
  entries = g_array_new (FALSE, TRUE, sizeof (KeyListEntry));

  custom_paths = g_settings_get_strv (binding_settings, "custom-keybindings");
  for (i = 0; custom_paths[i]; i++)
    {
      key.name = g_strdup (custom_paths[i]);
      if (!have_key_for_group (BINDING_GROUP_USER, key.name))
        {
          key.type = CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH;
          g_array_append_val (entries, key);
        }
      else
        g_free (key.name);
    }
  g_strfreev (custom_paths);

  if (entries->len > 0)
    {
      KeyListEntry *keys;
      int i;

      /* Empty KeyListEntry to end the array */
      key.name = NULL;
      g_array_append_val (entries, key);

      keys = (KeyListEntry *) entries->data;
      append_section (builder, _("Custom Shortcuts"), CUSTOM_SHORTCUTS_ID, BINDING_GROUP_USER, keys);
      for (i = 0; i < entries->len; ++i)
        {
          g_free (keys[i].name);
        }
    }
  else
    {
      append_section (builder, _("Custom Shortcuts"), CUSTOM_SHORTCUTS_ID, BINDING_GROUP_USER, NULL);
    }

  g_array_free (entries, TRUE);
}

static void
reload_sections (CcPanel *panel)
{
  GtkBuilder *builder;
  gchar **wm_keybindings;
  gchar *default_wm_keybindings[] = { "Mutter", "GNOME Shell", NULL };
  GDir *dir;
  GtkTreeModel *sort_model;
  GtkTreeModel *section_model;
  GtkTreeModel *shortcut_model;
  const gchar * const * data_dirs;
  guint i;
  GtkTreeView *section_treeview;
  GtkTreeSelection *selection;
  GtkTreeIter iter;
  GHashTable *loaded_files;
  const char *section_to_set;

  builder = g_object_get_data (G_OBJECT (panel), "builder");

  section_treeview = GTK_TREE_VIEW (gtk_builder_get_object (builder, "section_treeview"));
  sort_model = gtk_tree_view_get_model (section_treeview);
  section_model = gtk_tree_model_sort_get_model (GTK_TREE_MODEL_SORT (sort_model));

  shortcut_model = gtk_tree_view_get_model (GTK_TREE_VIEW (gtk_builder_get_object (builder, "shortcut_treeview")));
  /* FIXME: get current selection and keep it after refreshing */

  /* Clear previous models and hash tables */
  gtk_list_store_clear (GTK_LIST_STORE (section_model));
  gtk_list_store_clear (GTK_LIST_STORE (shortcut_model));
  if (kb_system_sections != NULL)
    g_hash_table_destroy (kb_system_sections);
  kb_system_sections = g_hash_table_new_full (g_str_hash,
                                              g_str_equal,
                                              g_free,
                                              (GDestroyNotify) free_key_array);

  if (kb_apps_sections != NULL)
    g_hash_table_destroy (kb_apps_sections);
  kb_apps_sections = g_hash_table_new_full (g_str_hash,
                                            g_str_equal,
                                            g_free,
                                            (GDestroyNotify) free_key_array);

  if (kb_user_sections != NULL)
    g_hash_table_destroy (kb_user_sections);
  kb_user_sections = g_hash_table_new_full (g_str_hash,
                                            g_str_equal,
                                            g_free,
                                            (GDestroyNotify) free_key_array);

  /* Load WM keybindings */
#ifdef GDK_WINDOWING_X11
  if (GDK_IS_X11_DISPLAY (gdk_display_get_default ()))
    wm_keybindings = wm_common_get_current_keybindings ();
#endif
  else
    wm_keybindings = g_strdupv (default_wm_keybindings);

  loaded_files = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

  data_dirs = g_get_system_data_dirs ();
  for (i = 0; data_dirs[i] != NULL; i++)
    {
      char *dir_path;
      const gchar *name;

      dir_path = g_build_filename (data_dirs[i], "gnome-control-center", "keybindings", NULL);

      dir = g_dir_open (dir_path, 0, NULL);
      if (!dir)
        {
          g_free (dir_path);
          continue;
        }

      for (name = g_dir_read_name (dir) ; name ; name = g_dir_read_name (dir))
        {
          gchar *path;

	  if (g_str_has_suffix (name, ".xml") == FALSE)
	    continue;

          if (g_hash_table_lookup (loaded_files, name) != NULL)
	    {
	      g_debug ("Not loading %s, it was already loaded from another directory", name);
              continue;
	    }

	  g_hash_table_insert (loaded_files, g_strdup (name), GINT_TO_POINTER (1));
	  path = g_build_filename (dir_path, name, NULL);
	  append_sections_from_file (builder, path, data_dirs[i], wm_keybindings);
	  g_free (path);
	}
      g_free (dir_path);
      g_dir_close (dir);
    }

  g_hash_table_destroy (loaded_files);
  g_strfreev (wm_keybindings);

  /* Add a separator */
  gtk_list_store_append (GTK_LIST_STORE (section_model), &iter);
  gtk_list_store_set (GTK_LIST_STORE (section_model), &iter,
                      SECTION_DESCRIPTION_COLUMN, NULL,
                      SECTION_GROUP_COLUMN, BINDING_GROUP_SEPARATOR,
                      -1);

  /* Load custom keybindings */
  append_sections_from_gsettings (builder);

  /* Select the first item, or the requested section, if any */
  section_to_set = g_object_get_data (G_OBJECT (panel), "section-to-set");
  if (section_to_set != NULL)
    {
      if (keyboard_shortcuts_set_section (panel, section_to_set))
        {
          g_object_set_data (G_OBJECT (panel), "section-to-set", NULL);
          return;
	}
    }
  g_assert (gtk_tree_model_get_iter_first (sort_model, &iter));
  selection = gtk_tree_view_get_selection (section_treeview);
  gtk_tree_selection_select_iter (selection, &iter);

  g_object_set_data (G_OBJECT (panel), "section-to-set", NULL);
}

static void
accel_set_func (GtkTreeViewColumn *tree_column,
                GtkCellRenderer   *cell,
                GtkTreeModel      *model,
                GtkTreeIter       *iter,
                gpointer           data)
{
  gpointer entry;
  ShortcutType type;

  gtk_tree_model_get (model, iter,
                      DETAIL_KEYENTRY_COLUMN, &entry,
                      DETAIL_TYPE_COLUMN, &type,
                      -1);

  gtk_cell_renderer_set_visible (cell, FALSE);

  if (type == SHORTCUT_TYPE_XKB_OPTION &&
      GTK_IS_CELL_RENDERER_COMBO (cell))
    {
      CcKeyboardOption *option = entry;

      gtk_cell_renderer_set_visible (cell, TRUE);
      g_object_set (cell,
                    "model", cc_keyboard_option_get_store (option),
                    "text", cc_keyboard_option_get_current_value_description (option),
                    NULL);
    }
  else if (type == SHORTCUT_TYPE_KEY_ENTRY &&
           GTK_IS_CELL_RENDERER_TEXT (cell) &&
           !GTK_IS_CELL_RENDERER_COMBO (cell) &&
           entry != NULL)
    {
      CcKeyboardItem *item = entry;

      gtk_cell_renderer_set_visible (cell, TRUE);

      if (item->editable)
        g_object_set (cell,
                      "editable", TRUE,
                      "accel-key", item->keyval,
                      "accel-mods", item->mask,
                      "keycode", item->keycode,
                      "style", PANGO_STYLE_NORMAL,
                      NULL);
      else
        g_object_set (cell,
                      "editable", FALSE,
                      "accel-key", item->keyval,
                      "accel-mods", item->mask,
                      "keycode", item->keycode,
                      "style", PANGO_STYLE_ITALIC,
                      NULL);
    }
}

static void
description_set_func (GtkTreeViewColumn *tree_column,
                      GtkCellRenderer   *cell,
                      GtkTreeModel      *model,
                      GtkTreeIter       *iter,
                      gpointer           data)
{
  gchar *description;
  CcKeyboardItem *item;
  ShortcutType type;

  gtk_tree_model_get (model, iter,
                      DETAIL_DESCRIPTION_COLUMN, &description,
                      DETAIL_KEYENTRY_COLUMN, &item,
                      DETAIL_TYPE_COLUMN, &type,
                      -1);

  if (type == SHORTCUT_TYPE_XKB_OPTION)
    {
      g_object_set (cell, "text", description, NULL);
    }
  else
    {
      if (item != NULL)
        g_object_set (cell,
                      "editable", FALSE,
                      "text", item->description != NULL ?
                      item->description : _("<Unknown Action>"),
                      NULL);
      else
        g_object_set (cell,
                      "editable", FALSE, NULL);
    }

  g_free (description);
}

static void
shortcut_selection_changed (GtkTreeSelection *selection, gpointer data)
{
  GtkWidget *button = data;
  GtkTreeModel *model;
  GtkTreeIter iter;
  CcKeyboardItem *item;
  gboolean can_remove;
  ShortcutType type;

  can_remove = FALSE;
  if (gtk_tree_selection_get_selected (selection, &model, &iter))
    {
      gtk_tree_model_get (model, &iter,
                          DETAIL_KEYENTRY_COLUMN, &item,
                          DETAIL_TYPE_COLUMN, &type,
                          -1);
      if (type == SHORTCUT_TYPE_KEY_ENTRY &&
          item && item->command != NULL && item->editable)
        can_remove = TRUE;
    }

  gtk_widget_set_sensitive (button, can_remove);
}

static void
fill_xkb_options_shortcuts (GtkTreeModel *model)
{
  GList *l;
  GtkTreeIter iter;

  for (l = cc_keyboard_option_get_all (); l; l = l->next)
    {
      CcKeyboardOption *option = l->data;

      gtk_list_store_append (GTK_LIST_STORE (model), &iter);
      gtk_list_store_set (GTK_LIST_STORE (model), &iter,
                          DETAIL_DESCRIPTION_COLUMN, cc_keyboard_option_get_description (option),
                          DETAIL_KEYENTRY_COLUMN, option,
                          DETAIL_TYPE_COLUMN, SHORTCUT_TYPE_XKB_OPTION,
                          -1);
    }
}

static void
section_selection_changed (GtkTreeSelection *selection, gpointer data)
{
  GtkTreeIter iter;
  GtkTreeModel *model;
  GtkBuilder *builder = GTK_BUILDER (data);

  if (gtk_tree_selection_get_selected (selection, &model, &iter))
    {
      GPtrArray *keys;
      GtkWidget *shortcut_treeview;
      GtkTreeModel *shortcut_model;
      gchar *id;
      BindingGroupType group;
      gint i;

      gtk_tree_model_get (model, &iter,
                          SECTION_ID_COLUMN, &id,
                          SECTION_GROUP_COLUMN, &group, -1);

      keys = g_hash_table_lookup (get_hash_for_group (group), id);
      if (keys == NULL)
        {
          g_warning ("Can't find section %s in sections hash table.", id);
          g_free (id);
          return;
        }

      gtk_widget_set_sensitive (WID (builder, "remove-toolbutton"), FALSE);

      /* Fill the shortcut treeview with the keys for the selected section */
      shortcut_treeview = GTK_WIDGET (gtk_builder_get_object (builder, "shortcut_treeview"));
      shortcut_model = gtk_tree_view_get_model (GTK_TREE_VIEW (shortcut_treeview));
      gtk_list_store_clear (GTK_LIST_STORE (shortcut_model));

      for (i = 0; i < keys->len; i++)
        {
          GtkTreeIter new_row;
          CcKeyboardItem *item = g_ptr_array_index (keys, i);

          if (!cc_keyboard_item_is_hidden (item))
            {
              gtk_list_store_append (GTK_LIST_STORE (shortcut_model), &new_row);
              gtk_list_store_set (GTK_LIST_STORE (shortcut_model), &new_row,
                                  DETAIL_DESCRIPTION_COLUMN, item->description,
                                  DETAIL_KEYENTRY_COLUMN, item,
                                  DETAIL_TYPE_COLUMN, SHORTCUT_TYPE_KEY_ENTRY,
                                  -1);
            }
        }

      if (g_str_equal (id, "Typing"))
        fill_xkb_options_shortcuts (shortcut_model);

      g_free (id);
    }
}

static gboolean
edit_custom_shortcut (CcKeyboardItem *item)
{
  gint result;
  gboolean ret;
  GSettings *settings;

  settings = g_settings_new_with_path (item->schema, item->gsettings_path);

  g_settings_bind (settings, "name",
                   G_OBJECT (custom_shortcut_name_entry), "text",
                   G_SETTINGS_BIND_DEFAULT);
  gtk_widget_grab_focus (custom_shortcut_name_entry);

  g_settings_bind (settings, "command",
                   G_OBJECT (custom_shortcut_command_entry), "text",
                   G_SETTINGS_BIND_DEFAULT);

  g_settings_delay (settings);

  gtk_widget_set_sensitive (custom_shortcut_name_entry,
                            item->desc_editable);
  gtk_widget_set_sensitive (custom_shortcut_command_entry,
                            item->cmd_editable);
  gtk_window_present (GTK_WINDOW (custom_shortcut_dialog));
  result = gtk_dialog_run (GTK_DIALOG (custom_shortcut_dialog));
  switch (result)
    {
    case GTK_RESPONSE_OK:
      g_settings_apply (settings);
      ret = TRUE;
      break;
    default:
      g_settings_revert (settings);
      ret = FALSE;
      break;
    }

  g_settings_unbind (G_OBJECT (custom_shortcut_name_entry), "text");
  g_settings_unbind (G_OBJECT (custom_shortcut_command_entry), "text");

  g_object_unref (settings);

  gtk_widget_hide (custom_shortcut_dialog);

  return ret;
}

static gboolean
remove_custom_shortcut (GtkTreeModel *model, GtkTreeIter *iter)
{
  CcKeyboardItem *item;
  GPtrArray *keys_array;
  GVariantBuilder builder;
  char **settings_paths;
  int i;

  gtk_tree_model_get (model, iter,
                      DETAIL_KEYENTRY_COLUMN, &item,
                      -1);

  /* not a custom shortcut */
  g_assert (item->type == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH);

  g_settings_delay (item->settings);
  g_settings_reset (item->settings, "name");
  g_settings_reset (item->settings, "command");
  g_settings_reset (item->settings, "binding");
  g_settings_apply (item->settings);
  g_settings_sync ();

  settings_paths = g_settings_get_strv (binding_settings, "custom-keybindings");
  g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
  for (i = 0; settings_paths[i]; i++)
    if (strcmp (settings_paths[i], item->gsettings_path) != 0)
      g_variant_builder_add (&builder, "s", settings_paths[i]);
  g_settings_set_value (binding_settings,
                        "custom-keybindings", g_variant_builder_end (&builder));
  g_strfreev (settings_paths);
  g_object_unref (item);

  keys_array = g_hash_table_lookup (get_hash_for_group (BINDING_GROUP_USER), CUSTOM_SHORTCUTS_ID);
  g_ptr_array_remove (keys_array, item);

  gtk_list_store_remove (GTK_LIST_STORE (model), iter);

  return TRUE;
}

static void
update_custom_shortcut (GtkTreeModel *model, GtkTreeIter *iter)
{
  CcKeyboardItem *item;

  gtk_tree_model_get (model, iter,
                      DETAIL_KEYENTRY_COLUMN, &item,
                      -1);

  g_assert (item->type == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH);

  edit_custom_shortcut (item);
  if (item->command == NULL || item->command[0] == '\0')
    {
      remove_custom_shortcut (model, iter);
    }
  else
    {
      gtk_list_store_set (GTK_LIST_STORE (model), iter,
                          DETAIL_KEYENTRY_COLUMN, item, -1);
    }
}

static gboolean
start_editing_cb (GtkTreeView    *tree_view,
                  GdkEventButton *event,
                  gpointer        user_data)
{
  GtkTreePath *path;
  GtkTreeViewColumn *column;
  GtkCellRenderer *cell = user_data;

  if (event->window != gtk_tree_view_get_bin_window (tree_view))
    return FALSE;

  if (gtk_tree_view_get_path_at_pos (tree_view,
                                     (gint) event->x,
                                     (gint) event->y,
                                     &path, &column,
                                     NULL, NULL))
    {
      GtkTreeModel *model;
      GtkTreeIter iter;
      CcKeyboardItem *item;
      ShortcutType type;

      model = gtk_tree_view_get_model (tree_view);
      gtk_tree_model_get_iter (model, &iter, path);
      gtk_tree_model_get (model, &iter,
                          DETAIL_KEYENTRY_COLUMN, &item,
                          DETAIL_TYPE_COLUMN, &type,
                         -1);

      if (type == SHORTCUT_TYPE_XKB_OPTION)
        {
          gtk_tree_path_free (path);
          return FALSE;
        }

      /* if only the accel can be edited on the selected row
       * always select the accel column */
      if (item->desc_editable &&
          column == gtk_tree_view_get_column (tree_view, 0))
        {
          gtk_widget_grab_focus (GTK_WIDGET (tree_view));
          gtk_tree_view_set_cursor (tree_view,
                                    path,
                                    column,
                                    FALSE);
          update_custom_shortcut (model, &iter);
        }
      else
        {
          gtk_widget_grab_focus (GTK_WIDGET (tree_view));
          gtk_tree_view_set_cursor_on_cell (tree_view,
                                            path,
                                            gtk_tree_view_get_column (tree_view, 1),
                                            cell,
                                            TRUE);
        }
      g_signal_stop_emission_by_name (tree_view, "button_press_event");
      gtk_tree_path_free (path);
    }
  return TRUE;
}

static void
start_editing_kb_cb (GtkTreeView       *treeview,
                     GtkTreePath       *path,
                     GtkTreeViewColumn *column,
                     gpointer           user_data)
{
  GtkTreeModel *model;
  GtkTreeIter iter;
  CcKeyboardItem *item;
  ShortcutType type;
  GtkCellRenderer *cell = user_data;

  model = gtk_tree_view_get_model (treeview);
  gtk_tree_model_get_iter (model, &iter, path);
  gtk_tree_model_get (model, &iter,
                      DETAIL_KEYENTRY_COLUMN, &item,
                      DETAIL_TYPE_COLUMN, &type,
                      -1);

  if (type == SHORTCUT_TYPE_XKB_OPTION)
    return;

  /* if only the accel can be edited on the selected row
   * always select the accel column */
  if (item->desc_editable &&
      column == gtk_tree_view_get_column (treeview, 0))
    {
      gtk_widget_grab_focus (GTK_WIDGET (treeview));
      gtk_tree_view_set_cursor (treeview,
                                path,
                                column,
                                FALSE);
      update_custom_shortcut (model, &iter);
    }
  else
    {
       gtk_widget_grab_focus (GTK_WIDGET (treeview));
       gtk_tree_view_set_cursor_on_cell (treeview,
                                         path,
                                         gtk_tree_view_get_column (treeview, 1),
                                         cell,
                                         TRUE);
    }
}

static const guint forbidden_keyvals[] = {
  /* Navigation keys */
  GDK_KEY_Home,
  GDK_KEY_Left,
  GDK_KEY_Up,
  GDK_KEY_Right,
  GDK_KEY_Down,
  GDK_KEY_Page_Up,
  GDK_KEY_Page_Down,
  GDK_KEY_End,
  GDK_KEY_Tab,

  /* Return */
  GDK_KEY_KP_Enter,
  GDK_KEY_Return,

  GDK_KEY_Mode_switch
};

static char*
binding_name (guint                   keyval,
              guint                   keycode,
              GdkModifierType         mask,
              gboolean                translate)
{
  if (keyval != 0 || keycode != 0)
    return translate ?
        gtk_accelerator_get_label_with_keycode (NULL, keyval, keycode, mask) :
        gtk_accelerator_name_with_keycode (NULL, keyval, keycode, mask);
  else
    return g_strdup (translate ? _("Disabled") : NULL);
}

static gboolean
keyval_is_forbidden (guint keyval)
{
  guint i;

  for (i = 0; i < G_N_ELEMENTS(forbidden_keyvals); i++) {
    if (keyval == forbidden_keyvals[i])
      return TRUE;
  }

  return FALSE;
}

typedef struct {
  CcKeyboardItem *orig_item;
  CcKeyboardItem *conflict_item;
  guint new_keyval;
  GdkModifierType new_mask;
  guint new_keycode;
} CcUniquenessData;

static gboolean
compare_keys_for_uniqueness (CcKeyboardItem   *element,
                             CcUniquenessData *data)
{
  CcKeyboardItem *orig_item;

  orig_item = data->orig_item;

  /* no conflict for : blanks, different modifiers, or ourselves */
  if (element == NULL || data->new_mask != element->mask ||
      cc_keyboard_item_equal (orig_item, element))
    return FALSE;

  if (data->new_keyval != 0) {
      if (data->new_keyval != element->keyval)
          return FALSE;
  } else if (element->keyval != 0 || data->new_keycode != element->keycode)
    return FALSE;

  data->conflict_item = element;

  return TRUE;
}

static gboolean
cb_check_for_uniqueness (gpointer          key,
                         GPtrArray        *keys_array,
                         CcUniquenessData *data)
{
  guint i;

  for (i = 0; i < keys_array->len; i++)
    {
      CcKeyboardItem *item;

      item = keys_array->pdata[i];
      if (compare_keys_for_uniqueness (item, data))
        return TRUE;
    }
  return FALSE;
}

static CcKeyboardItem *
search_for_conflict_item (CcKeyboardItem *item,
                          guint           keyval,
                          GdkModifierType mask,
                          guint           keycode)
{
  CcUniquenessData data;

  data.orig_item = item;
  data.new_keyval = keyval;
  data.new_mask = mask;
  data.new_keycode = keycode;
  data.conflict_item = NULL;

  if (keyval != 0 || keycode != 0) /* any number of shortcuts can be disabled */
    {
      BindingGroupType i;

      for (i = BINDING_GROUP_SYSTEM; i <= BINDING_GROUP_USER && data.conflict_item == NULL; i++)
        {
          GHashTable *table;

          table = get_hash_for_group (i);
          if (!table)
            continue;
          g_hash_table_find (table, (GHRFunc) cb_check_for_uniqueness, &data);
        }
    }

  return data.conflict_item;
}

static gboolean
is_valid_binding (guint                  keyval,
                  GdkModifierType        mask,
                  guint                  keycode)
{
  if ((mask == 0 || mask == GDK_SHIFT_MASK) && keycode != 0)
    {
      if ((keyval >= GDK_KEY_a && keyval <= GDK_KEY_z)
           || (keyval >= GDK_KEY_A && keyval <= GDK_KEY_Z)
           || (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9)
           || (keyval >= GDK_KEY_kana_fullstop && keyval <= GDK_KEY_semivoicedsound)
           || (keyval >= GDK_KEY_Arabic_comma && keyval <= GDK_KEY_Arabic_sukun)
           || (keyval >= GDK_KEY_Serbian_dje && keyval <= GDK_KEY_Cyrillic_HARDSIGN)
           || (keyval >= GDK_KEY_Greek_ALPHAaccent && keyval <= GDK_KEY_Greek_omega)
           || (keyval >= GDK_KEY_hebrew_doublelowline && keyval <= GDK_KEY_hebrew_taf)
           || (keyval >= GDK_KEY_Thai_kokai && keyval <= GDK_KEY_Thai_lekkao)
           || (keyval >= GDK_KEY_Hangul_Kiyeog && keyval <= GDK_KEY_Hangul_J_YeorinHieuh)
           || (keyval == GDK_KEY_space && mask == 0)
           || keyval_is_forbidden (keyval)) {
        return FALSE;
      }
    }
  return TRUE;
}

static GtkResponseType
show_invalid_binding_dialog (GtkTreeView           *view,
                             guint                  keyval,
                             GdkModifierType        mask,
                             guint                  keycode)
{
  GtkWidget *dialog;
  char *name;

  name = binding_name (keyval, keycode, mask, TRUE);

  dialog =
    gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (view))),
                            GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
                            GTK_MESSAGE_WARNING,
                            GTK_BUTTONS_CANCEL,
                            _("The shortcut \"%s\" cannot be used because it will become impossible to type using this key.\n"
                            "Please try with a key such as Control, Alt or Shift at the same time."),
                            name);

  g_free (name);
  gtk_dialog_run (GTK_DIALOG (dialog));
  gtk_widget_destroy (dialog);

  return GTK_RESPONSE_NONE;
}

static GtkResponseType
show_conflict_item_dialog (GtkTreeView           *view,
                           CcKeyboardItem        *item,
                           CcKeyboardItem        *conflict_item,
                           guint                  keyval,
                           GdkModifierType        mask,
                           guint                  keycode)
{
  GtkWidget *dialog;
  char *name;
  int response;

  name = binding_name (keyval, keycode, mask, TRUE);

  dialog =
    gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (view))),
                            GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
                            GTK_MESSAGE_WARNING,
                            GTK_BUTTONS_CANCEL,
                            _("The shortcut \"%s\" is already used for\n\"%s\""),
                            name, conflict_item->description);
  g_free (name);

  gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
      _("If you reassign the shortcut to \"%s\", the \"%s\" shortcut "
        "will be disabled."),
      item->description,
      conflict_item->description);

  gtk_dialog_add_button (GTK_DIALOG (dialog),
                         _("_Reassign"),
                         GTK_RESPONSE_ACCEPT);

  gtk_dialog_set_default_response (GTK_DIALOG (dialog),
                                   GTK_RESPONSE_ACCEPT);

  response = gtk_dialog_run (GTK_DIALOG (dialog));
  gtk_widget_destroy (dialog);

  return response;
}

static GtkResponseType
show_reverse_item_dialog (GtkTreeView           *view,
                          CcKeyboardItem        *item,
                          CcKeyboardItem        *reverse_item,
                          CcKeyboardItem        *reverse_conflict_item,
                          guint                  keyval,
                          GdkModifierType        mask,
                          guint                  keycode)
{
  GtkWidget *dialog;
  char *name;
  int response;

  name = binding_name (keyval, keycode, mask, TRUE);

  /* translators:
   * This is the text you get in a dialogue when an action has an associated
   * "reverse" action, for example Alt+Tab going in the opposite direction to
   * Alt+Shift+Tab.
   *
   * An example text would be:
   * The "Switch to next input source" shortcut has an associated "Switch to
   * previous input source" shortcut. Do you want to automatically set it to
   * "Shift+Ctrl+Alt+Space"? */
  dialog =
    gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (view))),
                            GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
                            GTK_MESSAGE_WARNING,
                            GTK_BUTTONS_CANCEL,
                            _("The \"%s\" shortcut has an associated \"%s\" shortcut. "
                              "Do you want to automatically set it to \"%s\"?"),
                            item->description, reverse_item->description, name);

  if (reverse_conflict_item != NULL) {
    /* translators:
     * This is the text you get in a dialogue when you try to use a shortcut
     * that was already associated with another action, for example:
     * "Alt+F4" is currently associated with "Close Window", ... */
    gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
        _("\"%s\" is currently associated with \"%s\", this shortcut will be"
          " disabled if you move forward."),
        name, reverse_conflict_item->description);
  }
  g_free (name);

  gtk_dialog_add_button (GTK_DIALOG (dialog),
                         _("_Assign"),
                         GTK_RESPONSE_ACCEPT);

  gtk_dialog_set_default_response (GTK_DIALOG (dialog),
                                   GTK_RESPONSE_ACCEPT);

  response = gtk_dialog_run (GTK_DIALOG (dialog));
  gtk_widget_destroy (dialog);

  return response;
}

static void
handle_reverse_item (CcKeyboardItem        *item,
                     CcKeyboardItem        *reverse_item,
                     guint                  keyval,
                     GdkModifierType        mask,
                     guint                  keycode,
                     GtkTreeView           *view)
{
  GtkResponseType response;
  GdkModifierType reverse_mask;

  reverse_mask = mask ^ GDK_SHIFT_MASK;

  if (!is_valid_binding(keyval, reverse_mask, keycode))
    return;

  if ((reverse_item->keyval != keyval)
       || (reverse_item->keycode != keycode)
       || (reverse_item->mask != reverse_mask))
    {
      CcKeyboardItem *reverse_conflict_item;
      char *binding_str;

      reverse_conflict_item = search_for_conflict_item (reverse_item, keyval,
                                                        reverse_mask,
                                                        keycode);

      response = show_reverse_item_dialog (view, item, reverse_item,
                                           reverse_conflict_item,
                                           keyval, reverse_mask,
                                           keycode);
      if (response == GTK_RESPONSE_ACCEPT)
        {
          binding_str = binding_name (keyval, keycode, reverse_mask, FALSE);
          g_object_set (G_OBJECT (reverse_item), "binding", binding_str, NULL);
          g_free (binding_str);

          if (reverse_conflict_item != NULL)
            g_object_set (G_OBJECT (reverse_conflict_item),
                          "binding", NULL, NULL);
        }
      else
        {
          /* The existing reverse binding may be conflicting with the binding
           * we are setting. Other conflicts have already been handled in
           * accel_edited_callback()
           */
          CcKeyboardItem *conflict_item;
          conflict_item = search_for_conflict_item (item, keyval,
                                                    mask, keycode);
          if (conflict_item != NULL)
            {
              g_warn_if_fail (conflict_item == reverse_item);
              g_object_set (G_OBJECT (conflict_item),
                            "binding", NULL, NULL);
            }
        }
    }
}

static void
accel_edited_callback (GtkCellRendererText   *cell,
                       const char            *path_string,
                       guint                  keyval,
                       GdkModifierType        mask,
                       guint                  keycode,
                       GtkTreeView           *view)
{
  GtkTreeModel *model;
  GtkTreePath *path = gtk_tree_path_new_from_string (path_string);
  GtkTreeIter iter;
  CcKeyboardItem *item;
  CcKeyboardItem *conflict_item;
  CcKeyboardItem *reverse_item;
  char *str;

  model = gtk_tree_view_get_model (view);
  gtk_tree_model_get_iter (model, &iter, path);
  gtk_tree_path_free (path);
  gtk_tree_model_get (model, &iter,
                      DETAIL_KEYENTRY_COLUMN, &item,
                      -1);

  /* sanity check */
  if (item == NULL)
    return;

  /* CapsLock isn't supported as a keybinding modifier, so keep it from confusing us */
  mask &= ~GDK_LOCK_MASK;

  conflict_item = search_for_conflict_item (item, keyval, mask, keycode);

  /* Check for unmodified keys */
  if (!is_valid_binding (keyval, mask, keycode))
    {
      show_invalid_binding_dialog (view, keyval, mask, keycode);

      /* set it back to its previous value. */
      g_object_set (G_OBJECT (cell),
                    "accel-key", item->keyval,
                    "keycode", item->keycode,
                    "accel-mods", item->mask,
                    NULL);
      return;
    }

  reverse_item = cc_keyboard_item_get_reverse_item (item);

  /* flag to see if the new accelerator was in use by something */
  if ((conflict_item != NULL) && (conflict_item != reverse_item))
    {
      GtkResponseType response;

      response = show_conflict_item_dialog (view, item, conflict_item,
                                            keyval, mask, keycode);

      if (response == GTK_RESPONSE_ACCEPT)
        {
	  g_object_set (G_OBJECT (conflict_item), "binding", NULL, NULL);

          str = binding_name (keyval, keycode, mask, FALSE);
          g_object_set (G_OBJECT (item), "binding", str, NULL);

          g_free (str);
          if (reverse_item == NULL)
            return;
        }
      else
        {
          /* set it back to its previous value. */
          g_object_set (G_OBJECT (cell),
                        "accel-key", item->keyval,
                        "keycode", item->keycode,
                        "accel-mods", item->mask,
                        NULL);
          return;
        }

    }

  str = binding_name (keyval, keycode, mask, FALSE);
  g_object_set (G_OBJECT (item), "binding", str, NULL);

  g_free (str);

  if (reverse_item != NULL)
    handle_reverse_item (item, reverse_item, keyval, mask, keycode, view);
}

static void
accel_cleared_callback (GtkCellRendererText *cell,
                        const char          *path_string,
                        gpointer             data)
{
  GtkTreeView *view = (GtkTreeView *) data;
  GtkTreePath *path = gtk_tree_path_new_from_string (path_string);
  CcKeyboardItem *item;
  GtkTreeIter iter;
  GtkTreeModel *model;

  model = gtk_tree_view_get_model (view);
  gtk_tree_model_get_iter (model, &iter, path);
  gtk_tree_path_free (path);
  gtk_tree_model_get (model, &iter,
                      DETAIL_KEYENTRY_COLUMN, &item,
                      -1);

  /* sanity check */
  if (item == NULL)
    return;

  /* Unset the key */
  g_object_set (G_OBJECT (item), "binding", NULL, NULL);
}

static gchar *
find_free_settings_path ()
{
  char **used_names;
  char *dir = NULL;
  int i, num, n_names;

  used_names = g_settings_get_strv (binding_settings, "custom-keybindings");
  n_names = g_strv_length (used_names);

  for (num = 0; dir == NULL; num++)
    {
      char *tmp;
      gboolean found = FALSE;

      tmp = g_strdup_printf ("%s/custom%d/", CUSTOM_KEYS_BASENAME, num);
      for (i = 0; i < n_names && !found; i++)
        found = strcmp (used_names[i], tmp) == 0;

      if (!found)
        dir = tmp;
      else
        g_free (tmp);
    }

  return dir;
}

static void
add_custom_shortcut (GtkTreeView  *tree_view,
                     GtkTreeModel *model)
{
  CcKeyboardItem *item;
  GtkTreePath *path;
  gchar *settings_path;

  item = cc_keyboard_item_new (CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH);

  settings_path = find_free_settings_path ();
  cc_keyboard_item_load_from_gsettings_path (item, settings_path, TRUE);
  g_free (settings_path);

  item->model = model;

  if (edit_custom_shortcut (item) &&
      item->command && item->command[0])
    {
      GPtrArray *keys_array;
      GtkTreeIter iter;
      GHashTable *hash;
      GVariantBuilder builder;
      char **settings_paths;
      int i;

      hash = get_hash_for_group (BINDING_GROUP_USER);
      keys_array = g_hash_table_lookup (hash, CUSTOM_SHORTCUTS_ID);
      if (keys_array == NULL)
        {
          keys_array = g_ptr_array_new ();
          g_hash_table_insert (hash, g_strdup (CUSTOM_SHORTCUTS_ID), keys_array);
        }

      g_ptr_array_add (keys_array, item);

      gtk_list_store_append (GTK_LIST_STORE (model), &iter);
      gtk_list_store_set (GTK_LIST_STORE (model), &iter, DETAIL_KEYENTRY_COLUMN, item, -1);

      settings_paths = g_settings_get_strv (binding_settings, "custom-keybindings");
      g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
      for (i = 0; settings_paths[i]; i++)
        g_variant_builder_add (&builder, "s", settings_paths[i]);
      g_variant_builder_add (&builder, "s", item->gsettings_path);
      g_settings_set_value (binding_settings, "custom-keybindings",
                            g_variant_builder_end (&builder));

      /* make the new shortcut visible */
      path = gtk_tree_model_get_path (model, &iter);
      gtk_tree_view_expand_to_path (tree_view, path);
      gtk_tree_view_scroll_to_cell (tree_view, path, NULL, FALSE, 0, 0);
      gtk_tree_path_free (path);
    }
  else
    {
      g_object_unref (item);
    }
}

static void
shortcut_entry_changed (GtkEntry *entry,
                        gpointer  user_data)
{
  guint16 name_length;
  guint16 command_length;

  name_length = gtk_entry_get_text_length (GTK_ENTRY (custom_shortcut_name_entry));
  command_length = gtk_entry_get_text_length (GTK_ENTRY (custom_shortcut_command_entry));

  gtk_widget_set_sensitive (custom_shortcut_ok_button,
                            name_length > 0 && command_length > 0);
}

static void
add_button_clicked (GtkWidget  *button,
                    GtkBuilder *builder)
{
  GtkTreeView *treeview;
  GtkTreeModel *model;
  GtkTreeModel *section_model;
  GtkTreeIter iter;
  gboolean found, cont;

  treeview = GTK_TREE_VIEW (gtk_builder_get_object (builder,
                                                    "shortcut_treeview"));
  model = gtk_tree_view_get_model (treeview);

  /* Select the Custom Shortcuts section
   * before adding the shortcut itself */
  section_model = gtk_tree_view_get_model (GTK_TREE_VIEW (WID (builder, "section_treeview")));
  cont = gtk_tree_model_get_iter_first (section_model, &iter);
  found = FALSE;
  while (cont)
    {
      BindingGroupType group;

      gtk_tree_model_get (section_model, &iter,
                          SECTION_GROUP_COLUMN, &group,
                          -1);

      if (group == BINDING_GROUP_USER)
        {
          found = TRUE;
          break;
        }
      cont = gtk_tree_model_iter_next (section_model, &iter);
    }
  if (found)
    {
      GtkTreeSelection *selection;

      selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (WID (builder, "section_treeview")));
      gtk_tree_selection_select_iter (selection, &iter);
    }

  /* And add the shortcut */
  add_custom_shortcut (treeview, model);
}

static void
remove_button_clicked (GtkWidget  *button,
                       GtkBuilder *builder)
{
  GtkTreeView *treeview;
  GtkTreeModel *model;
  GtkTreeSelection *selection;
  GtkTreeIter iter;

  treeview = GTK_TREE_VIEW (gtk_builder_get_object (builder,
                                                    "shortcut_treeview"));
  model = gtk_tree_view_get_model (treeview);

  selection = gtk_tree_view_get_selection (treeview);
  if (gtk_tree_selection_get_selected (selection, NULL, &iter))
    {
      remove_custom_shortcut (model, &iter);
    }
}

static int
section_sort_item  (GtkTreeModel *model,
                    GtkTreeIter  *a,
                    GtkTreeIter  *b,
                    gpointer      data)
{
  char *a_desc;
  int   a_group;
  char *b_desc;
  int   b_group;
  int   ret;

  gtk_tree_model_get (model, a,
                      SECTION_DESCRIPTION_COLUMN, &a_desc,
                      SECTION_GROUP_COLUMN, &a_group,
                      -1);
  gtk_tree_model_get (model, b,
                      SECTION_DESCRIPTION_COLUMN, &b_desc,
                      SECTION_GROUP_COLUMN, &b_group,
                      -1);

  if (a_group == b_group && a_desc && b_desc)
    ret = g_utf8_collate (a_desc, b_desc);
  else
    ret = a_group - b_group;

  g_free (a_desc);
  g_free (b_desc);

  return ret;
}

static gboolean
sections_separator_func (GtkTreeModel *model,
                         GtkTreeIter  *iter,
                         gpointer      data)
{
  BindingGroupType type;

  gtk_tree_model_get (model, iter, SECTION_GROUP_COLUMN, &type, -1);

  return type == BINDING_GROUP_SEPARATOR;
}

static void
xkb_options_combo_changed (GtkCellRendererCombo *combo,
                           gchar                *model_path,
                           GtkTreeIter          *model_iter,
                           gpointer              data)
{
  GtkTreeView *shortcut_treeview;
  GtkTreeModel *shortcut_model;
  GtkTreeIter shortcut_iter;
  GtkTreeSelection *selection;
  CcKeyboardOption *option;
  ShortcutType type;
  GtkBuilder *builder = data;

  shortcut_treeview = GTK_TREE_VIEW (gtk_builder_get_object (builder, "shortcut_treeview"));
  selection = gtk_tree_view_get_selection (shortcut_treeview);
  if (!gtk_tree_selection_get_selected (selection, &shortcut_model, &shortcut_iter))
    return;

  gtk_tree_model_get (shortcut_model, &shortcut_iter,
                      DETAIL_KEYENTRY_COLUMN, &option,
                      DETAIL_TYPE_COLUMN, &type,
                      -1);

  if (type != SHORTCUT_TYPE_XKB_OPTION)
    return;

  cc_keyboard_option_set_selection (option, model_iter);
}

static gboolean
poke_xkb_option_row (GtkTreeModel *model,
                     GtkTreePath  *path,
                     GtkTreeIter  *iter,
                     gpointer      option)
{
  gpointer item;

  gtk_tree_model_get (model, iter,
                      DETAIL_KEYENTRY_COLUMN, &item,
                      -1);

  if (item != option)
    return FALSE;

  gtk_tree_model_row_changed (model, path, iter);
  return TRUE;
}

static void
xkb_option_changed (CcKeyboardOption *option,
                    gpointer          data)
{
  GtkTreeModel *model = data;

  gtk_tree_model_foreach (model, poke_xkb_option_row, option);
}

static void
setup_keyboard_options (GtkListStore *store)
{
  GList *l;

  for (l = cc_keyboard_option_get_all (); l; l = l->next)
    g_signal_connect (l->data, "changed",
                      G_CALLBACK (xkb_option_changed), store);
}

static void
setup_dialog (CcPanel *panel, GtkBuilder *builder)
{
  GtkCellRenderer *renderer;
  GtkTreeViewColumn *column;
  GtkWidget *widget;
  GtkTreeView *treeview;
  GtkTreeSelection *selection;
  GList *focus_chain;
  CcShell *shell;
  GtkListStore *model;
  GtkTreeModelSort *sort_model;
  GtkStyleContext *context;

  gtk_widget_set_size_request (GTK_WIDGET (panel), -1, 400);

  /* Setup the section treeview */
  treeview = GTK_TREE_VIEW (gtk_builder_get_object (builder, "section_treeview"));
  gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (treeview),
					sections_separator_func,
					panel,
					NULL);

  renderer = gtk_cell_renderer_text_new ();
  column = gtk_tree_view_column_new_with_attributes (_("Section"),
                                                     renderer,
                                                     "text", SECTION_DESCRIPTION_COLUMN,
                                                     NULL);
  g_object_set (renderer,
                "width-chars", 20,
                "ellipsize", PANGO_ELLIPSIZE_END,
                NULL);

  gtk_tree_view_append_column (treeview, column);

  model = gtk_list_store_new (SECTION_N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT);
  sort_model = GTK_TREE_MODEL_SORT (gtk_tree_model_sort_new_with_model (GTK_TREE_MODEL (model)));
  gtk_tree_view_set_model (treeview, GTK_TREE_MODEL (sort_model));
  g_object_unref (model);

  gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (sort_model),
                                   SECTION_DESCRIPTION_COLUMN,
                                   section_sort_item,
                                   panel,
                                   NULL);

  gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (sort_model),
                                        SECTION_DESCRIPTION_COLUMN,
                                        GTK_SORT_ASCENDING);
  g_object_unref (sort_model);

  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));

  gtk_tree_selection_set_mode (selection, GTK_SELECTION_BROWSE);

  g_signal_connect (selection, "changed",
                    G_CALLBACK (section_selection_changed), builder);
  section_selection_changed (selection, builder);

  /* Setup the shortcut treeview */
  treeview = GTK_TREE_VIEW (gtk_builder_get_object (builder,
                                                    "shortcut_treeview"));

  binding_settings = g_settings_new (BINDINGS_SCHEMA);

  renderer = gtk_cell_renderer_text_new ();
  g_object_set (G_OBJECT (renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);

  column = gtk_tree_view_column_new_with_attributes (NULL, renderer, NULL);
  gtk_tree_view_column_set_cell_data_func (column, renderer, description_set_func, NULL, NULL);
  gtk_tree_view_column_set_resizable (column, FALSE);
  gtk_tree_view_column_set_expand (column, TRUE);

  gtk_tree_view_append_column (treeview, column);

  renderer = (GtkCellRenderer *) g_object_new (GTK_TYPE_CELL_RENDERER_ACCEL,
                                               "accel-mode", GTK_CELL_RENDERER_ACCEL_MODE_OTHER,
                                               NULL);

  g_signal_connect (treeview, "button_press_event",
                    G_CALLBACK (start_editing_cb), renderer);
  g_signal_connect (treeview, "row-activated",
                    G_CALLBACK (start_editing_kb_cb), renderer);

  g_signal_connect (renderer, "accel_edited",
                    G_CALLBACK (accel_edited_callback),
                    treeview);
  g_signal_connect (renderer, "accel_cleared",
                    G_CALLBACK (accel_cleared_callback),
                    treeview);

  column = gtk_tree_view_column_new_with_attributes (NULL, renderer, NULL);
  gtk_tree_view_column_set_cell_data_func (column, renderer, accel_set_func, NULL, NULL);
  gtk_tree_view_column_set_resizable (column, FALSE);
  gtk_tree_view_column_set_expand (column, FALSE);

  renderer = (GtkCellRenderer *) g_object_new (GTK_TYPE_CELL_RENDERER_COMBO,
                                               "has-entry", FALSE,
                                               "text-column", XKB_OPTION_DESCRIPTION_COLUMN,
                                               "editable", TRUE,
                                               "ellipsize", PANGO_ELLIPSIZE_END,
                                               "width-chars", 25,
                                               NULL);
  g_signal_connect (renderer, "changed",
                    G_CALLBACK (xkb_options_combo_changed), builder);

  gtk_tree_view_column_pack_end (column, renderer, FALSE);

  gtk_tree_view_column_set_cell_data_func (column, renderer, accel_set_func, NULL, NULL);

  gtk_tree_view_append_column (treeview, column);

  model = gtk_list_store_new (DETAIL_N_COLUMNS, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_INT);
  gtk_tree_view_set_model (treeview, GTK_TREE_MODEL (model));
  g_object_unref (model);

  setup_keyboard_options (model);

  widget = GTK_WIDGET (gtk_builder_get_object (builder, "actions_swindow"));
  context = gtk_widget_get_style_context (widget);
  gtk_style_context_set_junction_sides (context, GTK_JUNCTION_BOTTOM);
  widget = GTK_WIDGET (gtk_builder_get_object (builder, "shortcut-toolbar"));
  context = gtk_widget_get_style_context (widget);
  gtk_style_context_set_junction_sides (context, GTK_JUNCTION_TOP);

  /* set up the focus chain */
  focus_chain = g_list_append (NULL, WID (builder, "sections_swindow"));
  focus_chain = g_list_append (focus_chain, WID (builder, "actions_swindow"));
  focus_chain = g_list_append (focus_chain, WID (builder, "shortcut-toolbar"));

  widget = GTK_WIDGET (gtk_builder_get_object (builder, "shortcuts_grid"));
  gtk_container_set_focus_chain (GTK_CONTAINER (widget), focus_chain);
  g_list_free (focus_chain);

  /* set up the dialog */
  shell = cc_panel_get_shell (CC_PANEL (panel));
  widget = cc_shell_get_toplevel (shell);

  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));
  g_signal_connect (selection, "changed",
                    G_CALLBACK (shortcut_selection_changed),
                    WID (builder, "remove-toolbutton"));

  /* setup the custom shortcut dialog */
  custom_shortcut_dialog = WID (builder,
                                "custom-shortcut-dialog");
  custom_shortcut_ok_button = WID (builder, "custom-shortcut-ok-button");
  custom_shortcut_name_entry = WID (builder,
                                    "custom-shortcut-name-entry");
  g_signal_connect (custom_shortcut_name_entry, "changed",
                    G_CALLBACK (shortcut_entry_changed), NULL);
  custom_shortcut_command_entry = WID (builder,
                                       "custom-shortcut-command-entry");
  g_signal_connect (custom_shortcut_command_entry, "changed",
                    G_CALLBACK (shortcut_entry_changed), NULL);
  g_signal_connect (WID (builder, "add-toolbutton"),
                    "clicked", G_CALLBACK (add_button_clicked), builder);
  g_signal_connect (WID (builder, "remove-toolbutton"),
                    "clicked", G_CALLBACK (remove_button_clicked), builder);

  gtk_dialog_set_default_response (GTK_DIALOG (custom_shortcut_dialog),
                                   GTK_RESPONSE_OK);

  gtk_window_set_transient_for (GTK_WINDOW (custom_shortcut_dialog),
                                GTK_WINDOW (widget));

  gtk_window_set_resizable (GTK_WINDOW (custom_shortcut_dialog), FALSE);
}

static void
on_window_manager_change (const char *wm_name, CcPanel *panel)
{
  reload_sections (panel);
}

void
keyboard_shortcuts_init (CcPanel *panel, GtkBuilder *builder)
{
  g_object_set_data (G_OBJECT (panel), "builder", builder);
#ifdef GDK_WINDOWING_X11
  if (GDK_IS_X11_DISPLAY (gdk_display_get_default ()))
    wm_changed_id = wm_common_register_window_manager_change ((GFunc) on_window_manager_change,
                                                              panel);
#endif
  pictures_regex = g_regex_new ("\\$PICTURES", 0, 0, NULL);
  setup_dialog (panel, builder);
  reload_sections (panel);
}

gboolean
keyboard_shortcuts_set_section (CcPanel *panel, const char *section)
{
  GtkBuilder *builder;
  GtkTreeModel *section_model;
  GtkTreeIter iter;
  gboolean found, cont;

  builder = g_object_get_data (G_OBJECT (panel), "builder");
  if (builder == NULL)
    {
      /* Remember the section name to be set later */
      g_object_set_data_full (G_OBJECT (panel), "section-to-set", g_strdup (section), g_free);
      return TRUE;
    }
  section_model = gtk_tree_view_get_model (GTK_TREE_VIEW (WID (builder, "section_treeview")));
  cont = gtk_tree_model_get_iter_first (section_model, &iter);
  found = FALSE;
  while (cont)
    {
      char *id;

      gtk_tree_model_get (section_model, &iter,
                          SECTION_ID_COLUMN, &id,
                          -1);

      if (g_strcmp0 (id, section) == 0)
        {
          found = TRUE;
          g_free (id);
          break;
        }
      g_free (id);
      cont = gtk_tree_model_iter_next (section_model, &iter);
    }
  if (found)
    {
      GtkTreeSelection *selection;

      selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (WID (builder, "section_treeview")));
      gtk_tree_selection_select_iter (selection, &iter);
    }
  else
    {
      g_warning ("Could not find section '%s' to switch to.", section);
    }

  return found;
}

void
keyboard_shortcuts_dispose (CcPanel *panel)
{
  if (kb_system_sections != NULL)
    {
      g_hash_table_destroy (kb_system_sections);
      kb_system_sections = NULL;
    }
  if (kb_apps_sections != NULL)
    {
      g_hash_table_destroy (kb_apps_sections);
      kb_apps_sections = NULL;
    }
  if (kb_user_sections != NULL)
    {
      g_hash_table_destroy (kb_user_sections);
      kb_user_sections = NULL;
    }
  if (pictures_regex != NULL)
    {
      g_regex_unref (pictures_regex);
      pictures_regex = NULL;
    }

  g_clear_object (&binding_settings);

  g_clear_pointer (&custom_shortcut_dialog, gtk_widget_destroy);

  if (wm_changed_id)
    {
      wm_common_unregister_window_manager_change (wm_changed_id);
      wm_changed_id = NULL;
    }

  cc_keyboard_option_clear_all ();
}
