/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */

/*
 * Copyright (C) 2001 Havoc Pennington, Copyright (C) 2002 Red Hat Inc.
 * Copyright (C) 2006 Elijah Newren
 * Copyright (C) 2008 Thomas Thurman
 * Copyright (C) 2010 Milan Bouchet-Valat, Copyright (C) 2011 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/>.
 */

/**
 * SECTION:prefs
 * @title: Preferences
 * @short_description: Mutter preferences
 */

#include "config.h"

#include <glib.h>
#include <gio/gio.h>
#include <string.h>
#include <stdlib.h>

#include "compositor/meta-plugin-manager.h"
#include "core/keybindings-private.h"
#include "core/meta-accel-parse.h"
#include "core/util-private.h"
#include "meta/prefs.h"
#include "x11/meta-x11-display-private.h"

/* If you add a key, it needs updating in init() and in the gsettings
 * notify listener and of course in the .schemas file.
 *
 * Keys which are handled by one of the unified handlers below are
 * not given a name here, because the purpose of the unified handlers
 * is that keys should be referred to exactly once.
 */
#define KEY_TITLEBAR_FONT "titlebar-font"
#define KEY_NUM_WORKSPACES "num-workspaces"
#define KEY_WORKSPACE_NAMES "workspace-names"

/* Keys from "foreign" schemas */
#define KEY_GNOME_ACCESSIBILITY "toolkit-accessibility"
#define KEY_GNOME_ANIMATIONS "enable-animations"
#define KEY_GNOME_CURSOR_THEME "cursor-theme"
#define KEY_GNOME_CURSOR_SIZE "cursor-size"
#define KEY_XKB_OPTIONS "xkb-options"

#define KEY_MOUSEWHEEL_ZOOM_ENABLED "screen-magnifier-enabled"
#define KEY_BRING_ACTIVATED_WINDOWS_TO_CURRENT_WORKSPACE "bring-windows-to-current-workspace"

#define KEY_OVERLAY_KEY "overlay-key"
#define KEY_WORKSPACES_ONLY_ON_PRIMARY "workspaces-only-on-primary"
#define KEY_LOCATE_POINTER "locate-pointer"

#define KEY_GTK_THEME "gtk-theme"

/* These are the different schemas we are keeping
 * a GSettings instance for */
#define SCHEMA_GENERAL         "org.cinnamon.desktop.wm.preferences"
#define SCHEMA_MUFFIN          "org.cinnamon.muffin"
#define SCHEMA_INTERFACE       "org.cinnamon.desktop.interface"
#define SCHEMA_INPUT_SOURCES   "org.cinnamon.desktop.input-sources"
#define SCHEMA_MOUSE           "org.cinnamon.desktop.peripherals.mouse"
#define SCHEMA_A11Y_APPLICATIONS "org.cinnamon.desktop.a11y.applications"

#define SETTINGS(s) g_hash_table_lookup (settings_schemas, (s))

static GList *changes = NULL;
static guint changed_idle;
static GList *listeners = NULL;
static GHashTable *settings_schemas;

static gboolean use_system_font = FALSE;
static PangoFontDescription *titlebar_font = NULL;
static MetaVirtualModifier mouse_button_mods = Mod1Mask;
static MetaVirtualModifier mouse_button_zoom_mods = Mod1Mask;
static gboolean mouse_zoom_enabled = FALSE;
static MetaKeyCombo overlay_key_combo = { 0, 0, 0 };
static MetaKeyCombo locate_pointer_key_combo = { 0, 0, 0 };
static CDesktopFocusMode focus_mode = C_DESKTOP_FOCUS_MODE_CLICK;
static CDesktopFocusNewWindows focus_new_windows = C_DESKTOP_FOCUS_NEW_WINDOWS_SMART;
static gboolean raise_on_click = TRUE;
static MetaPlacementMode new_window_placement_mode = META_PLACEMENT_MODE_AUTOMATIC;
static gboolean attach_modal_dialogs = FALSE;
static int num_workspaces = 4;
static gboolean workspace_cycle = FALSE;
static CDesktopTitlebarAction action_double_click_titlebar = C_DESKTOP_TITLEBAR_ACTION_TOGGLE_MAXIMIZE;
static CDesktopTitlebarAction action_middle_click_titlebar = C_DESKTOP_TITLEBAR_ACTION_LOWER;
static CDesktopTitlebarAction action_right_click_titlebar = C_DESKTOP_TITLEBAR_ACTION_MENU;
static CDesktopTitlebarScrollAction action_scroll_titlebar = C_DESKTOP_TITLEBAR_SCROLL_ACTION_NONE;
static int min_window_opacity = 0;
static gboolean dynamic_workspaces = FALSE;
static gboolean disable_workarounds = FALSE;
static gboolean auto_raise = FALSE;
static gboolean auto_raise_delay = 500;
static gboolean focus_change_on_pointer_rest = FALSE;
static gboolean bell_is_visible = FALSE;
static gboolean bell_is_audible = TRUE;
static gboolean gnome_accessibility = FALSE;
static gboolean gnome_animations = TRUE;
static gboolean locate_pointer_is_enabled = FALSE;
static unsigned int check_alive_timeout = 5000;
static char *cursor_theme = NULL;
/* cursor_size will, when running as an X11 compositing window manager, be the
 * actual cursor size, multiplied with the global window scaling factor. On
 * Wayland, it will be the actual cursor size retrieved from gsettings.
 */
static int   cursor_size = 24;
static int   draggable_border_width = 10;
static int   drag_threshold;
static gboolean resize_with_right_button = FALSE;
static gboolean edge_tiling = FALSE;
static gboolean force_fullscreen = TRUE;
static gboolean auto_maximize = TRUE;
static gboolean show_fallback_app_menu = TRUE;

static CDesktopVisualBellType visual_bell_type = C_DESKTOP_VISUAL_BELL_FULLSCREEN_FLASH;
static MetaButtonLayout button_layout;

/* NULL-terminated array */
static char **workspace_names = NULL;

static gboolean workspaces_only_on_primary = FALSE;

static char *iso_next_group_option = NULL;

static MetaX11BackgroundTransition background_transition = META_X11_BACKGROUND_TRANSITION_BLEND;
static gboolean unredirect_fullscreen_windows = FALSE;
static gboolean tile_maximize = FALSE;
static gboolean invert_workspace_flip = FALSE;
static char *gtk_theme = NULL;
static char *bell_sound = NULL;
static gboolean bring_user_activated_windows_to_current_workspace = FALSE;

static void handle_preference_update_enum (GSettings *settings,
                                           gchar     *key);
static gboolean update_binding         (MetaKeyPref *binding,
                                        gchar      **strokes);
static gboolean update_key_binding     (const char  *key,
                                        gchar      **strokes);
static void update_min_win_opacity (void);


static void settings_changed (GSettings      *settings,
                              gchar          *key,
                              gpointer        data);
static void bindings_changed (GSettings      *settings,
                              gchar          *key,
                              gpointer        data);

static void queue_changed (MetaPreference  pref);

static void maybe_give_disable_workarounds_warning (void);

static gboolean titlebar_handler (GVariant*, gpointer*, gpointer);
static gboolean mouse_button_mods_handler (GVariant*, gpointer*, gpointer);
static gboolean mouse_button_mods_zoom_handler (GVariant*, gpointer*, gpointer);
static gboolean button_layout_handler (GVariant*, gpointer*, gpointer);
static gboolean overlay_key_handler (GVariant*, gpointer*, gpointer);
static gboolean locate_pointer_key_handler (GVariant*, gpointer*, gpointer);

static gboolean iso_next_group_handler (GVariant*, gpointer*, gpointer);

static void     init_bindings             (void);

typedef struct
{
  MetaPrefsChangedFunc func;
  gpointer data;
} MetaPrefsListener;

typedef struct
{
  const char *key;
  const char *schema;
  MetaPreference pref;
} MetaBasePreference;

typedef struct
{
  MetaBasePreference base;
  gpointer target;
} MetaEnumPreference;

typedef struct
{
  MetaBasePreference base;
  gboolean *target;
} MetaBoolPreference;


/**
 * MetaStringPreference:
 * @handler: (nullable): A handler. Many of the string preferences
 * aren't stored as strings and need parsing; others of them have
 * default values which can't be solved in the general case.  If you
 * include a function pointer here, it will be called instead of writing
 * the string value out to the target variable.
 * The function will be passed to g_settings_get_mapped() and should
 * return %TRUE if the mapping was successful and %FALSE otherwise.
 * In the former case the function is expected to handle the result
 * of the conversion itself and call queue_changed() appropriately;
 * in particular the @result (out) parameter as returned by
 * g_settings_get_mapped() will be ignored in all cases.
 * This may be %NULL.  If it is, see "target", below.
 * @target: (nullable): Where to write the incoming string.
 * This must be %NULL if the handler is non-%NULL.
 * If the incoming string is %NULL, no change will be made.
 */
