/*
 * Copyright (C) 2014-2020 Red Hat
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 * Written by:
 *     Carlos Garnacho <carlosg@gnome.org>
 */

#include "config.h"

#include <glib/gi18n-lib.h>
#include <gsettings-desktop-schemas/gdesktop-enums.h>

#ifdef HAVE_LIBWACOM
#include <libwacom/libwacom.h>
#endif

#include "core/meta-pad-action-mapper.h"
#include "backends/meta-input-device-private.h"
#include "backends/meta-logical-monitor.h"
#include "backends/meta-monitor.h"
#include "core/display-private.h"

typedef struct _PadMappingInfo PadMappingInfo;

struct _PadMappingInfo
{
  ClutterInputDevice *device;
  GSettings *settings;
  guint *group_modes;
};

typedef enum
{
  META_PAD_DIRECTION_NONE = -1,
  META_PAD_DIRECTION_UP = 0,
  META_PAD_DIRECTION_DOWN,
  META_PAD_DIRECTION_CW,
  META_PAD_DIRECTION_CCW,
} MetaPadDirection;

struct _MetaPadActionMapper
{
  GObject parent_class;

  GHashTable *pads;
  ClutterSeat *seat;
  ClutterVirtualInputDevice *virtual_pad_keyboard;
  MetaMonitorManager *monitor_manager;

  /* Pad ring/strip emission */
  struct {
    ClutterInputDevice *pad;
    MetaPadActionType action;
    guint number;
    double value;
  } last_pad_action_info;
};

G_DEFINE_TYPE (MetaPadActionMapper, meta_pad_action_mapper, G_TYPE_OBJECT)

static MetaDisplay *
display_from_mapper (MetaPadActionMapper *mapper)
{
  MetaBackend *backend =
    meta_monitor_manager_get_backend (mapper->monitor_manager);
  MetaContext *context = meta_backend_get_context (backend);

  return meta_context_get_display (context);
}

static void
meta_pad_action_mapper_finalize (GObject *object)
{
  MetaPadActionMapper *mapper = META_PAD_ACTION_MAPPER (object);

  g_hash_table_unref (mapper->pads);
  g_object_unref (mapper->monitor_manager);
  g_clear_object (&mapper->virtual_pad_keyboard);

  G_OBJECT_CLASS (meta_pad_action_mapper_parent_class)->finalize (object);
}

static void
meta_pad_action_mapper_class_init (MetaPadActionMapperClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = meta_pad_action_mapper_finalize;
}

static GSettings *
lookup_device_settings (ClutterInputDevice *device)
{
  const char *vendor, *product;
  GSettings *settings;
  char *path;

  vendor = clutter_input_device_get_vendor_id (device);
  product = clutter_input_device_get_product_id (device);
  path = g_strdup_printf ("/org/gnome/desktop/peripherals/tablets/%s:%s/",
                          vendor, product);

  settings = g_settings_new_with_path ("org.gnome.desktop.peripherals.tablet",
                                       path);
  g_free (path);

  return settings;
}

static PadMappingInfo *
pad_mapping_info_new (ClutterInputDevice *pad)
{
  PadMappingInfo *info;

  info = g_new0 (PadMappingInfo, 1);
  info->device = pad;
  info->settings = lookup_device_settings (pad);
  info->group_modes =
    g_new0 (guint, clutter_input_device_get_n_mode_groups (pad));

  return info;
}

static void
pad_mapping_info_free (PadMappingInfo *info)
{
  g_object_unref (info->settings);
  g_free (info->group_modes);
  g_free (info);
}

static void
device_added (ClutterSeat         *seat,
              ClutterInputDevice  *device,
              MetaPadActionMapper *mapper)
{
  PadMappingInfo *info;

  if ((clutter_input_device_get_capabilities (device) &
       CLUTTER_INPUT_CAPABILITY_TABLET_PAD) != 0)
    {
      info = pad_mapping_info_new (device);
      g_hash_table_insert (mapper->pads, device, info);
    }
}

static void
device_removed (ClutterSeat         *seat,
                ClutterInputDevice  *device,
                MetaPadActionMapper *mapper)
{
  g_hash_table_remove (mapper->pads, device);
}