typedef struct
{
  MetaBasePreference base;
  GSettingsGetMapping handler;
  gchar **target;
} MetaStringPreference;

typedef struct
{
  MetaBasePreference base;
  GSettingsGetMapping handler;
  gchar ***target;
} MetaStringArrayPreference;

typedef struct
{
  MetaBasePreference base;
  gint *target;
} MetaIntPreference;

typedef struct
{
  MetaBasePreference base;
  unsigned int *target;
} MetaUintPreference;


/* All preferences that are not keybindings must be listed here,
 * plus in the GSettings schemas and the MetaPreference enum.
 */

/* FIXMEs: */
/* @@@ Don't use NULL lines at the end; glib can tell you how big it is */

static MetaEnumPreference preferences_enum[] =
  {
    {
      { "focus-new-windows",
        SCHEMA_GENERAL,
        META_PREF_FOCUS_NEW_WINDOWS,
      },
      &focus_new_windows,
    },
    {
      { "focus-mode",
        SCHEMA_GENERAL,
        META_PREF_FOCUS_MODE,
      },
      &focus_mode,
    },
    {
      { "placement-mode",
        SCHEMA_MUFFIN,
        META_PREF_NEW_WINDOW_PLACEMENT_MODE,
      },
      &new_window_placement_mode,
    },
    {
      { "visual-bell-type",
        SCHEMA_GENERAL,
        META_PREF_VISUAL_BELL_TYPE,
      },
      &visual_bell_type,
    },
    {
      { "action-double-click-titlebar",
        SCHEMA_GENERAL,
        META_PREF_ACTION_DOUBLE_CLICK_TITLEBAR,
      },
      &action_double_click_titlebar,
    },
    {
      { "action-middle-click-titlebar",
        SCHEMA_GENERAL,
        META_PREF_ACTION_MIDDLE_CLICK_TITLEBAR,
      },
      &action_middle_click_titlebar,
    },
    {
      { "action-right-click-titlebar",
        SCHEMA_GENERAL,
        META_PREF_ACTION_RIGHT_CLICK_TITLEBAR,
      },
      &action_right_click_titlebar,
    },
    {
      { "action-scroll-titlebar",
        SCHEMA_GENERAL,
        META_PREF_ACTION_SCROLL_WHEEL_TITLEBAR,
      },
      &action_scroll_titlebar,
    },
    {
      { "background-transition",
        SCHEMA_MUFFIN,
        META_PREF_BACKGROUND_TRANSITION,
      },
      &background_transition,
    },
    { { NULL, 0, 0 }, NULL },
  };

static MetaBoolPreference preferences_bool[] =
  {
    {
      { "attach-modal-dialogs",
        SCHEMA_MUFFIN,
        META_PREF_ATTACH_MODAL_DIALOGS,
      },
      &attach_modal_dialogs,
    },
    {
      { "raise-on-click",
        SCHEMA_GENERAL,
        META_PREF_RAISE_ON_CLICK,
      },
      &raise_on_click,
    },
    {
      { "titlebar-uses-system-font",
        SCHEMA_GENERAL,
        META_PREF_TITLEBAR_FONT, /* note! shares a pref */
      },
      &use_system_font,
    },
    {
      { "dynamic-workspaces",
        SCHEMA_MUFFIN,
        META_PREF_DYNAMIC_WORKSPACES,
      },
      &dynamic_workspaces,
    },
    {
      { "disable-workarounds",
        SCHEMA_GENERAL,
        META_PREF_DISABLE_WORKAROUNDS,
      },
      &disable_workarounds,
    },
    {
      { "auto-raise",
        SCHEMA_GENERAL,
        META_PREF_AUTO_RAISE,
      },
      &auto_raise,
    },
    {
      { "focus-change-on-pointer-rest",
        SCHEMA_MUFFIN,
        META_PREF_FOCUS_CHANGE_ON_POINTER_REST,
      },
      &focus_change_on_pointer_rest
    },
    {
      { "visual-bell",
        SCHEMA_GENERAL,
        META_PREF_VISUAL_BELL,
      },
      &bell_is_visible, /* FIXME: change the name: it's confusing */
    },
    {
      { "audible-bell",
        SCHEMA_GENERAL,
        META_PREF_AUDIBLE_BELL,
      },
      &bell_is_audible, /* FIXME: change the name: it's confusing */
    },
    {
      { KEY_GNOME_ACCESSIBILITY,
        SCHEMA_INTERFACE,
        META_PREF_GNOME_ACCESSIBILITY,
      },
      &gnome_accessibility,
    },
    {
      { KEY_GNOME_ANIMATIONS,
        SCHEMA_INTERFACE,
        META_PREF_GNOME_ANIMATIONS,
      },
      &gnome_animations,
    },
    {
      { "resize-with-right-button",
        SCHEMA_GENERAL,
        META_PREF_RESIZE_WITH_RIGHT_BUTTON,
      },
      &resize_with_right_button,
    },
    {
      { "edge-tiling",
        SCHEMA_MUFFIN,
        META_PREF_EDGE_TILING,
      },
      &edge_tiling,
    },
    {
      { "workspaces-only-on-primary",
        SCHEMA_MUFFIN,
        META_PREF_WORKSPACES_ONLY_ON_PRIMARY,
      },
      &workspaces_only_on_primary,
    },
       {
      { "invert-workspace-flip-direction",
        SCHEMA_MUFFIN,
        META_PREF_INVERT_WORKSPACE_FLIP_DIRECTION,
      },
      &invert_workspace_flip,
    },
    {
      { "auto-maximize",
        SCHEMA_MUFFIN,
        META_PREF_AUTO_MAXIMIZE,
      },
      &auto_maximize,
    },
    {
      { KEY_LOCATE_POINTER,
        SCHEMA_MOUSE,
        META_PREF_LOCATE_POINTER,
      },
      &locate_pointer_is_enabled,
    },
    {
      { "unredirect-fullscreen-windows",
        SCHEMA_MUFFIN,
        META_PREF_UNREDIRECT_FULLSCREEN_WINDOWS,
      },
      &unredirect_fullscreen_windows,
    },
    {
      { "workspace-cycle",
        SCHEMA_MUFFIN,
        META_PREF_WORKSPACE_CYCLE,
      },
      &workspace_cycle,
    },
    {
      { KEY_MOUSEWHEEL_ZOOM_ENABLED,
        SCHEMA_A11Y_APPLICATIONS,
        META_PREF_MOUSE_ZOOM_ENABLED,
      },
      &mouse_zoom_enabled,
    },
    {
      { "tile-maximize",
        SCHEMA_MUFFIN,
        META_PREF_TILE_MAXIMIZE,
      },
      &tile_maximize,
    },
    {
      { "bring-windows-to-current-workspace",
        SCHEMA_MUFFIN,
        META_PREF_BRING_WINDOWS_TO_CURRENT_WORKSPACE,
      },
      &bring_user_activated_windows_to_current_workspace,
    },
    { { NULL, 0, 0 }, NULL },
  };

static MetaStringPreference preferences_string[] =
  {
    {
      { "mouse-button-modifier",
        SCHEMA_GENERAL,
        META_PREF_MOUSE_BUTTON_MODS,
      },
      mouse_button_mods_handler,
      NULL,
    },
    {
      { "mouse-button-zoom-modifier",
        SCHEMA_GENERAL,
        META_PREF_MOUSE_BUTTON_ZOOM_MODS,
      },
      mouse_button_mods_zoom_handler,
      NULL,
    },
    {
      { KEY_TITLEBAR_FONT,
        SCHEMA_GENERAL,
        META_PREF_TITLEBAR_FONT,
      },
      titlebar_handler,
      NULL,
    },
    {
      { "button-layout",
        SCHEMA_GENERAL,
        META_PREF_BUTTON_LAYOUT,
      },
      button_layout_handler,
      NULL,
    },
    {
      { "cursor-theme",
        SCHEMA_INTERFACE,
        META_PREF_CURSOR_THEME,
      },
      NULL,
      &cursor_theme,
    },
    {
      { "overlay-key",
        SCHEMA_MUFFIN,
        META_PREF_KEYBINDINGS,
      },
      overlay_key_handler,
      NULL,
    },
    {
      { "locate-pointer-key",
        SCHEMA_MUFFIN,
        META_PREF_KEYBINDINGS,
      },
      locate_pointer_key_handler,
      NULL,
    },
    {
      { "gtk-theme",
        SCHEMA_INTERFACE,
        META_PREF_GTK_THEME,
      },
      NULL,
      &gtk_theme,
    },
    {
      { "bell-sound",
        SCHEMA_GENERAL,
        META_PREF_BELL_SOUND,
      },
      NULL,
      &bell_sound,
    },
    { { NULL, 0, 0 }, NULL },
  };

static MetaStringArrayPreference preferences_string_array[] =
  {
    {
      { KEY_WORKSPACE_NAMES,
        SCHEMA_GENERAL,
        META_PREF_WORKSPACE_NAMES,
      },
      NULL,
      &workspace_names,
    },
    {
      { KEY_XKB_OPTIONS,
        SCHEMA_INPUT_SOURCES,
        META_PREF_KEYBINDINGS,
      },
      iso_next_group_handler,
      NULL,
    },
    { { NULL, 0, 0 }, NULL },
  };

static MetaIntPreference preferences_int[] =
  {
    {
      { KEY_NUM_WORKSPACES,
        SCHEMA_GENERAL,
        META_PREF_NUM_WORKSPACES,
      },
      &num_workspaces
    },
    {
      { "auto-raise-delay",
        SCHEMA_GENERAL,
        META_PREF_AUTO_RAISE_DELAY,
      },
      &auto_raise_delay
    },
    {
      { "draggable-border-width",
        SCHEMA_MUFFIN,
        META_PREF_DRAGGABLE_BORDER_WIDTH,
      },
      &draggable_border_width
    },
    {
      { "drag-threshold",
        SCHEMA_MOUSE,
        META_PREF_DRAG_THRESHOLD,
      },
      &drag_threshold
    },
    {
      { "cursor-size",
        SCHEMA_INTERFACE,
        META_PREF_CURSOR_SIZE,
      },
      &cursor_size
    },
    { { NULL, 0, 0 }, NULL },
  };

static MetaUintPreference preferences_uint[] =
  {
    {
      { "check-alive-timeout",
        SCHEMA_MUFFIN,
        META_PREF_CHECK_ALIVE_TIMEOUT,
      },
      &check_alive_timeout,
    },
    { { NULL, 0, 0 }, NULL },
  };

static void
handle_preference_init_enum (void)
{
  MetaEnumPreference *cursor = preferences_enum;

  while (cursor->base.key != NULL)
    {
      if (cursor->target==NULL)
        continue;

      *((gint *) cursor->target) =
        g_settings_get_enum (SETTINGS (cursor->base.schema), cursor->base.key);

      ++cursor;
    }
}

static void
handle_preference_init_bool (void)
{
  MetaBoolPreference *cursor = preferences_bool;

  while (cursor->base.key != NULL)
    {
      if (cursor->target!=NULL)
        *cursor->target =
          g_settings_get_boolean (SETTINGS (cursor->base.schema),
                                  cursor->base.key);

      ++cursor;
    }

  maybe_give_disable_workarounds_warning ();
}

static void
handle_preference_init_string (void)
{
  MetaStringPreference *cursor = preferences_string;

  while (cursor->base.key != NULL)
    {
      char *value;

      /* Complex keys have a mapping function to check validity */
      if (cursor->handler)
        {
          if (cursor->target)
            meta_bug ("%s has both a target and a handler\n", cursor->base.key);

          g_settings_get_mapped (SETTINGS (cursor->base.schema),
                                 cursor->base.key, cursor->handler, NULL);
        }
      else
        {
          if (!cursor->target)
            meta_bug ("%s must have handler or target\n", cursor->base.key);

          g_free (*(cursor->target));

          value = g_settings_get_string (SETTINGS (cursor->base.schema),
                                         cursor->base.key);

          *(cursor->target) = value;
        }

      ++cursor;
    }
}

static void
handle_preference_init_string_array (void)
{
  MetaStringArrayPreference *cursor = preferences_string_array;

  while (cursor->base.key != NULL)
    {
      char **value;

      /* Complex keys have a mapping function to check validity */
      if (cursor->handler)
        {
          if (cursor->target)
            meta_bug ("%s has both a target and a handler\n", cursor->base.key);

          g_settings_get_mapped (SETTINGS (cursor->base.schema),
                                 cursor->base.key, cursor->handler, NULL);
        }
      else
        {
          if (!cursor->target)
            meta_bug ("%s must have handler or target\n", cursor->base.key);

          if (*(cursor->target))
            g_strfreev (*(cursor->target));

          value = g_settings_get_strv (SETTINGS (cursor->base.schema),
                                       cursor->base.key);

          *(cursor->target) = value;
        }

      ++cursor;
    }
}

static void
handle_preference_init_int (void)
{
  MetaIntPreference *cursor = preferences_int;


  while (cursor->base.key != NULL)
    {
      if (cursor->target)
        *cursor->target = g_settings_get_int (SETTINGS (cursor->base.schema),
                                              cursor->base.key);

      ++cursor;
    }
}

static void
handle_preference_init_uint (void)
{
  MetaUintPreference *cursor = preferences_uint;

  while (cursor->base.key != NULL)
    {
      if (cursor->target)
        *cursor->target = g_settings_get_uint (SETTINGS (cursor->base.schema),
                                               cursor->base.key);

      ++cursor;
    }
}

static void
handle_preference_update_enum (GSettings *settings,
                               gchar *key)
{
  MetaEnumPreference *cursor = preferences_enum;
  gint old_value;

  while (cursor->base.key != NULL && strcmp (key, cursor->base.key) != 0)
    ++cursor;

  if (cursor->base.key == NULL)
    /* Didn't recognise that key. */
    return;

  /* We need to know whether the value changes, so
   * store the current value away.
   */

  old_value = * ((gint *)cursor->target);
  *((gint *)cursor->target) =
    g_settings_get_enum (SETTINGS (cursor->base.schema), key);

  /* Did it change?  If so, tell the listeners about it. */
  if (old_value != *((gint *)cursor->target))
    queue_changed (cursor->base.pref);
}

static void
handle_preference_update_bool (GSettings *settings,
                               gchar *key)
{
  MetaBoolPreference *cursor = preferences_bool;
  gboolean old_value;

  while (cursor->base.key != NULL && strcmp (key, cursor->base.key) != 0)
    ++cursor;

  if (cursor->base.key == NULL || cursor->target == NULL)
    /* Unknown key or no work for us to do. */
    return;

  /* We need to know whether the value changes, so
   * store the current value away.
   */
  old_value = *((gboolean *) cursor->target);

  /* Now look it up... */
  *((gboolean *) cursor->target) =
    g_settings_get_boolean (SETTINGS (cursor->base.schema), key);

  /* Did it change?  If so, tell the listeners about it. */
  if (old_value != *((gboolean *)cursor->target))
    queue_changed (cursor->base.pref);

  if (cursor->base.pref==META_PREF_DISABLE_WORKAROUNDS)
    maybe_give_disable_workarounds_warning ();
}

static void
handle_preference_update_string (GSettings *settings,
                                 gchar *key)
{
  MetaStringPreference *cursor = preferences_string;
  char *value;
  gboolean inform_listeners = FALSE;

  while (cursor->base.key != NULL && strcmp (key, cursor->base.key) != 0)
    ++cursor;

  if (cursor->base.key==NULL)
    /* Didn't recognise that key. */
    return;

  /* Complex keys have a mapping function to check validity */
  if (cursor->handler)
    {
      if (cursor->target)
        meta_bug ("%s has both a target and a handler\n", cursor->base.key);

      g_settings_get_mapped (SETTINGS (cursor->base.schema),
                             cursor->base.key, cursor->handler, NULL);
    }
  else
    {
      if (!cursor->target)
        meta_bug ("%s must have handler or target\n", cursor->base.key);

      value = g_settings_get_string (SETTINGS (cursor->base.schema),
                                     cursor->base.key);

      inform_listeners = (g_strcmp0 (value, *(cursor->target)) != 0);

      g_free(*(cursor->target));

      *(cursor->target) = value;
    }

  if (inform_listeners)
    queue_changed (cursor->base.pref);
}