static void
meta_pad_action_mapper_init (MetaPadActionMapper *mapper)
{
  mapper->pads = g_hash_table_new_full (NULL, NULL, NULL,
                                        (GDestroyNotify) pad_mapping_info_free);

  mapper->seat = clutter_backend_get_default_seat (clutter_get_default_backend ());
  g_signal_connect (mapper->seat, "device-added",
                    G_CALLBACK (device_added), mapper);
  g_signal_connect (mapper->seat, "device-removed",
                    G_CALLBACK (device_removed), mapper);
}

MetaPadActionMapper *
meta_pad_action_mapper_new (MetaMonitorManager *monitor_manager)
{
  MetaPadActionMapper *action_mapper;

  action_mapper = g_object_new (META_TYPE_PAD_ACTION_MAPPER, NULL);
  g_set_object (&action_mapper->monitor_manager, monitor_manager);

  return action_mapper;
}

static GSettings *
lookup_pad_action_settings (ClutterInputDevice *device,
                            MetaPadActionType   action,
                            guint               number,
                            MetaPadDirection    direction,
                            int                 mode)
{
  const char *vendor, *product, *action_type, *detail_type = NULL;
  GSettings *settings;
  GString *path;
  char action_label;

  vendor = clutter_input_device_get_vendor_id (device);
  product = clutter_input_device_get_product_id (device);

  action_label = 'A' + number;

  switch (action)
    {
    case META_PAD_ACTION_BUTTON:
      action_type = "button";
      break;
    case META_PAD_ACTION_RING:
      g_assert (direction == META_PAD_DIRECTION_CW ||
                direction == META_PAD_DIRECTION_CCW);
      action_type = "ring";
      detail_type = (direction == META_PAD_DIRECTION_CW) ? "cw" : "ccw";
      break;
    case META_PAD_ACTION_STRIP:
      g_assert (direction == META_PAD_DIRECTION_UP ||
                direction == META_PAD_DIRECTION_DOWN);
      action_type = "strip";
      detail_type = (direction == META_PAD_DIRECTION_UP) ? "up" : "down";
      break;
    default:
      return NULL;
    }

  path = g_string_new (NULL);
  g_string_append_printf (path, "/org/gnome/desktop/peripherals/tablets/%s:%s/%s%c",
                          vendor, product, action_type, action_label);

  if (detail_type)
    g_string_append_printf (path, "-%s", detail_type);

  if (mode >= 0)
    g_string_append_printf (path, "-mode-%d", mode);

  g_string_append_c (path, '/');

  settings = g_settings_new_with_path ("org.gnome.desktop.peripherals.tablet.pad-button",
                                       path->str);
  g_string_free (path, TRUE);

  return settings;
}

static GDesktopPadButtonAction
meta_pad_action_mapper_get_button_action (MetaPadActionMapper *mapper,
                                          ClutterInputDevice  *pad,
                                          guint                button)
{
  GDesktopPadButtonAction action;
  GSettings *settings;

  g_return_val_if_fail (META_IS_PAD_ACTION_MAPPER (mapper),
                        G_DESKTOP_PAD_BUTTON_ACTION_NONE);
  g_return_val_if_fail (CLUTTER_IS_INPUT_DEVICE (pad),
                        G_DESKTOP_PAD_BUTTON_ACTION_NONE);

  settings = lookup_pad_action_settings (pad, META_PAD_ACTION_BUTTON,
                                         button, META_PAD_DIRECTION_NONE, -1);
  action = g_settings_get_enum (settings, "action");
  g_object_unref (settings);

  return action;
}

static gboolean
cycle_logical_monitors (MetaPadActionMapper *mapper,
                        gboolean             skip_all_monitors,
                        MetaLogicalMonitor  *current_logical_monitor,
                        MetaLogicalMonitor **next_logical_monitor)
{
  MetaMonitorManager *monitor_manager = mapper->monitor_manager;
  GList *logical_monitors;

  /* We cycle between:
   * - the span of all monitors (current_output = NULL), only for
   *   non-integrated devices.
   * - each monitor individually.
   */

  logical_monitors =
    meta_monitor_manager_get_logical_monitors (monitor_manager);

  if (!current_logical_monitor)
    {
      *next_logical_monitor = logical_monitors->data;
    }
  else
    {
      GList *l;

      l = g_list_find (logical_monitors, current_logical_monitor);
      if (l->next)
        *next_logical_monitor = l->next->data;
      else if (skip_all_monitors)
        *next_logical_monitor = logical_monitors->data;
      else
        *next_logical_monitor = NULL;
    }

  return TRUE;
}