static void
handle_preference_update_string_array (GSettings *settings,
                                       gchar *key)
{
  MetaStringArrayPreference *cursor = preferences_string_array;
  gboolean inform_listeners = FALSE;

  while (cursor->base.key != NULL && strcmp (key, cursor->base.key) != 0)
    ++cursor;

  if (cursor->base.key==NULL)
    /* Didn't recognise that key. */
    return;

  /* Complex keys have a mapping function to check validity */
  if (cursor->handler)
    {
      if (cursor->target)
        meta_bug ("%s has both a target and a handler\n", cursor->base.key);

      g_settings_get_mapped (SETTINGS (cursor->base.schema),
                             cursor->base.key, cursor->handler, NULL);
    }
  else
    {
      char **values, **previous;
      int n_values, n_previous, i;

      if (!cursor->target)
        meta_bug ("%s must have handler or target\n", cursor->base.key);

      values = g_settings_get_strv (SETTINGS (cursor->base.schema),
                                    cursor->base.key);
      n_values = g_strv_length (values);
      previous = *(cursor->target);
      n_previous = previous ? g_strv_length (previous) : 0;

      inform_listeners = n_previous != n_values;
      for (i = 0; i < n_values && !inform_listeners; i++)
        inform_listeners = g_strcmp0 (values[i], previous[i]) != 0;

      if (*(cursor->target))
        g_strfreev (*(cursor->target));
      *(cursor->target) = values;
    }

  if (inform_listeners)
    queue_changed (cursor->base.pref);
}

static void
handle_preference_update_int (GSettings *settings,
                              gchar *key)
{
  MetaIntPreference *cursor = preferences_int;
  gint new_value;

  while (cursor->base.key != NULL && strcmp (key, cursor->base.key) != 0)
    ++cursor;

  if (cursor->base.key == NULL || cursor->target == NULL)
    /* Unknown key or no work for us to do. */
    return;

  new_value = g_settings_get_int (SETTINGS (cursor->base.schema), key);

  /* Did it change?  If so, tell the listeners about it. */
  if (*cursor->target != new_value)
    {
      *cursor->target = new_value;
      queue_changed (cursor->base.pref);
    }
}

static void
handle_preference_update_uint (GSettings *settings,
                               char *key)
{
  MetaUintPreference *cursor = preferences_uint;
  unsigned int new_value;

  while (cursor->base.key && strcmp (key, cursor->base.key) != 0)
    ++cursor;

  if (!cursor->base.key || !cursor->target)
    return;

  new_value = g_settings_get_uint (SETTINGS (cursor->base.schema), key);

  if (*cursor->target != new_value)
    {
      *cursor->target = new_value;
      queue_changed (cursor->base.pref);
    }
}


/****************************************************************************/
/* Listeners.                                                               */
/****************************************************************************/

/**
 * meta_prefs_add_listener: (skip)
 * @func: a #MetaPrefsChangedFunc
 * @user_data: data passed to the function
 *
 */
void
meta_prefs_add_listener (MetaPrefsChangedFunc func,
                         gpointer             user_data)
{
  MetaPrefsListener *l;

  l = g_new (MetaPrefsListener, 1);
  l->func = func;
  l->data = user_data;

  listeners = g_list_prepend (listeners, l);
}

/**
 * meta_prefs_remove_listener: (skip)
 * @func: a #MetaPrefsChangedFunc
 * @user_data: data passed to the function
 *
 */
void
meta_prefs_remove_listener (MetaPrefsChangedFunc func,
                            gpointer             user_data)
{
  GList *tmp;

  tmp = listeners;
  while (tmp != NULL)
    {
      MetaPrefsListener *l = tmp->data;

      if (l->func == func &&
          l->data == user_data)
        {
          g_free (l);
          listeners = g_list_delete_link (listeners, tmp);

          return;
        }

      tmp = tmp->next;
    }
}

static void
emit_changed (MetaPreference pref)
{
  GList *tmp;
  GList *copy;

  meta_topic (META_DEBUG_PREFS, "Notifying listeners that pref %s changed\n",
              meta_preference_to_string (pref));

  copy = g_list_copy (listeners);

  tmp = copy;

  while (tmp != NULL)
    {
      MetaPrefsListener *l = tmp->data;

      (* l->func) (pref, l->data);

      tmp = tmp->next;
    }

  g_list_free (copy);
}

static gboolean
changed_idle_handler (gpointer data)
{
  GList *tmp;
  GList *copy;

  changed_idle = 0;

  copy = g_list_copy (changes); /* reentrancy paranoia */

  g_list_free (changes);
  changes = NULL;

  tmp = copy;
  while (tmp != NULL)
    {
      MetaPreference pref = GPOINTER_TO_INT (tmp->data);

      emit_changed (pref);

      tmp = tmp->next;
    }

  g_list_free (copy);

  return FALSE;
}

static void
queue_changed (MetaPreference pref)
{
  meta_topic (META_DEBUG_PREFS, "Queueing change of pref %s\n",
              meta_preference_to_string (pref));

  if (g_list_find (changes, GINT_TO_POINTER (pref)) == NULL)
    changes = g_list_prepend (changes, GINT_TO_POINTER (pref));
  else
    meta_topic (META_DEBUG_PREFS, "Change of pref %s was already pending\n",
                meta_preference_to_string (pref));

  if (changed_idle == 0)
    {
      changed_idle = g_idle_add_full (META_PRIORITY_PREFS_NOTIFY,
                                      changed_idle_handler, NULL, NULL);
      g_source_set_name_by_id (changed_idle, "[muffin] changed_idle_handler");
    }
}


/****************************************************************************/
/* Initialisation.                                                          */
/****************************************************************************/

void
meta_prefs_init (void)
{
  GSettings *settings;

  settings_schemas = g_hash_table_new_full (g_str_hash, g_str_equal,
                                            g_free, g_object_unref);

  settings = g_settings_new (SCHEMA_GENERAL);
  g_signal_connect (settings, "changed", G_CALLBACK (settings_changed), NULL);
  g_hash_table_insert (settings_schemas, g_strdup (SCHEMA_GENERAL), settings);

  settings = g_settings_new (SCHEMA_MUFFIN);
  g_signal_connect (settings, "changed", G_CALLBACK (settings_changed), NULL);
  g_hash_table_insert (settings_schemas, g_strdup (SCHEMA_MUFFIN), settings);

  settings = g_settings_new (SCHEMA_MOUSE);
  g_signal_connect (settings, "changed", G_CALLBACK (settings_changed), NULL);
  g_hash_table_insert (settings_schemas, g_strdup (SCHEMA_MOUSE), settings);

  settings = g_settings_new (SCHEMA_A11Y_APPLICATIONS);
  g_signal_connect (settings, "changed::" KEY_MOUSEWHEEL_ZOOM_ENABLED,
                    G_CALLBACK (settings_changed), NULL);
  g_hash_table_insert (settings_schemas, g_strdup (SCHEMA_A11Y_APPLICATIONS), settings);

  /* Individual keys we watch outside of our schemas */
  settings = g_settings_new (SCHEMA_INTERFACE);
  g_signal_connect (settings, "changed::" KEY_GNOME_ACCESSIBILITY,
                    G_CALLBACK (settings_changed), NULL);
  g_signal_connect (settings, "changed::" KEY_GNOME_ANIMATIONS,
                    G_CALLBACK (settings_changed), NULL);
  g_signal_connect (settings, "changed::" KEY_GNOME_CURSOR_THEME,
                    G_CALLBACK (settings_changed), NULL);
  g_signal_connect (settings, "changed::" KEY_GNOME_CURSOR_SIZE,
                    G_CALLBACK (settings_changed), NULL);
  g_signal_connect (settings, "changed::" KEY_LOCATE_POINTER,
                    G_CALLBACK (settings_changed), NULL);
  g_signal_connect (settings, "changed::" KEY_GTK_THEME,
                    G_CALLBACK (settings_changed), NULL);
  g_hash_table_insert (settings_schemas, g_strdup (SCHEMA_INTERFACE), settings);

  settings = g_settings_new (SCHEMA_INPUT_SOURCES);
  g_signal_connect (settings, "changed::" KEY_XKB_OPTIONS,
                    G_CALLBACK (settings_changed), NULL);
  g_hash_table_insert (settings_schemas, g_strdup (SCHEMA_INPUT_SOURCES), settings);

  /* Pick up initial values. */

  handle_preference_init_enum ();
  handle_preference_init_bool ();
  handle_preference_init_string ();
  handle_preference_init_string_array ();
  handle_preference_init_int ();
  handle_preference_init_uint ();
  update_min_win_opacity ();

  init_bindings ();
}

static gboolean
find_pref (void                *prefs,
           size_t               pref_size,
           const char          *search_key,
           MetaBasePreference **pref)
{
  void *p = prefs;

  while (TRUE)
    {
      char **key = p;
      if (*key == NULL)
        break;

      if (strcmp (*key, search_key) == 0)
        {
          *pref = p;
          return TRUE;
        }

      p = (guchar *)p + pref_size;
    }

  return FALSE;
}


/****************************************************************************/
/* Updates.                                                                 */
/****************************************************************************/


static void
settings_changed (GSettings *settings,
                  gchar *key,
                  gpointer data)
{
  GVariant *value;
  const GVariantType *type;
  MetaEnumPreference *cursor;
  gboolean found_enum;

  value = g_settings_get_value (settings, key);
  type = g_variant_get_type (value);

  if (g_variant_type_equal (type, G_VARIANT_TYPE_BOOLEAN))
    handle_preference_update_bool (settings, key);
  else if (g_variant_type_equal (type, G_VARIANT_TYPE_INT32))
    {
      if (strcmp (key, "min-window-opacity") == 0)
        {
          update_min_win_opacity ();
          queue_changed (META_PREF_MIN_WIN_OPACITY);
        }
      else
        {
          handle_preference_update_int (settings, key);
        }
    }
  else if (g_variant_type_equal (type, G_VARIANT_TYPE_UINT32))
    handle_preference_update_uint (settings, key);
  else if (g_variant_type_equal (type, G_VARIANT_TYPE_STRING_ARRAY))
    handle_preference_update_string_array (settings, key);
  else if (g_variant_type_equal (type, G_VARIANT_TYPE_STRING))
    {
      cursor = preferences_enum;
      found_enum = FALSE;

      while (cursor->base.key != NULL)
        {

          if (strcmp (key, cursor->base.key) == 0)
            found_enum = TRUE;

          cursor++;
        }

      if (found_enum)
        handle_preference_update_enum (settings, key);
      else
        handle_preference_update_string (settings, key);
    }
  else
    {
      /* Unknown preference type. This quite likely simply isn't
       * a preference we track changes to. */
    }

  g_variant_unref (value);
}

static void
bindings_changed (GSettings *settings,
                  gchar *key,
                  gpointer data)
{
  gchar **strokes;
  strokes = g_settings_get_strv (settings, key);

  if (update_key_binding (key, strokes))
    queue_changed (META_PREF_KEYBINDINGS);

  g_strfreev (strokes);
}

/**
 * maybe_give_disable_workaround_warning:
 *
 * Special case: give a warning the first time disable_workarounds
 * is turned on.
 */
static void
maybe_give_disable_workarounds_warning (void)
{
  static gboolean first_disable = TRUE;

  if (first_disable && disable_workarounds)
    {
      first_disable = FALSE;

      meta_warning ("Workarounds for broken applications disabled. "
                    "Some applications may not behave properly.\n");
    }
}

MetaVirtualModifier
meta_prefs_get_mouse_button_mods  (void)
{
  return mouse_button_mods;
}

MetaVirtualModifier
meta_prefs_get_mouse_button_zoom_mods  (void)
{
  return mouse_button_zoom_mods;
}

gboolean
meta_prefs_get_mouse_zoom_enabled (void)
{
  return mouse_zoom_enabled;
}

CDesktopFocusMode
meta_prefs_get_focus_mode (void)
{
  return focus_mode;
}

CDesktopFocusNewWindows
meta_prefs_get_focus_new_windows (void)
{
  return focus_new_windows;
}

MetaPlacementMode
meta_prefs_get_new_window_placement_mode (void)
{
  return new_window_placement_mode;
}

gboolean
meta_prefs_get_attach_modal_dialogs (void)
{
  return attach_modal_dialogs;
}

gboolean
meta_prefs_get_raise_on_click (void)
{
  return raise_on_click;
}

gboolean
meta_prefs_get_bring_windows_to_current_workspace (void)
{
  /* Windows that the user activates (with a current timestamp) will
   * be brought to the currently active workspace if necessary. Normally
   * the user is brought to the window's workspace instead. */
  return bring_user_activated_windows_to_current_workspace;
}

gboolean
meta_prefs_get_show_fallback_app_menu (void)
{
  return show_fallback_app_menu;
}

void
meta_prefs_set_show_fallback_app_menu (gboolean whether)
{
  gboolean changed = FALSE;

  changed = (show_fallback_app_menu == !whether);

  show_fallback_app_menu = whether;

  if (changed)
    queue_changed (META_PREF_BUTTON_LAYOUT);
}

const char*
meta_prefs_get_cursor_theme (void)
{
  return cursor_theme;
}

int
meta_prefs_get_cursor_size (void)
{
  return cursor_size;
}


/****************************************************************************/
/* Handlers for string preferences.                                         */
/****************************************************************************/

static gboolean
titlebar_handler (GVariant *value,
                  gpointer *result,
                  gpointer data)
{
  PangoFontDescription *desc;
  const gchar *string_value;

  *result = NULL; /* ignored */
  string_value = g_variant_get_string (value, NULL);
  desc = pango_font_description_from_string (string_value);

  if (desc == NULL)
    {
      meta_warning ("Could not parse font description "
                    "\"%s\" from GSettings key %s\n",
                    string_value ? string_value : "(null)",
                    KEY_TITLEBAR_FONT);
      return FALSE;
    }

  /* Is the new description the same as the old? */
  if (titlebar_font &&
      pango_font_description_equal (desc, titlebar_font))
    {
      pango_font_description_free (desc);
    }
  else
    {
      if (titlebar_font)
        pango_font_description_free (titlebar_font);

      titlebar_font = desc;
      queue_changed (META_PREF_TITLEBAR_FONT);
    }

  return TRUE;
}

static gboolean
real_button_mods_handler (GVariant *value,
                           gpointer *result,
                           gpointer  data,
                           MetaVirtualModifier *modifier,
                           MetaPreference pref_id)
{
  MetaVirtualModifier mods;
  const gchar *string_value;

  *result = NULL; /* ignored */
  string_value = g_variant_get_string (value, NULL);

  if (!string_value || !meta_parse_modifier (string_value, &mods))
    {
      meta_topic (META_DEBUG_KEYBINDINGS,
                  "Failed to parse new GSettings value\n");

      meta_warning ("\"%s\" found in configuration database is "
                    "not a valid value for mouse button modifier\n",
                    string_value);

      return FALSE;
    }

  meta_topic (META_DEBUG_KEYBINDINGS,
              "Mouse button modifier has new GSettings value \"%s\"\n",
              string_value);

  if (mods != *modifier)
    {
      *modifier = mods;
      queue_changed (pref_id);
    }

  return TRUE;
}

static gboolean
mouse_button_mods_handler (GVariant *value,
                           gpointer *result,
                           gpointer  data)
{
  return real_button_mods_handler (value, result, data, &mouse_button_mods, META_PREF_MOUSE_BUTTON_MODS);
}

static gboolean
mouse_button_mods_zoom_handler (GVariant *value,
                                gpointer *result,
                                gpointer  data)
{
  return real_button_mods_handler (value, result, data, &mouse_button_zoom_mods, META_PREF_MOUSE_BUTTON_ZOOM_MODS);
}

static gboolean
button_layout_equal (const MetaButtonLayout *a,
                     const MetaButtonLayout *b)
{
  int i;

  i = 0;
  while (i < MAX_BUTTONS_PER_CORNER)
    {
      if (a->left_buttons[i] != b->left_buttons[i])
        return FALSE;
      if (a->right_buttons[i] != b->right_buttons[i])
        return FALSE;
      if (a->left_buttons_has_spacer[i] != b->left_buttons_has_spacer[i])
        return FALSE;
      if (a->right_buttons_has_spacer[i] != b->right_buttons_has_spacer[i])
        return FALSE;
      ++i;
    }

  return TRUE;
}

/*
 * This conversion cannot be handled by GSettings since
 * several values are stored in the same key (as a string).
 */
static MetaButtonFunction
button_function_from_string (const char *str)
{
  if (strcmp (str, "menu") == 0)
    return META_BUTTON_FUNCTION_MENU;
  else if (strcmp (str, "minimize") == 0)
    return META_BUTTON_FUNCTION_MINIMIZE;
  else if (strcmp (str, "maximize") == 0)
    return META_BUTTON_FUNCTION_MAXIMIZE;
  else if (strcmp (str, "close") == 0)
    return META_BUTTON_FUNCTION_CLOSE;
  else
    /* don't know; give up */
    return META_BUTTON_FUNCTION_LAST;
}