static MetaMonitor *
logical_monitor_find_monitor (MetaLogicalMonitor *logical_monitor,
                              const char         *vendor,
                              const char         *product,
                              const char         *serial)
{
  GList *monitors;
  GList *l;

  monitors = meta_logical_monitor_get_monitors (logical_monitor);
  for (l = monitors; l; l = l->next)
    {
      MetaMonitor *monitor = l->data;

      if (g_strcmp0 (meta_monitor_get_vendor (monitor), vendor) == 0 &&
          g_strcmp0 (meta_monitor_get_product (monitor), product) == 0 &&
          g_strcmp0 (meta_monitor_get_serial (monitor), serial) == 0)
        return monitor;
    }

  return NULL;
}

static void
meta_pad_action_mapper_find_monitor (MetaPadActionMapper  *mapper,
                                     GSettings            *settings,
                                     ClutterInputDevice   *device,
                                     MetaMonitor         **out_monitor,
                                     MetaLogicalMonitor  **out_logical_monitor)
{
  MetaMonitorManager *monitor_manager;
  MetaMonitor *monitor;
  guint n_values;
  GList *logical_monitors;
  GList *l;
  char **edid;

  edid = g_settings_get_strv (settings, "output");
  n_values = g_strv_length (edid);

  if (n_values != 3)
    {
      g_warning ("EDID configuration for device '%s' "
                 "is incorrect, must have 3 values",
                 clutter_input_device_get_device_name (device));
      goto out;
    }

  if (!*edid[0] && !*edid[1] && !*edid[2])
    goto out;

  monitor_manager = mapper->monitor_manager;
  logical_monitors =
    meta_monitor_manager_get_logical_monitors (monitor_manager);
  for (l = logical_monitors; l; l = l->next)
    {
      MetaLogicalMonitor *logical_monitor = l->data;

      monitor = logical_monitor_find_monitor (logical_monitor,
                                              edid[0], edid[1], edid[2]);
      if (monitor)
        {
          if (out_monitor)
            *out_monitor = monitor;
          if (out_logical_monitor)
            *out_logical_monitor = logical_monitor;
          break;
        }
    }

out:
  g_strfreev (edid);
}

static void
meta_pad_action_mapper_cycle_tablet_output (MetaPadActionMapper *mapper,
                                            ClutterInputDevice  *device)
{
  PadMappingInfo *info;
  MetaLogicalMonitor *logical_monitor = NULL;
  const char *edid[4] = { 0 }, *pretty_name = NULL;
  gboolean is_integrated_device = FALSE;
#ifdef HAVE_LIBWACOM
  WacomDevice *wacom_device;
#endif

  g_return_if_fail (META_IS_PAD_ACTION_MAPPER (mapper));
  g_return_if_fail (CLUTTER_IS_INPUT_DEVICE (device));
  g_return_if_fail ((clutter_input_device_get_capabilities (device) &
                     (CLUTTER_INPUT_CAPABILITY_TABLET_TOOL |
                      CLUTTER_INPUT_CAPABILITY_TABLET_PAD)) != 0);

  info = g_hash_table_lookup (mapper->pads, device);
  g_return_if_fail (info != NULL);

#ifdef HAVE_LIBWACOM
  wacom_device = meta_input_device_get_wacom_device (META_INPUT_DEVICE (device));

  if (wacom_device)
    {
      pretty_name = libwacom_get_name (wacom_device);
      is_integrated_device =
        libwacom_get_integration_flags (wacom_device) != WACOM_DEVICE_INTEGRATED_NONE;
    }
#endif

  meta_pad_action_mapper_find_monitor (mapper, info->settings, device,
                                       NULL, &logical_monitor);

  if (!cycle_logical_monitors (mapper,
                               is_integrated_device,
                               logical_monitor,
                               &logical_monitor))
    return;

  if (logical_monitor)
    {
      MetaMonitor *monitor;
      const char *vendor;
      const char *product;
      const char *serial;

      /* Pick an arbitrary monitor in the logical monitor to represent it. */
      monitor = meta_logical_monitor_get_monitors (logical_monitor)->data;
      vendor = meta_monitor_get_vendor (monitor);
      product = meta_monitor_get_product (monitor);
      serial = meta_monitor_get_serial (monitor);
      edid[0] = vendor ? vendor : "";
      edid[1] = product ? product : "";
      edid[2] = serial ? serial : "";
    }
  else
    {
      edid[0] = "";
      edid[1] = "";
      edid[2] = "";
    }

  g_settings_set_strv (info->settings, "output", edid);
  meta_display_show_tablet_mapping_notification (display_from_mapper (mapper),
                                                 device, pretty_name);
}