static gboolean
button_layout_handler (GVariant *value,
                       gpointer *result,
                       gpointer  data)
{
  MetaButtonLayout new_layout;
  const gchar *string_value;
  char **sides = NULL;
  int i;

  /* We need to ignore unknown button functions, for
   * compat with future versions
   */

  *result = NULL; /* ignored */
  string_value = g_variant_get_string (value, NULL);

  if (string_value)
    sides = g_strsplit (string_value, ":", 2);

  i = 0;
  if (sides != NULL && sides[0] != NULL)
    {
      char **buttons;
      int b;
      gboolean used[META_BUTTON_FUNCTION_LAST];

      while (i < META_BUTTON_FUNCTION_LAST)
        {
          used[i] = FALSE;
          new_layout.left_buttons_has_spacer[i] = FALSE;
          ++i;
        }

      buttons = g_strsplit (sides[0], ",", -1);
      i = 0;
      b = 0;
      while (buttons[b] != NULL)
        {
          MetaButtonFunction f = button_function_from_string (buttons[b]);
          if (i > 0 && strcmp("spacer", buttons[b]) == 0)
            {
              new_layout.left_buttons_has_spacer[i-1] = TRUE;
            }
          else
            {
              if (f != META_BUTTON_FUNCTION_LAST && !used[f])
                {
                  new_layout.left_buttons[i] = f;
                  used[f] = TRUE;
                  ++i;
                }
              else
                {
                  meta_topic (META_DEBUG_PREFS, "Ignoring unknown or already-used button name \"%s\"\n",
                              buttons[b]);
                }
            }

          ++b;
        }

      g_strfreev (buttons);
    }

  for (; i < MAX_BUTTONS_PER_CORNER; i++)
    {
      new_layout.left_buttons[i] = META_BUTTON_FUNCTION_LAST;
      new_layout.left_buttons_has_spacer[i] = FALSE;
    }

  i = 0;
  if (sides != NULL && sides[0] != NULL && sides[1] != NULL)
    {
      char **buttons;
      int b;
      gboolean used[META_BUTTON_FUNCTION_LAST];

      while (i < META_BUTTON_FUNCTION_LAST)
        {
          used[i] = FALSE;
          new_layout.right_buttons_has_spacer[i] = FALSE;
          ++i;
        }

      buttons = g_strsplit (sides[1], ",", -1);
      i = 0;
      b = 0;
      while (buttons[b] != NULL)
        {
          MetaButtonFunction f = button_function_from_string (buttons[b]);
          if (i > 0 && strcmp("spacer", buttons[b]) == 0)
            {
              new_layout.right_buttons_has_spacer[i-1] = TRUE;
            }
          else
            {
              if (f != META_BUTTON_FUNCTION_LAST && !used[f])
                {
                  new_layout.right_buttons[i] = f;
                  used[f] = TRUE;
                  ++i;
                }
              else
                {
                  meta_topic (META_DEBUG_PREFS, "Ignoring unknown or already-used button name \"%s\"\n",
                              buttons[b]);
                }
            }

          ++b;
        }

      g_strfreev (buttons);
    }

  for (; i < MAX_BUTTONS_PER_CORNER; i++)
    {
      new_layout.right_buttons[i] = META_BUTTON_FUNCTION_LAST;
      new_layout.right_buttons_has_spacer[i] = FALSE;
    }

  g_strfreev (sides);

  /* Invert the button layout for RTL languages */
  if (meta_get_locale_direction() == META_LOCALE_DIRECTION_RTL)
    {
      MetaButtonLayout rtl_layout;
      int j;

      for (i = 0; new_layout.left_buttons[i] != META_BUTTON_FUNCTION_LAST; i++);
      for (j = 0; j < i; j++)
        {
          rtl_layout.right_buttons[j] = new_layout.left_buttons[i - j - 1];
          if (j == 0)
            rtl_layout.right_buttons_has_spacer[i - 1] = new_layout.left_buttons_has_spacer[i - j - 1];
          else
            rtl_layout.right_buttons_has_spacer[j - 1] = new_layout.left_buttons_has_spacer[i - j - 1];
        }
      for (; j < MAX_BUTTONS_PER_CORNER; j++)
        {
          rtl_layout.right_buttons[j] = META_BUTTON_FUNCTION_LAST;
          rtl_layout.right_buttons_has_spacer[j] = FALSE;
        }

      for (i = 0; new_layout.right_buttons[i] != META_BUTTON_FUNCTION_LAST; i++);
      for (j = 0; j < i; j++)
        {
          rtl_layout.left_buttons[j] = new_layout.right_buttons[i - j - 1];
          if (j == 0)
            rtl_layout.left_buttons_has_spacer[i - 1] = new_layout.right_buttons_has_spacer[i - j - 1];
          else
            rtl_layout.left_buttons_has_spacer[j - 1] = new_layout.right_buttons_has_spacer[i - j - 1];
        }
      for (; j < MAX_BUTTONS_PER_CORNER; j++)
        {
          rtl_layout.left_buttons[j] = META_BUTTON_FUNCTION_LAST;
          rtl_layout.left_buttons_has_spacer[j] = FALSE;
        }

      new_layout = rtl_layout;
    }

  if (!button_layout_equal (&button_layout, &new_layout))
    {
      button_layout = new_layout;
      emit_changed (META_PREF_BUTTON_LAYOUT);
    }

  return TRUE;
}

static gboolean
overlay_key_handler (GVariant *value,
                     gpointer *result,
                     gpointer  data)
{
  MetaKeyCombo combo;
  const gchar *string_value;

  *result = NULL; /* ignored */
  string_value = g_variant_get_string (value, NULL);

  if (string_value && meta_parse_accelerator (string_value, &combo))
    ;
  else
    {
      meta_topic (META_DEBUG_KEYBINDINGS,
                  "Failed to parse value for overlay-key\n");
      return FALSE;
    }

  combo.modifiers = 0;

  if (overlay_key_combo.keysym != combo.keysym ||
      overlay_key_combo.keycode != combo.keycode)
    {
      overlay_key_combo = combo;
      queue_changed (META_PREF_KEYBINDINGS);
    }

  return TRUE;
}

static gboolean
locate_pointer_key_handler (GVariant *value,
                            gpointer *result,
                            gpointer  data)
{
  MetaKeyCombo *combo;
  const gchar **string_values;
  gboolean output;
  int i;

  *result = NULL; /* ignored */
  string_values = g_variant_get_strv (value, NULL);
  output = TRUE;

  for (i = 0; string_values && string_values[i]; i++)
    {

      combo = g_malloc0 (sizeof(MetaKeyCombo));

      if (!meta_parse_accelerator (string_values[i], combo))
        {
          meta_topic (META_DEBUG_KEYBINDINGS,
                      "Failed to parse keybinding value \"%s\" for locate-pointer-key\n",
                      string_values[i]);

          g_free (combo);

          output = FALSE;
        }

      combo->modifiers = 0;
    }

  if (locate_pointer_key_combo.keysym != combo->keysym ||
      locate_pointer_key_combo.keycode != combo->keycode)
    {
      locate_pointer_key_combo = *combo;
      queue_changed (META_PREF_KEYBINDINGS);
    }

  g_free (string_values);

  return output;
}

static gboolean
iso_next_group_handler (GVariant *value,
                        gpointer *result,
                        gpointer  data)
{
  const char **xkb_options, **p;
  const char *option = NULL;
  gboolean changed = FALSE;

  *result = NULL; /* ignored */
  xkb_options = g_variant_get_strv (value, NULL);

  for (p = xkb_options; p && *p; ++p)
    if (g_str_has_prefix (*p, "grp:"))
      {
        option = (*p + 4);
        break;
      }

  changed = (g_strcmp0 (option, iso_next_group_option) != 0);

  if (changed)
    {
      g_free (iso_next_group_option);
      iso_next_group_option = g_strdup (option);
      queue_changed (META_PREF_KEYBINDINGS);
    }

  g_free (xkb_options);

  return TRUE;
}

const PangoFontDescription*
meta_prefs_get_titlebar_font (void)
{
  if (use_system_font)
    return NULL;
  else
    return titlebar_font;
}

int
meta_prefs_get_num_workspaces (void)
{
  return num_workspaces;
}

gboolean
meta_prefs_get_dynamic_workspaces (void)
{
  return dynamic_workspaces;
}

gboolean
meta_prefs_get_disable_workarounds (void)
{
  return disable_workarounds;
}

gboolean
meta_prefs_get_unredirect_fullscreen_windows (void)
{
  return unredirect_fullscreen_windows;
}

gboolean
meta_prefs_get_workspace_cycle (void)
{
  return workspace_cycle;
}

#ifdef WITH_VERBOSE_MODE
const char*
meta_preference_to_string (MetaPreference pref)
{
  /* TODO: better handled via GLib enum nicknames */
  switch (pref)
    {
    case META_PREF_MOUSE_BUTTON_MODS:
      return "MOUSE_BUTTON_MODS";

    case META_PREF_MOUSE_BUTTON_ZOOM_MODS:
      return "MOUSE_BUTTON_ZOOM_MODS";

    case META_PREF_FOCUS_MODE:
      return "FOCUS_MODE";

    case META_PREF_FOCUS_NEW_WINDOWS:
      return "FOCUS_NEW_WINDOWS";

    case META_PREF_NEW_WINDOW_PLACEMENT_MODE:
      return "NEW_WINDOW_PLACEMENT_MODE";

    case META_PREF_ATTACH_MODAL_DIALOGS:
      return "ATTACH_MODAL_DIALOGS";

    case META_PREF_RAISE_ON_CLICK:
      return "RAISE_ON_CLICK";

    case META_PREF_TITLEBAR_FONT:
      return "TITLEBAR_FONT";

    case META_PREF_NUM_WORKSPACES:
      return "NUM_WORKSPACES";

    case META_PREF_KEYBINDINGS:
      return "KEYBINDINGS";

    case META_PREF_DISABLE_WORKAROUNDS:
      return "DISABLE_WORKAROUNDS";

    case META_PREF_ACTION_DOUBLE_CLICK_TITLEBAR:
      return "ACTION_DOUBLE_CLICK_TITLEBAR";

    case META_PREF_ACTION_MIDDLE_CLICK_TITLEBAR:
      return "ACTION_MIDDLE_CLICK_TITLEBAR";

    case META_PREF_ACTION_RIGHT_CLICK_TITLEBAR:
      return "ACTION_RIGHT_CLICK_TITLEBAR";

    case META_PREF_ACTION_SCROLL_WHEEL_TITLEBAR:
      return "ACTION_SCROLL_WHEEL_TITLEBAR";

    case META_PREF_AUTO_RAISE:
      return "AUTO_RAISE";

    case META_PREF_AUTO_RAISE_DELAY:
      return "AUTO_RAISE_DELAY";

    case META_PREF_FOCUS_CHANGE_ON_POINTER_REST:
      return "FOCUS_CHANGE_ON_POINTER_REST";

    case META_PREF_BUTTON_LAYOUT:
      return "BUTTON_LAYOUT";

    case META_PREF_WORKSPACE_NAMES:
      return "WORKSPACE_NAMES";

    case META_PREF_VISUAL_BELL:
      return "VISUAL_BELL";

    case META_PREF_AUDIBLE_BELL:
      return "AUDIBLE_BELL";

    case META_PREF_VISUAL_BELL_TYPE:
      return "VISUAL_BELL_TYPE";

    case META_PREF_GNOME_ACCESSIBILITY:
      return "GNOME_ACCESSIBILTY";

    case META_PREF_GNOME_ANIMATIONS:
      return "GNOME_ANIMATIONS";

    case META_PREF_CURSOR_THEME:
      return "CURSOR_THEME";

    case META_PREF_CURSOR_SIZE:
      return "CURSOR_SIZE";

    case META_PREF_RESIZE_WITH_RIGHT_BUTTON:
      return "RESIZE_WITH_RIGHT_BUTTON";

    case META_PREF_EDGE_TILING:
      return "EDGE_TILING";

    case META_PREF_FORCE_FULLSCREEN:
      return "FORCE_FULLSCREEN";

    case META_PREF_WORKSPACES_ONLY_ON_PRIMARY:
      return "WORKSPACES_ONLY_ON_PRIMARY";

    case META_PREF_DRAGGABLE_BORDER_WIDTH:
      return "DRAGGABLE_BORDER_WIDTH";

    case META_PREF_DRAG_THRESHOLD:
      return "DRAG_THRESHOLD";

    case META_PREF_DYNAMIC_WORKSPACES:
      return "DYNAMIC_WORKSPACES";

    case META_PREF_AUTO_MAXIMIZE:
      return "AUTO_MAXIMIZE";

    case META_PREF_LOCATE_POINTER:
      return "LOCATE_POINTER";

    case META_PREF_CHECK_ALIVE_TIMEOUT:
      return "CHECK_ALIVE_TIMEOUT";

    case META_PREF_UNREDIRECT_FULLSCREEN_WINDOWS:
      return "UNREDIRECT_FULLSCREEN_WINDOWS";

    case META_PREF_WORKSPACE_CYCLE:
      return "WORKSPACE_CYCLE";

    case META_PREF_MIN_WIN_OPACITY:
      return "MIN_WIN_OPACITY";

    case META_PREF_MOUSE_ZOOM_ENABLED:
      return "MOUSE_ZOOM_ENABLED";

    case META_PREF_TILE_MAXIMIZE:
      return "TILE_MAXIMIZE";

    case META_PREF_GTK_THEME:
      return "GTK_THEME";

    case META_PREF_BELL_SOUND:
      return "BELL_SOUND";

    case META_PREF_BRING_WINDOWS_TO_CURRENT_WORKSPACE:
      return "BRING_WINDOWS_TO_CURRENT_WORKSPACE";
    }

  return "(unknown)";
}
#endif /* WITH_VERBOSE_MODE */

void
meta_prefs_set_num_workspaces (int n_workspaces)
{
  MetaBasePreference *pref;

  if (find_pref (preferences_int, sizeof(MetaIntPreference),
                 KEY_NUM_WORKSPACES, &pref))
    {
      g_settings_set_int (SETTINGS (pref->schema),
                          KEY_NUM_WORKSPACES,
                          n_workspaces);
    }
}

static GHashTable *key_bindings;

static void
meta_key_pref_free (MetaKeyPref *pref)
{
  update_binding (pref, NULL);

  g_free (pref->name);
  g_clear_object (&pref->settings);

  g_free (pref);
}


static void
init_bindings (void)
{
  MetaKeyPref *pref;

  key_bindings = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
                                        (GDestroyNotify)meta_key_pref_free);
}

static gboolean
update_binding (MetaKeyPref *binding,
                gchar      **strokes)
{
  GSList *old_combos, *a, *b;
  gboolean changed;
  int i;

  meta_topic (META_DEBUG_KEYBINDINGS,
              "Binding \"%s\" has new GSettings value\n",
              binding->name);

  old_combos = binding->combos;
  binding->combos = NULL;

  for (i = 0; strokes && strokes[i]; i++)
    {
      MetaKeyCombo *combo;

      combo = g_malloc0 (sizeof (MetaKeyCombo));

      if (!meta_parse_accelerator (strokes[i], combo))
        {
          meta_topic (META_DEBUG_KEYBINDINGS,
                      "Failed to parse new GSettings value\n");
          meta_warning ("\"%s\" found in configuration database is not a valid value for keybinding \"%s\"\n",
                        strokes[i], binding->name);

          g_free (combo);

          /* Value is kept and will thus be removed next time we save the key.
           * Changing the key in response to a modification could lead to cyclic calls. */
          continue;
        }

      binding->combos = g_slist_prepend (binding->combos, combo);
    }

  binding->combos = g_slist_reverse (binding->combos);

  a = old_combos;
  b = binding->combos;
  while (TRUE)
    {
      if ((!a && b) || (a && !b))
        {
          changed = TRUE;
          break;
        }
      else if (!a && !b)
        {
          changed = FALSE;
          break;
        }
      else if (memcmp (a->data, b->data, sizeof (MetaKeyCombo)) != 0)
        {
          changed = TRUE;
          break;
        }
      else
        {
          a = a->next;
          b = b->next;
        }
    }

  g_slist_free_full (old_combos, g_free);

  return changed;
}

static gboolean
update_key_binding (const char *key,
                    gchar     **strokes)
{
  MetaKeyPref *pref = g_hash_table_lookup (key_bindings, key);

  if (pref)
    return update_binding (pref, strokes);
  else
    return FALSE;
}

static void
update_min_win_opacity (void)
{
  int pct;
  int mapped;

  pct = g_settings_get_int (SETTINGS (SCHEMA_GENERAL), "min-window-opacity");
  mapped = (int)(((double)pct / 100.0) * 255.0);

  min_window_opacity = CLAMP (mapped, 0, 255);
}