gboolean
meta_pad_action_mapper_is_button_grabbed (MetaPadActionMapper *mapper,
                                          ClutterInputDevice  *pad,
                                          guint                button)
{
  g_return_val_if_fail (META_IS_PAD_ACTION_MAPPER (mapper), FALSE);
  g_return_val_if_fail (CLUTTER_IS_INPUT_DEVICE (pad), FALSE);
  g_return_val_if_fail ((clutter_input_device_get_capabilities (pad) &
                         CLUTTER_INPUT_CAPABILITY_TABLET_PAD) != 0, FALSE);

  return (meta_pad_action_mapper_get_button_action (mapper, pad, button) !=
          G_DESKTOP_PAD_BUTTON_ACTION_NONE);
}

static void
emulate_modifiers (ClutterVirtualInputDevice *device,
                   ClutterModifierType        mods,
                   ClutterKeyState            state)
{
  guint i;
  struct {
    ClutterModifierType mod;
    guint keyval;
  } mod_map[] = {
    { CLUTTER_SHIFT_MASK, CLUTTER_KEY_Shift_L },
    { CLUTTER_CONTROL_MASK, CLUTTER_KEY_Control_L },
    { CLUTTER_META_MASK, CLUTTER_KEY_Meta_L }
  };

  for (i = 0; i < G_N_ELEMENTS (mod_map); i++)
    {
      if ((mods & mod_map[i].mod) == 0)
        continue;

      clutter_virtual_input_device_notify_keyval (device,
                                                  clutter_get_current_event_time (),
                                                  mod_map[i].keyval, state);
    }
}

static void
meta_pad_action_mapper_emulate_keybinding (MetaPadActionMapper *mapper,
                                           const char          *accel,
                                           gboolean             is_press)
{
  ClutterKeyState state;
  MetaKeyCombo combo = { 0 };

  if (!accel || !*accel)
    return;

  if (!meta_parse_accelerator (accel, &combo))
    {
      g_warning ("\"%s\" is not a valid accelerator", accel);
      return;
    }

  if (!mapper->virtual_pad_keyboard)
    {
      ClutterBackend *backend;
      ClutterSeat *seat;

      backend = clutter_get_default_backend ();
      seat = clutter_backend_get_default_seat (backend);

      mapper->virtual_pad_keyboard =
        clutter_seat_create_virtual_device (seat,
                                            CLUTTER_KEYBOARD_DEVICE);
    }

  state = is_press ? CLUTTER_KEY_STATE_PRESSED : CLUTTER_KEY_STATE_RELEASED;

  if (is_press)
    emulate_modifiers (mapper->virtual_pad_keyboard, combo.modifiers, state);

  clutter_virtual_input_device_notify_keyval (mapper->virtual_pad_keyboard,
                                              clutter_get_current_event_time (),
                                              combo.keysym, state);
  if (!is_press)
    emulate_modifiers (mapper->virtual_pad_keyboard, combo.modifiers, state);
}