const char*
meta_prefs_get_workspace_name (int i)
{
  const char *name;

  if (!workspace_names ||
      g_strv_length (workspace_names) < (guint)i + 1 ||
      !*workspace_names[i])
    {
      char *generated_name = g_strdup_printf (_("Workspace %d"), i + 1);
      name = g_intern_string (generated_name);
      g_free (generated_name);
    }
  else
    name = workspace_names[i];

  meta_topic (META_DEBUG_PREFS,
              "Getting name of workspace %d: \"%s\"\n", i, name);

  return name;
}

void
meta_prefs_change_workspace_name (int         num,
                                  const char *name)
{
  GVariantBuilder builder;
  int n_workspace_names, i;

  g_return_if_fail (num >= 0);

  meta_topic (META_DEBUG_PREFS,
              "Changing name of workspace %d to %s\n",
              num, name ? name : "none");

  /* NULL and empty string both mean "default" here,
   * and we also need to match the name against its default value
   * to avoid saving it literally. */
  if (g_strcmp0 (name, meta_prefs_get_workspace_name (num)) == 0)
    {
      if (!name || !*name)
        meta_topic (META_DEBUG_PREFS,
                    "Workspace %d already uses default name\n", num);
      else
        meta_topic (META_DEBUG_PREFS,
                    "Workspace %d already has name %s\n", num, name);
      return;
    }

  g_variant_builder_init (&builder, G_VARIANT_TYPE_STRING_ARRAY);
  n_workspace_names = workspace_names ? g_strv_length (workspace_names) : 0;

  for (i = 0; i < MAX (num + 1, n_workspace_names); i++)
    {
      const char *value;

      if (i == num)
        value = name ? name : "";
      else if (i < n_workspace_names)
        value = workspace_names[i] ? workspace_names[i] : "";
      else
        value = "";

      g_variant_builder_add (&builder, "s", value);
    }

  g_settings_set_value (SETTINGS (SCHEMA_GENERAL), KEY_WORKSPACE_NAMES,
                        g_variant_builder_end (&builder));
}

/**
 * meta_prefs_get_button_layout:
 * @button_layout: (out):
 */
void
meta_prefs_get_button_layout (MetaButtonLayout *button_layout_p)
{
  *button_layout_p = button_layout;
}

gboolean
meta_prefs_get_visual_bell (void)
{
  return bell_is_visible;
}

gboolean
meta_prefs_bell_is_audible (void)
{
  return bell_is_audible;
}

CDesktopVisualBellType
meta_prefs_get_visual_bell_type (void)
{
  return visual_bell_type;
}

gboolean
meta_prefs_add_keybinding (const char           *name,
                           GSettings            *settings,
                           const gchar         **bindings,
                           MetaKeyBindingAction  action,
                           MetaKeyBindingFlags   flags)
{
  MetaKeyPref  *pref;
  char        **strokes = NULL;
  guint         id;

  if (g_hash_table_lookup (key_bindings, name))
    {
      meta_warning ("Trying to re-add keybinding \"%s\".\n", name);
      return FALSE;
    }

  pref = g_new0 (MetaKeyPref, 1);
  pref->name = g_strdup (name);
  pref->settings = settings != NULL ? g_object_ref (settings) : NULL;
  pref->action = action;
  pref->combos = NULL;
  pref->builtin = (flags & META_KEY_BINDING_BUILTIN) != 0;

  if (pref->builtin)
    {
      if (g_object_get_data (G_OBJECT (settings), "changed-signal") == NULL)
        {
          id = g_signal_connect (settings, "changed",
                                 G_CALLBACK (bindings_changed), NULL);
          g_object_set_data (G_OBJECT (settings), "changed-signal", GUINT_TO_POINTER (id));
        }

      strokes = g_settings_get_strv (settings, name);
    }
  else
    {
      if (pref->settings != NULL)
        {
          char *changed_signal = g_strdup_printf ("changed::%s", name);
          id = g_signal_connect (settings, changed_signal,
                                 G_CALLBACK (bindings_changed), NULL);
          g_free (changed_signal);

          g_object_set_data (G_OBJECT (settings), name, GUINT_TO_POINTER (id));

          strokes = g_settings_get_strv (settings, name);
        }
      else
        {
          strokes = g_strdupv((gchar **)bindings);
        }

      queue_changed (META_PREF_KEYBINDINGS);
    }

  update_binding (pref, strokes);
  g_strfreev (strokes);

  g_hash_table_insert (key_bindings, g_strdup (name), pref);

  return TRUE;
}

gboolean
meta_prefs_remove_keybinding (const char *name)
{
  MetaKeyPref *pref;
  gulong id;

  pref = g_hash_table_lookup (key_bindings, name);
  if (!pref)
    {
      meta_warning ("Trying to remove non-existent keybinding \"%s\".\n", name);
      return FALSE;
    }

  if (pref->builtin)
    {
      meta_warning ("Trying to remove builtin keybinding \"%s\".\n", name);
      return FALSE;
    }

  if (pref->settings != NULL)
    {
      id = GPOINTER_TO_UINT (g_object_steal_data (G_OBJECT (pref->settings), name));
      g_clear_signal_handler (&id, pref->settings);
    }

  g_hash_table_remove (key_bindings, name);

  queue_changed (META_PREF_KEYBINDINGS);

  return TRUE;
}

GList *
meta_prefs_get_keybindings (void)
{
  return g_hash_table_get_values (key_bindings);
}

void
meta_prefs_get_overlay_binding (MetaKeyCombo *combo)
{
  *combo = overlay_key_combo;
}

void
meta_prefs_get_locate_pointer_binding (MetaKeyCombo *combo)
{
  *combo = locate_pointer_key_combo;
}

gboolean
meta_prefs_is_locate_pointer_enabled (void)
{
  return locate_pointer_is_enabled;
}

unsigned int
meta_prefs_get_check_alive_timeout (void)
{
  return check_alive_timeout;
}

const char *
meta_prefs_get_iso_next_group_option (void)
{
  return iso_next_group_option;
}

CDesktopTitlebarAction
meta_prefs_get_action_double_click_titlebar (void)
{
  return action_double_click_titlebar;
}

CDesktopTitlebarAction
meta_prefs_get_action_middle_click_titlebar (void)
{
  return action_middle_click_titlebar;
}

CDesktopTitlebarAction
meta_prefs_get_action_right_click_titlebar (void)
{
  return action_right_click_titlebar;
}

CDesktopTitlebarScrollAction
meta_prefs_get_action_scroll_wheel_titlebar (void)
{
  return action_scroll_titlebar;
}

gboolean
meta_prefs_get_auto_raise (void)
{
  return auto_raise;
}

int
meta_prefs_get_auto_raise_delay (void)
{
  return auto_raise_delay;
}

gboolean
meta_prefs_get_focus_change_on_pointer_rest (void)
{
  return focus_change_on_pointer_rest;
}

gboolean
meta_prefs_get_gnome_accessibility (void)
{
  return gnome_accessibility;
}

gboolean
meta_prefs_get_gnome_animations (void)
{
  return gnome_animations;
}

gboolean
meta_prefs_get_edge_tiling (void)
{
  return edge_tiling;
}

gboolean
meta_prefs_get_tile_maximize (void)
{
    return tile_maximize;
}

gboolean
meta_prefs_get_auto_maximize (void)
{
  return auto_maximize;
}

gboolean
meta_prefs_get_invert_flip_direction (void)
{
    return invert_workspace_flip;
}

MetaKeyBindingAction
meta_prefs_get_keybinding_action (const char *name)
{
  MetaKeyPref *pref = g_hash_table_lookup (key_bindings, name);

  return pref ? pref->action
              : META_KEYBINDING_ACTION_NONE;
}

gint
meta_prefs_get_mouse_button_resize (void)
{
  return resize_with_right_button ? 3: 2;
}

gint
meta_prefs_get_mouse_button_menu (void)
{
  return resize_with_right_button ? 2: 3;
}

gboolean
meta_prefs_get_force_fullscreen (void)
{
  return force_fullscreen;
}

gboolean
meta_prefs_get_workspaces_only_on_primary (void)
{
  return workspaces_only_on_primary;
}

int
meta_prefs_get_draggable_border_width (void)
{
  return draggable_border_width;
}

int
meta_prefs_get_drag_threshold (void)
{
  return drag_threshold;
}

void
meta_prefs_set_force_fullscreen (gboolean whether)
{
  force_fullscreen = whether;
}

MetaX11BackgroundTransition
meta_prefs_get_background_transition (void)
{
  return background_transition;
}

gint
meta_prefs_get_min_win_opacity (void)
{
  return min_window_opacity;
}

const char *
meta_prefs_get_bell_sound (void)
{
    return bell_sound;
}