static gboolean
meta_pad_action_mapper_handle_button (MetaPadActionMapper         *mapper,
                                      ClutterInputDevice          *pad,
                                      const ClutterPadButtonEvent *event)
{
  GDesktopPadButtonAction action;
  int button, group, mode, n_modes = 0;
  gboolean is_press;
  GSettings *settings;
  char *accel;

  g_return_val_if_fail (META_IS_PAD_ACTION_MAPPER (mapper), FALSE);
  g_return_val_if_fail (event->type == CLUTTER_PAD_BUTTON_PRESS ||
                        event->type == CLUTTER_PAD_BUTTON_RELEASE, FALSE);

  button = event->button;
  mode = event->mode;
  group = clutter_input_device_get_mode_switch_button_group (pad, button);
  is_press = event->type == CLUTTER_PAD_BUTTON_PRESS;

  if (group >= 0)
    n_modes = clutter_input_device_get_group_n_modes (pad, group);

  if (is_press && n_modes > 0)
    {
      const char *pretty_name = NULL;
      PadMappingInfo *info;
#ifdef HAVE_LIBWACOM
      WacomDevice *wacom_device;
#endif

      info = g_hash_table_lookup (mapper->pads, pad);

#ifdef HAVE_LIBWACOM
      wacom_device = meta_input_device_get_wacom_device (META_INPUT_DEVICE (pad));

      if (wacom_device)
        pretty_name = libwacom_get_name (wacom_device);
#endif
      meta_display_notify_pad_group_switch (display_from_mapper (mapper), pad,
                                            pretty_name, group, mode, n_modes);
      info->group_modes[group] = mode;
    }

  action = meta_pad_action_mapper_get_button_action (mapper, pad, button);

  switch (action)
    {
    case G_DESKTOP_PAD_BUTTON_ACTION_SWITCH_MONITOR:
      if (is_press)
        meta_pad_action_mapper_cycle_tablet_output (mapper, pad);
      return TRUE;
    case G_DESKTOP_PAD_BUTTON_ACTION_HELP:
      if (is_press)
        meta_display_request_pad_osd (display_from_mapper (mapper), pad, FALSE);
      return TRUE;
    case G_DESKTOP_PAD_BUTTON_ACTION_KEYBINDING:
      settings = lookup_pad_action_settings (pad, META_PAD_ACTION_BUTTON,
                                             button, META_PAD_DIRECTION_NONE, -1);
      accel = g_settings_get_string (settings, "keybinding");
      meta_pad_action_mapper_emulate_keybinding (mapper, accel, is_press);
      g_object_unref (settings);
      g_free (accel);
      return TRUE;
    case G_DESKTOP_PAD_BUTTON_ACTION_NONE:
    default:
      return FALSE;
    }
}

static gboolean
meta_pad_action_mapper_get_action_direction (MetaPadActionMapper *mapper,
                                             const ClutterEvent  *event,
                                             MetaPadDirection    *direction)
{
  ClutterInputDevice *pad = clutter_event_get_device (event);
  MetaPadActionType pad_action;
  gboolean has_direction = FALSE;
  MetaPadDirection inc_dir, dec_dir;
  guint number;
  double value;

  *direction = META_PAD_DIRECTION_NONE;

  switch (event->type)
    {
    case CLUTTER_PAD_RING:
      pad_action = META_PAD_ACTION_RING;
      number = event->pad_ring.ring_number;
      value = event->pad_ring.angle;
      inc_dir = META_PAD_DIRECTION_CW;
      dec_dir = META_PAD_DIRECTION_CCW;
      break;
    case CLUTTER_PAD_STRIP:
      pad_action = META_PAD_ACTION_STRIP;
      number = event->pad_strip.strip_number;
      value = event->pad_strip.value;
      inc_dir = META_PAD_DIRECTION_DOWN;
      dec_dir = META_PAD_DIRECTION_UP;
      break;
    default:
      return FALSE;
    }

  if (mapper->last_pad_action_info.pad == pad &&
      mapper->last_pad_action_info.action == pad_action &&
      mapper->last_pad_action_info.number == number &&
      value >= 0 && mapper->last_pad_action_info.value >= 0)
    {
      *direction = (value - mapper->last_pad_action_info.value) > 0 ?
        inc_dir : dec_dir;
      has_direction = TRUE;
    }

  mapper->last_pad_action_info.pad = pad;
  mapper->last_pad_action_info.action = pad_action;
  mapper->last_pad_action_info.number = number;
  mapper->last_pad_action_info.value = value;
  return has_direction;
}

static gboolean
meta_pad_action_mapper_handle_action (MetaPadActionMapper *mapper,
                                      ClutterInputDevice  *pad,
                                      const ClutterEvent  *event,
                                      MetaPadActionType    action,
                                      guint                number,
                                      guint                mode)
{
  MetaPadDirection direction = META_PAD_DIRECTION_NONE;
  g_autoptr (GSettings) settings1 = NULL, settings2 = NULL;
  g_autofree char *accel1 = NULL, *accel2 = NULL;
  gboolean handled;

  if (action == META_PAD_ACTION_RING)
    {
      settings1 = lookup_pad_action_settings (pad, action, number,
                                              META_PAD_DIRECTION_CW, mode);
      settings2 = lookup_pad_action_settings (pad, action, number,
                                              META_PAD_DIRECTION_CCW, mode);
    }
  else if (action == META_PAD_ACTION_STRIP)
    {
      settings1 = lookup_pad_action_settings (pad, action, number,
                                              META_PAD_DIRECTION_UP, mode);
      settings2 = lookup_pad_action_settings (pad, action, number,
                                              META_PAD_DIRECTION_DOWN, mode);
    }
  else
    {
      return FALSE;
    }

  accel1 = g_settings_get_string (settings1, "keybinding");
  accel2 = g_settings_get_string (settings2, "keybinding");
  handled = ((accel1 && *accel1) || (accel2 && *accel2));

  if (meta_pad_action_mapper_get_action_direction (mapper, event, &direction))
    {
      const gchar *accel;

      if (direction == META_PAD_DIRECTION_UP ||
          direction == META_PAD_DIRECTION_CW)
        accel = accel1;
      else if (direction == META_PAD_DIRECTION_DOWN ||
               direction == META_PAD_DIRECTION_CCW)
        accel = accel2;

      if (accel && *accel)
        {
          meta_pad_action_mapper_emulate_keybinding (mapper, accel, TRUE);
          meta_pad_action_mapper_emulate_keybinding (mapper, accel, FALSE);
        }
    }

  return handled;
}

gboolean
meta_pad_action_mapper_handle_event (MetaPadActionMapper *mapper,
                                     const ClutterEvent  *event)
{
  ClutterInputDevice *pad;

  pad = clutter_event_get_source_device ((ClutterEvent *) event);

  switch (event->type)
    {
    case CLUTTER_PAD_BUTTON_PRESS:
    case CLUTTER_PAD_BUTTON_RELEASE:
      return meta_pad_action_mapper_handle_button (mapper, pad,
                                                   &event->pad_button);
    case CLUTTER_PAD_RING:
      return meta_pad_action_mapper_handle_action (mapper, pad, event,
                                                   META_PAD_ACTION_RING,
                                                   event->pad_ring.ring_number,
                                                   event->pad_ring.mode);
    case CLUTTER_PAD_STRIP:
      return meta_pad_action_mapper_handle_action (mapper, pad, event,
                                                   META_PAD_ACTION_STRIP,
                                                   event->pad_strip.strip_number,
                                                   event->pad_strip.mode);
    default:
      return FALSE;
    }
}

static void
format_directional_action (GString          *str,
                           MetaPadDirection  direction,
                           const gchar      *action)
{
  switch (direction)
    {
    case META_PAD_DIRECTION_CW:
      g_string_append_printf (str, "⭮ %s", action);
      break;
    case META_PAD_DIRECTION_CCW:
      g_string_append_printf (str, "⭯ %s", action);
      break;
    case META_PAD_DIRECTION_UP:
      g_string_append_printf (str, "↥ %s", action);
      break;
    case META_PAD_DIRECTION_DOWN:
      g_string_append_printf (str, "↧ %s", action);
      break;
    case META_PAD_DIRECTION_NONE:
      g_assert_not_reached ();
    }
}

static char *
compose_directional_action_label (MetaPadDirection  direction1,
                                  GSettings        *value1,
                                  MetaPadDirection  direction2,
                                  GSettings        *value2)
{
  g_autofree char *accel1 = NULL, *accel2 = NULL;
  GString *str;

  accel1 = g_settings_get_string (value1, "keybinding");
  accel2 = g_settings_get_string (value2, "keybinding");

  if ((!accel1 || !*accel1) && ((!accel2 || !*accel2)))
    return NULL;

  str = g_string_new (NULL);

  if (accel1 && *accel1)
    format_directional_action (str, direction1, accel1);

  if (accel2 && *accel2)
    {
      if (str->len != 0)
        g_string_append (str, " / ");

      format_directional_action (str, direction2, accel2);
    }

  return g_string_free (str, FALSE);
}

static char *
meta_pad_action_mapper_get_ring_label (MetaPadActionMapper *mapper,
                                       ClutterInputDevice  *pad,
                                       guint                number,
                                       guint                mode)
{
  GSettings *settings1, *settings2;
  char *label;

  /* We only allow keybinding actions with those */
  settings1 = lookup_pad_action_settings (pad, META_PAD_ACTION_RING, number,
                                          META_PAD_DIRECTION_CW, mode);
  settings2 = lookup_pad_action_settings (pad, META_PAD_ACTION_RING, number,
                                          META_PAD_DIRECTION_CCW, mode);
  label = compose_directional_action_label (META_PAD_DIRECTION_CW, settings1,
                                            META_PAD_DIRECTION_CCW, settings2);
  g_object_unref (settings1);
  g_object_unref (settings2);

  return label;
}

static char *
meta_pad_action_mapper_get_strip_label (MetaPadActionMapper *mapper,
                                        ClutterInputDevice  *pad,
                                        guint                number,
                                        guint                mode)
{
  GSettings *settings1, *settings2;
  char *label;

  /* We only allow keybinding actions with those */
  settings1 = lookup_pad_action_settings (pad, META_PAD_ACTION_STRIP, number,
                                          META_PAD_DIRECTION_UP, mode);
  settings2 = lookup_pad_action_settings (pad, META_PAD_ACTION_STRIP, number,
                                          META_PAD_DIRECTION_DOWN, mode);
  label = compose_directional_action_label (META_PAD_DIRECTION_UP, settings1,
                                            META_PAD_DIRECTION_DOWN, settings2);
  g_object_unref (settings1);
  g_object_unref (settings2);

  return label;
}

static char *
meta_pad_action_mapper_get_button_label (MetaPadActionMapper *mapper,
                                         ClutterInputDevice *pad,
                                         guint               button)
{
  GDesktopPadButtonAction action;
  int group;

  g_return_val_if_fail (META_IS_PAD_ACTION_MAPPER (mapper), NULL);
  g_return_val_if_fail (CLUTTER_IS_INPUT_DEVICE (pad), NULL);
  g_return_val_if_fail ((clutter_input_device_get_capabilities (pad) &
                         CLUTTER_INPUT_CAPABILITY_TABLET_PAD) != 0, NULL);

  group = clutter_input_device_get_mode_switch_button_group (pad, button);

  if (group >= 0)
    {
      /* TRANSLATORS: This string refers to a button that switches between
       * different modes.
       */
      return g_strdup_printf (_("Mode Switch (Group %d)"), group);
    }

  action = meta_pad_action_mapper_get_button_action (mapper, pad, button);

  switch (action)
    {
    case G_DESKTOP_PAD_BUTTON_ACTION_KEYBINDING:
      {
        GSettings *settings;
        char *accel;

        settings = lookup_pad_action_settings (pad, META_PAD_ACTION_BUTTON,
                                               button, META_PAD_DIRECTION_NONE, -1);
        accel = g_settings_get_string (settings, "keybinding");
        g_object_unref (settings);

        return accel;
      }
    case G_DESKTOP_PAD_BUTTON_ACTION_SWITCH_MONITOR:
      /* TRANSLATORS: This string refers to an action, cycles drawing tablets'
       * mapping through the available outputs.
       */
      return g_strdup (_("Switch monitor"));
    case G_DESKTOP_PAD_BUTTON_ACTION_HELP:
      return g_strdup (_("Show on-screen help"));
    case G_DESKTOP_PAD_BUTTON_ACTION_NONE:
    default:
      return NULL;
    }
}

static guint
get_current_pad_mode (MetaPadActionMapper *mapper,
                      ClutterInputDevice  *pad,
                      MetaPadActionType    action_type,
                      guint                number)
{
  PadMappingInfo *info;
  guint group = 0, n_groups;

  info = g_hash_table_lookup (mapper->pads, pad);
  n_groups = clutter_input_device_get_n_mode_groups (pad);

  if (!info->group_modes || n_groups == 0)
    return 0;

  if (action_type == META_PAD_ACTION_RING ||
      action_type == META_PAD_ACTION_STRIP)
    {
      /* Assume features are evenly distributed in groups */
      group = number % n_groups;
    }

  return info->group_modes[group];
}

char *
meta_pad_action_mapper_get_action_label (MetaPadActionMapper *mapper,
                                         ClutterInputDevice  *pad,
                                         MetaPadActionType    action_type,
                                         guint                number)
{
  guint mode;

  switch (action_type)
    {
    case META_PAD_ACTION_BUTTON:
      return meta_pad_action_mapper_get_button_label (mapper, pad, number);
    case META_PAD_ACTION_RING:
      mode = get_current_pad_mode (mapper, pad, action_type, number);
      return meta_pad_action_mapper_get_ring_label (mapper, pad, number, mode);
    case META_PAD_ACTION_STRIP:
      mode = get_current_pad_mode (mapper, pad, action_type, number);
      return meta_pad_action_mapper_get_strip_label (mapper, pad, number, mode);
    }

  return NULL;
}
