/* GTK - The GIMP Toolkit
 * Copyright (C) 1998, 2001 Tim Janik
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Modified by the GTK+ Team and others 1997-2000.  See the AUTHORS
 * file for a list of people on the GTK+ Team.  See the ChangeLog
 * files for a list of changes.  These files are distributed with
 * GTK+ at ftp://ftp.gtk.org/pub/gtk/.
 */

#include "config.h"
#include <string.h>
#include <stdlib.h>

#include "gtkaccelgroup.h"
#include "gtkaccelgroupprivate.h"
#include <glib/gi18n-lib.h>
#include "gtkmarshalers.h"
#include "gtkprivate.h"

/* --- functions --- */
/**
 * gtk_accelerator_valid:
 * @keyval: a GDK keyval
 * @modifiers: modifier mask
 *
 * Determines whether a given keyval and modifier mask constitute
 * a valid keyboard accelerator.
 *
 * For example, the `GDK_KEY_a` keyval plus `GDK_CONTROL_MASK` mask is valid,
 * and matches the “Ctrl+a” accelerator. But, you can't, for instance, use
 * the `GDK_KEY_Control_L` keyval as an accelerator.
 *
 * Returns: true if the accelerator is valid
 */
gboolean
gtk_accelerator_valid (guint           keyval,
                       GdkModifierType modifiers)
{
  static const guint invalid_accelerator_vals[] = {
    GDK_KEY_Shift_L, GDK_KEY_Shift_R, GDK_KEY_Shift_Lock,
    GDK_KEY_Caps_Lock, GDK_KEY_ISO_Lock, GDK_KEY_Control_L,
    GDK_KEY_Control_R, GDK_KEY_Meta_L, GDK_KEY_Meta_R,
    GDK_KEY_Alt_L, GDK_KEY_Alt_R, GDK_KEY_Super_L, GDK_KEY_Super_R,
    GDK_KEY_Hyper_L, GDK_KEY_Hyper_R, GDK_KEY_ISO_Level3_Shift,
    GDK_KEY_ISO_Next_Group, GDK_KEY_ISO_Prev_Group,
    GDK_KEY_ISO_First_Group, GDK_KEY_ISO_Last_Group,
    GDK_KEY_Mode_switch, GDK_KEY_Num_Lock, GDK_KEY_Multi_key,
    GDK_KEY_Scroll_Lock, GDK_KEY_Sys_Req,
    GDK_KEY_Tab, GDK_KEY_ISO_Left_Tab, GDK_KEY_KP_Tab,
    GDK_KEY_First_Virtual_Screen, GDK_KEY_Prev_Virtual_Screen,
    GDK_KEY_Next_Virtual_Screen, GDK_KEY_Last_Virtual_Screen,
    GDK_KEY_Terminate_Server, GDK_KEY_AudibleBell_Enable,
    0
  };
  static const guint invalid_unmodified_vals[] = {
    GDK_KEY_Up, GDK_KEY_Down, GDK_KEY_Left, GDK_KEY_Right,
    GDK_KEY_KP_Up, GDK_KEY_KP_Down, GDK_KEY_KP_Left, GDK_KEY_KP_Right,
    0
  };
  const guint *ac_val;

  modifiers &= GDK_MODIFIER_MASK;

  if (keyval <= 0xFF)
    return keyval >= 0x20;

  ac_val = invalid_accelerator_vals;
  while (*ac_val)
    {
      if (keyval == *ac_val++)
        return FALSE;
    }

  if (!modifiers)
    {
      ac_val = invalid_unmodified_vals;
      while (*ac_val)
        {
          if (keyval == *ac_val++)
            return FALSE;
        }
    }

  return TRUE;
}

static inline gboolean
is_alt (const char *string)
{
  return ((string[0] == '<') &&
          (string[1] == 'a' || string[1] == 'A') &&
          (string[2] == 'l' || string[2] == 'L') &&
          (string[3] == 't' || string[3] == 'T') &&
          (string[4] == '>'));
}

static inline gboolean
is_ctl (const char *string)
{
  return ((string[0] == '<') &&
          (string[1] == 'c' || string[1] == 'C') &&
          (string[2] == 't' || string[2] == 'T') &&
          (string[3] == 'l' || string[3] == 'L') &&
          (string[4] == '>'));
}

static inline gboolean
is_ctrl (const char *string)
{
  return ((string[0] == '<') &&
          (string[1] == 'c' || string[1] == 'C') &&
          (string[2] == 't' || string[2] == 'T') &&
          (string[3] == 'r' || string[3] == 'R') &&
          (string[4] == 'l' || string[4] == 'L') &&
          (string[5] == '>'));
}

static inline gboolean
is_shft (const char *string)
{
  return ((string[0] == '<') &&
          (string[1] == 's' || string[1] == 'S') &&
          (string[2] == 'h' || string[2] == 'H') &&
          (string[3] == 'f' || string[3] == 'F') &&
          (string[4] == 't' || string[4] == 'T') &&
          (string[5] == '>'));
}

static inline gboolean
is_shift (const char *string)
{
  return ((string[0] == '<') &&
          (string[1] == 's' || string[1] == 'S') &&
          (string[2] == 'h' || string[2] == 'H') &&
          (string[3] == 'i' || string[3] == 'I') &&
          (string[4] == 'f' || string[4] == 'F') &&
          (string[5] == 't' || string[5] == 'T') &&
          (string[6] == '>'));
}

static inline gboolean
is_control (const char *string)
{
  return ((string[0] == '<') &&
          (string[1] == 'c' || string[1] == 'C') &&
          (string[2] == 'o' || string[2] == 'O') &&
          (string[3] == 'n' || string[3] == 'N') &&
          (string[4] == 't' || string[4] == 'T') &&
          (string[5] == 'r' || string[5] == 'R') &&
          (string[6] == 'o' || string[6] == 'O') &&
          (string[7] == 'l' || string[7] == 'L') &&
          (string[8] == '>'));
}

static inline gboolean
is_meta (const char *string)
{
  return ((string[0] == '<') &&
          (string[1] == 'm' || string[1] == 'M') &&
          (string[2] == 'e' || string[2] == 'E') &&
          (string[3] == 't' || string[3] == 'T') &&
          (string[4] == 'a' || string[4] == 'A') &&
          (string[5] == '>'));
}

static inline gboolean
is_super (const char *string)
{
  return ((string[0] == '<') &&
          (string[1] == 's' || string[1] == 'S') &&
          (string[2] == 'u' || string[2] == 'U') &&
          (string[3] == 'p' || string[3] == 'P') &&
          (string[4] == 'e' || string[4] == 'E') &&
          (string[5] == 'r' || string[5] == 'R') &&
          (string[6] == '>'));
}

static inline gboolean
is_hyper (const char *string)
{
  return ((string[0] == '<') &&
          (string[1] == 'h' || string[1] == 'H') &&
          (string[2] == 'y' || string[2] == 'Y') &&
          (string[3] == 'p' || string[3] == 'P') &&
          (string[4] == 'e' || string[4] == 'E') &&
          (string[5] == 'r' || string[5] == 'R') &&
          (string[6] == '>'));
}

static inline gboolean
is_primary (const char *string)
{
  return ((string[0] == '<') &&
          (string[1] == 'p' || string[1] == 'P') &&
          (string[2] == 'r' || string[2] == 'R') &&
          (string[3] == 'i' || string[3] == 'I') &&
          (string[4] == 'm' || string[4] == 'M') &&
          (string[5] == 'a' || string[5] == 'A') &&
          (string[6] == 'r' || string[6] == 'R') &&
          (string[7] == 'y' || string[7] == 'Y') &&
          (string[8] == '>'));
}

static inline gboolean
is_keycode (const char *string)
{
  return (string[0] == '0' &&
          string[1] == 'x' &&
          g_ascii_isxdigit (string[2]) &&
          g_ascii_isxdigit (string[3]));
}

/**
 * gtk_accelerator_parse_with_keycode:
 * @accelerator: string representing an accelerator
 * @display: (nullable): the `GdkDisplay` to look up @accelerator_codes in
 * @accelerator_key: (out) (optional): return location for accelerator keyval
 * @accelerator_codes: (out) (array zero-terminated=1) (transfer full) (optional):
 *   return location for accelerator keycodes
 * @accelerator_mods: (out) (optional): return location for accelerator
 *   modifier mask
 *
 * Parses a string representing an accelerator.
 *
 * This is similar to [func@Gtk.accelerator_parse] but handles keycodes as
 * well. This is only useful for system-level components, applications should
 * use [func@Gtk.accelerator_parse] instead.
 *
 * If @accelerator_codes is given and the result stored in it is non-%NULL,
 * the result must be freed with g_free().
 *
 * If a keycode is present in the accelerator and no @accelerator_codes
 * is given, the parse will fail.
 *
 * If the parse fails, @accelerator_key, @accelerator_mods and
 * @accelerator_codes will be set to 0 (zero).
 *
 * Returns: true if parsing succeeded
 */
gboolean
gtk_accelerator_parse_with_keycode (const char      *accelerator,
                                    GdkDisplay      *display,
                                    guint           *accelerator_key,
                                    guint          **accelerator_codes,
                                    GdkModifierType *accelerator_mods)
{
  guint keyval;
  GdkModifierType mods;
  int len;
  gboolean error;

  if (accelerator_key)
    *accelerator_key = 0;
  if (accelerator_mods)
    *accelerator_mods = 0;
  if (accelerator_codes)
    *accelerator_codes = NULL;

  g_return_val_if_fail (accelerator != NULL, FALSE);

  if (!display)
    display = gdk_display_get_default ();

  error = FALSE;
  keyval = 0;
  mods = 0;
  len = strlen (accelerator);
  while (len)
    {
      if (*accelerator == '<')
        {
          if (len >= 9 && is_primary (accelerator))
            {
              accelerator += 9;
              len -= 9;
              mods |= GDK_CONTROL_MASK;
            }
          else if (len >= 9 && is_control (accelerator))
            {
              accelerator += 9;
              len -= 9;
              mods |= GDK_CONTROL_MASK;
            }
          else if (len >= 7 && is_shift (accelerator))
            {
              accelerator += 7;
              len -= 7;
              mods |= GDK_SHIFT_MASK;
            }
          else if (len >= 6 && is_shft (accelerator))
            {
              accelerator += 6;
              len -= 6;
              mods |= GDK_SHIFT_MASK;
            }
          else if (len >= 6 && is_ctrl (accelerator))
            {
              accelerator += 6;
              len -= 6;
              mods |= GDK_CONTROL_MASK;
            }
          else if (len >= 5 && is_ctl (accelerator))
            {
              accelerator += 5;
              len -= 5;
              mods |= GDK_CONTROL_MASK;
            }
          else if (len >= 5 && is_alt (accelerator))
            {
              accelerator += 5;
              len -= 5;
              mods |= GDK_ALT_MASK;
            }
          else if (len >= 6 && is_meta (accelerator))
            {
              accelerator += 6;
              len -= 6;
              mods |= GDK_META_MASK;
            }
          else if (len >= 7 && is_hyper (accelerator))
            {
              accelerator += 7;
              len -= 7;
              mods |= GDK_HYPER_MASK;
            }
          else if (len >= 7 && is_super (accelerator))
            {
              accelerator += 7;
              len -= 7;
              mods |= GDK_SUPER_MASK;
            }
          else
            {
              char last_ch;

              last_ch = *accelerator;
              while (last_ch && last_ch != '>')
                {
                  accelerator += 1;
                  len -= 1;
                  last_ch = *accelerator;
                }

              if (!last_ch)
                {
                  /* The opened '<' has never been closed with '>' */
                  error = TRUE;
                  goto out;
                }
            }
        }
      else
        {
          if (len >= 4 && is_keycode (accelerator))
            {
               char keystring[5];
               char *endptr;
               int tmp_keycode;

               memcpy (keystring, accelerator, 4);
               keystring [4] = '\000';

               tmp_keycode = strtol (keystring, &endptr, 16);

               if (endptr == NULL || *endptr != '\000')
                 {
                   error = TRUE;
                   goto out;
                 }
               else if (accelerator_codes != NULL)
                 {
                   /* 0x00 is an invalid keycode too. */
                   if (tmp_keycode == 0)
                     {
                       error = TRUE;
                       goto out;
                     }
                   else
                     {
                       *accelerator_codes = g_new0 (guint, 2);
                       (*accelerator_codes)[0] = tmp_keycode;
                     }
                 }
               else
                 {
                   /* There was a keycode in the string, but
                    * we cannot store it, so we have an error */
                   error = TRUE;
                   goto out;
                 }
            }
          else
            {
              keyval = gdk_keyval_from_name (accelerator);
              if (keyval == GDK_KEY_VoidSymbol)
                {
                  error = TRUE;
                  goto out;
                }
            }

          if (keyval && accelerator_codes != NULL)
            {
              GdkKeymapKey *keys;
              int n_keys, i, j;

              if (!gdk_display_map_keyval (display, keyval, &keys, &n_keys))
                {
                  /* Not in keymap */
                  error = TRUE;
                  goto out;
                }
              else
                {
                  *accelerator_codes = g_new0 (guint, n_keys + 1);

                  /* Prefer level-0 group-0 keys to modified keys */
                  for (i = 0, j = 0; i < n_keys; ++i)
                    {
                      if (keys[i].level == 0 && keys[i].group == 0)
                        (*accelerator_codes)[j++] = keys[i].keycode;
                    }

                  /* No level-0 group-0 keys? Find in the whole group-0 */
                  if (j == 0)
                    {
                      for (i = 0, j = 0; i < n_keys; ++i)
                        {
                          if (keys[i].group == 0)
                            (*accelerator_codes)[j++] = keys[i].keycode;
                        }
                    }

                  /* Still nothing? Try in other groups */
                  if (j == 0)
                    {
                      for (i = 0, j = 0; i < n_keys; ++i)
                        (*accelerator_codes)[j++] = keys[i].keycode;
                    }

                  if (j == 0)
                    {
                      g_clear_pointer (accelerator_codes, g_free);
                      /* Not in keymap */
                      error = TRUE;
                      goto out;
                    }
                  g_free (keys);
                }
            }

          accelerator += len;
          len -= len;
        }
    }

out:
  if (error)
    keyval = mods = 0;

  if (accelerator_key)
    *accelerator_key = gdk_keyval_to_lower (keyval);
  if (accelerator_mods)
    *accelerator_mods = mods;

  return !error;
}

/**
 * gtk_accelerator_parse:
 * @accelerator: string representing an accelerator
 * @accelerator_key: (out) (optional): return location for accelerator keyval
 * @accelerator_mods: (out) (optional): return location for accelerator
 *   modifier mask
 *
 * Parses a string representing an accelerator.
 *
 * The format looks like “`<Control>a`” or “`<Shift><Alt>F1`”.
 *
 * The parser is fairly liberal and allows lower or upper case, and also
 * abbreviations such as “`<Ctl>`” and “`<Ctrl>`”.
 *
 * Key names are parsed using [func@Gdk.keyval_from_name]. For character keys
 * the name is not the symbol, but the lowercase name, e.g. one would use
 * “`<Ctrl>minus`” instead of “`<Ctrl>-`”.
 *
 * Modifiers are enclosed in angular brackets `<>`, and match the
 * [flags@Gdk.ModifierType] mask:
 *
 * - `<Shift>` for `GDK_SHIFT_MASK`
 * - `<Ctrl>` for `GDK_CONTROL_MASK`
 * - `<Alt>` for `GDK_ALT_MASK`
 * - `<Meta>` for `GDK_META_MASK`
 * - `<Super>` for `GDK_SUPER_MASK`
 * - `<Hyper>` for `GDK_HYPER_MASK`
 *
 * If the parse operation fails, @accelerator_key and @accelerator_mods will
 * be set to 0 (zero).
 *
 * Returns: whether parsing succeeded
 */
gboolean
gtk_accelerator_parse (const char      *accelerator,
                       guint           *accelerator_key,
                       GdkModifierType *accelerator_mods)
{
  return gtk_accelerator_parse_with_keycode (accelerator, NULL, accelerator_key, NULL, accelerator_mods);
}

/**
 * gtk_accelerator_name_with_keycode:
 * @display: (nullable): a `GdkDisplay`
 * @accelerator_key: accelerator keyval
 * @keycode: accelerator keycode
 * @accelerator_mods: accelerator modifier mask
 *
 * Converts an accelerator keyval and modifier mask
 * into a string that can be parsed by [func@Gtk.accelerator_parse_with_keycode].
 *
 * This is similar to [func@Gtk.accelerator_name] but handling keycodes.
 * This is only useful for system-level components, applications
 * should use [func@Gtk.accelerator_name] instead.
 *
 * Returns: a newly allocated accelerator name.
 */
char *
gtk_accelerator_name_with_keycode (GdkDisplay      *display,
                                   guint            accelerator_key,
                                   guint            keycode,
                                   GdkModifierType  accelerator_mods)
{
  char *gtk_name;

  gtk_name = gtk_accelerator_name (accelerator_key, accelerator_mods);

  if (!accelerator_key)
    {
      char *name;
      name = g_strdup_printf ("%s0x%02x", gtk_name, keycode);
      g_free (gtk_name);
      return name;
    }

  return gtk_name;
}

/**
 * gtk_accelerator_name:
 * @accelerator_key: accelerator keyval
 * @accelerator_mods: accelerator modifier mask
 *
 * Converts an accelerator keyval and modifier mask into a string
 * that can be parsed by [func@Gtk.accelerator_parse].
 *
 * For example, if you pass in `GDK_KEY_q` and `GDK_CONTROL_MASK`,
 * this function returns `<Control>q`.
 *
 * If you need to display accelerators in the user interface,
 * see [func@Gtk.accelerator_get_label].
 *
 * Returns: (transfer full): a newly-allocated accelerator name
 */
char *
gtk_accelerator_name (guint           accelerator_key,
                      GdkModifierType accelerator_mods)
{
  static const struct {
    guint mask;
    const char *text;
  } mask_text[] = {
    { GDK_SHIFT_MASK,   "<Shift>"   },
    { GDK_CONTROL_MASK, "<Control>" },
    { GDK_ALT_MASK,     "<Alt>"     },
    { GDK_META_MASK,    "<Meta>"    },
    { GDK_SUPER_MASK,   "<Super>"   },
    { GDK_HYPER_MASK,   "<Hyper>"   }
  };
  const char *keyval_name;
  GString *result;
  gsize i;

  accelerator_mods &= GDK_MODIFIER_MASK;

  keyval_name = gdk_keyval_name (gdk_keyval_to_lower (accelerator_key));
  if (keyval_name == NULL)
    keyval_name = "";

  if (accelerator_mods == 0)
    return g_strdup (keyval_name);

  result = g_string_new (NULL);

  for (i = 0; i < G_N_ELEMENTS (mask_text); i++)
    {
      if (accelerator_mods & mask_text[i].mask)
        g_string_append (result, mask_text[i].text);
    }

  if (keyval_name)
    g_string_append (result, keyval_name);

  return g_string_free_and_steal (result);
}

/**
 * gtk_accelerator_get_label_with_keycode:
 * @display: (nullable): a `GdkDisplay`
 * @accelerator_key: accelerator keyval
 * @keycode: accelerator keycode
 * @accelerator_mods: accelerator modifier mask
 *
 * Converts an accelerator keyval and modifier mask
 * into a string that can be displayed to the user.
 *
 * The string may be translated.
 *
 * This function is similar to [func@Gtk.accelerator_get_label],
 * but handling keycodes. This is only useful for system-level
 * components, applications should use [func@Gtk.accelerator_get_label]
 * instead.
 *
 * Returns: (transfer full): a newly-allocated string representing the accelerator
 */
char *
gtk_accelerator_get_label_with_keycode (GdkDisplay      *display,
                                        guint            accelerator_key,
                                        guint            keycode,
                                        GdkModifierType  accelerator_mods)
{
  char *gtk_label;

  gtk_label = gtk_accelerator_get_label (accelerator_key, accelerator_mods);

  if (!accelerator_key)
    {
      char *label;
      label = g_strdup_printf ("%s0x%02x", gtk_label, keycode);
      g_free (gtk_label);
      return label;
    }

  return gtk_label;
}

/* Underscores in key names are better displayed as spaces
 * E.g., Page_Up should be “Page Up”.
 *
 * Some keynames also have prefixes that are not suitable
 * for display, e.g XF86AudioMute, so strip those out, too.
 *
 * This function is only called on untranslated keynames,
 * so no need to be UTF-8 safe.
 */
static void
append_without_underscores (GString    *s,
                            const char *str)
{
  const char *p;

  if (g_str_has_prefix (str, "XF86"))
    p = str + 4;
  else if (g_str_has_prefix (str, "ISO_"))
    p = str + 4;
  else
    p = str;

  for ( ; *p; p++)
    {
      if (*p == '_')
        g_string_append_c (s, ' ');
      else
        g_string_append_c (s, *p);
    }
}

/* On Mac, if the key has symbolic representation (e.g. arrow keys),
 * append it to gstring and return TRUE; otherwise return FALSE.
 * See http://docs.info.apple.com/article.html?path=Mac/10.5/en/cdb_symbs.html
 * for the list of special keys. */
static gboolean
append_keyval_symbol (guint    accelerator_key,
                      GString *gstring)
{
#ifdef GDK_WINDOWING_MACOS
  switch (accelerator_key)
  {
  case GDK_KEY_Return:
    /* U+21A9 LEFTWARDS ARROW WITH HOOK */
    g_string_append (gstring, "\xe2\x86\xa9");
    return TRUE;

  case GDK_KEY_ISO_Enter:
    /* U+2324 UP ARROWHEAD BETWEEN TWO HORIZONTAL BARS */
    g_string_append (gstring, "\xe2\x8c\xa4");
    return TRUE;

  case GDK_KEY_Left:
    /* U+2190 LEFTWARDS ARROW */
    g_string_append (gstring, "\xe2\x86\x90");
    return TRUE;

  case GDK_KEY_Up:
    /* U+2191 UPWARDS ARROW */
    g_string_append (gstring, "\xe2\x86\x91");
    return TRUE;

  case GDK_KEY_Right:
    /* U+2192 RIGHTWARDS ARROW */
    g_string_append (gstring, "\xe2\x86\x92");
    return TRUE;

  case GDK_KEY_Down:
    /* U+2193 DOWNWARDS ARROW */
    g_string_append (gstring, "\xe2\x86\x93");
    return TRUE;

  case GDK_KEY_Page_Up:
    /* U+21DE UPWARDS ARROW WITH DOUBLE STROKE */
    g_string_append (gstring, "\xe2\x87\x9e");
    return TRUE;

  case GDK_KEY_Page_Down:
    /* U+21DF DOWNWARDS ARROW WITH DOUBLE STROKE */
    g_string_append (gstring, "\xe2\x87\x9f");
    return TRUE;

  case GDK_KEY_Home:
    /* U+2196 NORTH WEST ARROW */
    g_string_append (gstring, "\xe2\x86\x96");
    return TRUE;

  case GDK_KEY_End:
    /* U+2198 SOUTH EAST ARROW */
    g_string_append (gstring, "\xe2\x86\x98");
    return TRUE;

  case GDK_KEY_Escape:
    /* U+238B BROKEN CIRCLE WITH NORTHWEST ARROW */
    g_string_append (gstring, "\xe2\x8e\x8b");
    return TRUE;

  case GDK_KEY_BackSpace:
    /* U+232B ERASE TO THE LEFT */
    g_string_append (gstring, "\xe2\x8c\xab");
    return TRUE;

  case GDK_KEY_Delete:
    /* U+2326 ERASE TO THE RIGHT */
    g_string_append (gstring, "\xe2\x8c\xa6");
    return TRUE;

  default:
    return FALSE;
  }
#else /* !GDK_WINDOWING_MACOS */
  return FALSE;
#endif
}

static void
append_separator (GString *string)
{
#ifndef GDK_WINDOWING_MACOS
  g_string_append (string, "+");
#else
  /* no separator on quartz */
#endif
}

/**
 * gtk_accelerator_get_label:
 * @accelerator_key: accelerator keyval
 * @accelerator_mods: accelerator modifier mask
 *
 * Converts an accelerator keyval and modifier mask into a string
 * which can be used to represent the accelerator to the user.
 *
 * Returns: (transfer full): a newly-allocated string representing the accelerator
 */
char *
gtk_accelerator_get_label (guint           accelerator_key,
                           GdkModifierType accelerator_mods)
{
  GString *gstring;

  gstring = g_string_new (NULL);

  gtk_accelerator_print_label (gstring, accelerator_key, accelerator_mods);

  return g_string_free (gstring, FALSE);
}

void
gtk_accelerator_print_label (GString        *gstring,
                             guint           accelerator_key,
                             GdkModifierType accelerator_mods)
{
  gboolean seen_mod = FALSE;
  gunichar ch;

  if (accelerator_mods & GDK_SHIFT_MASK)
    {
#ifndef GDK_WINDOWING_MACOS
      /* This is the text that should appear next to menu accelerators
       * that use the shift key. If the text on this key isn't typically
       * translated on keyboards used for your language, don't translate
       * this.
       */
      g_string_append (gstring, C_("keyboard label", "Shift"));
#else
      /* U+21E7 UPWARDS WHITE ARROW */
      g_string_append (gstring, "⇧");
#endif
      seen_mod = TRUE;
    }

  if (accelerator_mods & GDK_CONTROL_MASK)
    {
      if (seen_mod)
        append_separator (gstring);

#ifndef GDK_WINDOWING_MACOS
      /* This is the text that should appear next to menu accelerators
       * that use the control key. If the text on this key isn't typically
       * translated on keyboards used for your language, don't translate
       * this.
       */
      g_string_append (gstring, C_("keyboard label", "Ctrl"));
#else
      /* U+2303 UP ARROWHEAD */
      g_string_append (gstring, "⌃");
#endif
      seen_mod = TRUE;
    }

  if (accelerator_mods & GDK_ALT_MASK)
    {
      if (seen_mod)
        append_separator (gstring);

#ifndef GDK_WINDOWING_MACOS
      /* This is the text that should appear next to menu accelerators
       * that use the alt key. If the text on this key isn't typically
       * translated on keyboards used for your language, don't translate
       * this.
       */
      g_string_append (gstring, C_("keyboard label", "Alt"));
#else
      /* U+2325 OPTION KEY */
      g_string_append (gstring, "⌥");
#endif
      seen_mod = TRUE;
    }

  if (accelerator_mods & GDK_SUPER_MASK)
    {
      if (seen_mod)
        append_separator (gstring);

      /* This is the text that should appear next to menu accelerators
       * that use the super key. If the text on this key isn't typically
       * translated on keyboards used for your language, don't translate
       * this.
       */
      g_string_append (gstring, C_("keyboard label", "Super"));
      seen_mod = TRUE;
    }

  if (accelerator_mods & GDK_HYPER_MASK)
    {
      if (seen_mod)
        append_separator (gstring);

      /* This is the text that should appear next to menu accelerators
       * that use the hyper key. If the text on this key isn't typically
       * translated on keyboards used for your language, don't translate
       * this.
       */
      g_string_append (gstring, C_("keyboard label", "Hyper"));
      seen_mod = TRUE;
    }

  if (accelerator_mods & GDK_META_MASK)
    {
      if (seen_mod)
        append_separator (gstring);

#ifndef GDK_WINDOWING_MACOS
      /* This is the text that should appear next to menu accelerators
       * that use the meta key. If the text on this key isn't typically
       * translated on keyboards used for your language, don't translate
       * this.
       */
      g_string_append (gstring, C_("keyboard label", "Meta"));
#else
      g_string_append (gstring, "⌘");
#endif
      seen_mod = TRUE;
    }

  ch = gdk_keyval_to_unicode (accelerator_key);
  if (ch && (ch == ' ' || g_unichar_isgraph (ch)))
    {
      if (seen_mod)
        append_separator (gstring);

      if (accelerator_key >= GDK_KEY_KP_Space &&
          accelerator_key <= GDK_KEY_KP_9)
        {
          /* Translators: "KP" means "numeric key pad". This string will
           * be used in accelerators such as "Ctrl+Shift+KP 1" in menus,
           * and therefore the translation needs to be very short.
           */
          g_string_append (gstring, C_("keyboard label", "KP"));
          g_string_append (gstring, " ");
        }

      switch (ch)
        {
        case ' ':
          g_string_append (gstring, C_("keyboard label", "Space"));
          break;
        case '\\':
          g_string_append (gstring, C_("keyboard label", "Backslash"));
          break;
        default:
          g_string_append_unichar (gstring, g_unichar_toupper (ch));
          break;
        }
    }
  else if (!append_keyval_symbol (accelerator_key, gstring))
    {
      const char *tmp;

      tmp = gdk_keyval_name (gdk_keyval_to_lower (accelerator_key));
      if (tmp != NULL)
        {
          if (seen_mod)
            append_separator (gstring);

          if (tmp[0] != 0 && tmp[1] == 0)
            g_string_append_c (gstring, g_ascii_toupper (tmp[0]));
          else
            {
              const char *str;
              str = g_dpgettext2 (GETTEXT_PACKAGE, "keyboard label", tmp);
              if (str == tmp)
                append_without_underscores (gstring, tmp);
              else
                g_string_append (gstring, str);
            }
        }
    }
}

/* See https://www.w3.org/TR/uievents-key/#named-key-attribute-values for the
 * list of WAI-ARIA named key attribute values. */
static const gchar *
keyval_get_accessible_name (guint accelerator_key)
{
  switch (accelerator_key)
  {
    case GDK_KEY_Alt_L:
    case GDK_KEY_Alt_R:
      return "Alt";

    case GDK_KEY_Mode_switch:
    case GDK_KEY_ISO_Level3_Shift:
    case GDK_KEY_ISO_Level3_Latch:
    case GDK_KEY_ISO_Level3_Lock:
    case GDK_KEY_ISO_Level5_Shift:
    case GDK_KEY_ISO_Level5_Latch:
    case GDK_KEY_ISO_Level5_Lock:
      return "AltGraph";

    case GDK_KEY_Caps_Lock:
      return "CapsLock";

    case GDK_KEY_Control_L:
    case GDK_KEY_Control_R:
      return "Control";

    /* Firefox treats these as "Alt" */
    case GDK_KEY_Meta_L:
    case GDK_KEY_Meta_R:
      return "Meta";

    case GDK_KEY_Num_Lock:
      return "NumLock";

    case GDK_KEY_Scroll_Lock:
      return "ScrollLock";

    case GDK_KEY_Shift_L:
    case GDK_KEY_Shift_R:
    case GDK_KEY_Shift_Lock:
      return "Shift";

    /* Firefox treats these as "Meta" */
    case GDK_KEY_Super_L:
    case GDK_KEY_Super_R:
      return "Super";

    /* Firefox treats these as "Meta" */
    case GDK_KEY_Hyper_L:
    case GDK_KEY_Hyper_R:
      return "Hyper";

    case GDK_KEY_Return:
    case GDK_KEY_KP_Enter:
    case GDK_KEY_ISO_Enter:
    case GDK_KEY_3270_Enter:
      return "Enter";

    case GDK_KEY_Tab:
    case GDK_KEY_ISO_Left_Tab:
    case GDK_KEY_KP_Tab:
      return "Tab";

    case GDK_KEY_Down:
    case GDK_KEY_KP_Down:
      return "ArrowDown";

    case GDK_KEY_Left:
    case GDK_KEY_KP_Left:
      return "ArrowLeft";

    case GDK_KEY_Right:
    case GDK_KEY_KP_Right:
      return "ArrowRight";

    case GDK_KEY_Up:
    case GDK_KEY_KP_Up:
      return "ArrowUp";

    case GDK_KEY_End:
    case GDK_KEY_KP_End:
      return "End";

    case GDK_KEY_Home:
    case GDK_KEY_KP_Home:
      return "Home";

    case GDK_KEY_Page_Down:
    case GDK_KEY_KP_Page_Down:
      return "PageDown";

    case GDK_KEY_Page_Up:
    case GDK_KEY_KP_Page_Up:
      return "PageUp";

    case GDK_KEY_BackSpace:
      return "Backspace";

    case GDK_KEY_Clear:
      return "Clear";

    case GDK_KEY_Copy:
      return "Copy";

    case GDK_KEY_3270_CursorSelect:
      return "CrSel";

    case GDK_KEY_Cut:
      return "Cut";

    case GDK_KEY_Delete:
    case GDK_KEY_KP_Delete:
      return "Delete";

    case GDK_KEY_3270_EraseEOF:
      return "EraseEof";

    case GDK_KEY_3270_ExSelect:
      return "ExSel";

    case GDK_KEY_Insert:
    case GDK_KEY_KP_Insert:
      return "Insert";

    case GDK_KEY_Paste:
      return "Paste";

    case GDK_KEY_Redo:
      return "Redo";

    case GDK_KEY_Undo:
      return "Undo";

    case GDK_KEY_3270_Attn:
      return "Attn";

    case GDK_KEY_Cancel:
      return "Cancel";

    case GDK_KEY_Menu:
      return "ContextMenu";

    case GDK_KEY_Escape:
      return "Escape";

    case GDK_KEY_Execute:
      return "Execute";

    case GDK_KEY_Find:
      return "Find";

    case GDK_KEY_Help:
      return "Help";

    case GDK_KEY_Pause:
    case GDK_KEY_Break:
      return "Pause";

    case GDK_KEY_3270_Play:
      return "Play";

    case GDK_KEY_Select:
      return "Select";

    case GDK_KEY_ZoomIn:
      return "ZoomIn";

    case GDK_KEY_ZoomOut:
      return "ZoomOut";

    case GDK_KEY_MonBrightnessDown:
      return "BrightnessDown";

    case GDK_KEY_MonBrightnessUp:
      return "BrightnessUp";

    case GDK_KEY_Eject:
      return "Eject";

    case GDK_KEY_LogOff:
      return "LogOff";

    case GDK_KEY_PowerDown:
    case GDK_KEY_PowerOff:
      return "PowerOff";

    case GDK_KEY_3270_PrintScreen:
    case GDK_KEY_Print:
    case GDK_KEY_Sys_Req:
      return "PrintScreen";

    case GDK_KEY_Hibernate:
      return "Hibernate";

    case GDK_KEY_Standby:
    case GDK_KEY_Suspend:
    case GDK_KEY_Sleep:
      return "Standby";

    case GDK_KEY_WakeUp:
      return "WakeUp";

    case GDK_KEY_MultipleCandidate:
      return "AllCandidates";

    case GDK_KEY_Eisu_Shift:
    case GDK_KEY_Eisu_toggle:
      return "Alphanumeric";

    case GDK_KEY_Codeinput:
      return "CodeInput";

    case GDK_KEY_Multi_key:
      return "Compose";

    case GDK_KEY_Henkan:
      return "Convert";

    case GDK_KEY_dead_grave:
    case GDK_KEY_dead_acute:
    case GDK_KEY_dead_circumflex:
    case GDK_KEY_dead_tilde:
    case GDK_KEY_dead_macron:
    case GDK_KEY_dead_breve:
    case GDK_KEY_dead_abovedot:
    case GDK_KEY_dead_diaeresis:
    case GDK_KEY_dead_abovering:
    case GDK_KEY_dead_doubleacute:
    case GDK_KEY_dead_caron:
    case GDK_KEY_dead_cedilla:
    case GDK_KEY_dead_ogonek:
    case GDK_KEY_dead_iota:
    case GDK_KEY_dead_voiced_sound:
    case GDK_KEY_dead_semivoiced_sound:
    case GDK_KEY_dead_belowdot:
    case GDK_KEY_dead_hook:
    case GDK_KEY_dead_horn:
    case GDK_KEY_dead_stroke:
    case GDK_KEY_dead_abovecomma:
    case GDK_KEY_dead_abovereversedcomma:
    case GDK_KEY_dead_doublegrave:
    case GDK_KEY_dead_belowring:
    case GDK_KEY_dead_belowmacron:
    case GDK_KEY_dead_belowcircumflex:
    case GDK_KEY_dead_belowtilde:
    case GDK_KEY_dead_belowbreve:
    case GDK_KEY_dead_belowdiaeresis:
    case GDK_KEY_dead_invertedbreve:
    case GDK_KEY_dead_belowcomma:
    case GDK_KEY_dead_currency:
    case GDK_KEY_dead_a:
    case GDK_KEY_dead_A:
    case GDK_KEY_dead_e:
    case GDK_KEY_dead_E:
    case GDK_KEY_dead_i:
    case GDK_KEY_dead_I:
    case GDK_KEY_dead_o:
    case GDK_KEY_dead_O:
    case GDK_KEY_dead_u:
    case GDK_KEY_dead_U:
    case GDK_KEY_dead_small_schwa:
    case GDK_KEY_dead_capital_schwa:
    case GDK_KEY_dead_greek:
      return "Dead";

    case GDK_KEY_ISO_First_Group:
      return "GroupFirst";

    case GDK_KEY_ISO_Last_Group:
      return "GroupLast";

    case GDK_KEY_ISO_Next_Group:
      return "GroupNext";

    case GDK_KEY_ISO_Prev_Group:
      return "GroupPrevious";

    case GDK_KEY_Muhenkan:
      return "NonConvert";

    case GDK_KEY_PreviousCandidate:
      return "PreviousCandidate";

    case GDK_KEY_SingleCandidate:
      return "SingleCandidate";

    case GDK_KEY_Hankaku:
      return "Hankaku";

    case GDK_KEY_Hiragana:
      return "Hiragana";

    case GDK_KEY_Hiragana_Katakana:
      return "HiraganaKatakana";

    case GDK_KEY_Kana_Lock:
    case GDK_KEY_Kana_Shift:
      return "KanaMode";

    case GDK_KEY_Kanji:
      return "KanjiMode";

    case GDK_KEY_Katakana:
      return "Katakana";

    case GDK_KEY_Romaji:
      return "Romaji";

    case GDK_KEY_Zenkaku:
      return "Zenkaku";

    case GDK_KEY_Zenkaku_Hankaku:
      return "ZenkakuHankaku";

    case GDK_KEY_F1:
    case GDK_KEY_KP_F1:
      return "F1";

    case GDK_KEY_F2:
    case GDK_KEY_KP_F2:
      return "F2";

    case GDK_KEY_F3:
    case GDK_KEY_KP_F3:
      return "F3";

    case GDK_KEY_F4:
    case GDK_KEY_KP_F4:
      return "F4";

    case GDK_KEY_F5:
      return "F5";

    case GDK_KEY_F6:
      return "F6";

    case GDK_KEY_F7:
      return "F7";

    case GDK_KEY_F8:
      return "F8";

    case GDK_KEY_F9:
      return "F9";

    case GDK_KEY_F10:
      return "F10";

    case GDK_KEY_F11:
      return "F11";

    case GDK_KEY_F12:
      return "F12";

    case GDK_KEY_F13:
      return "F13";

    case GDK_KEY_F14:
      return "F14";

    case GDK_KEY_F15:
      return "F15";

    case GDK_KEY_F16:
      return "F16";

    case GDK_KEY_F17:
      return "F17";

    case GDK_KEY_F18:
      return "F18";

    case GDK_KEY_F19:
      return "F19";

    case GDK_KEY_F20:
      return "F20";

    case GDK_KEY_F21:
      return "F21";

    case GDK_KEY_F22:
      return "F22";

    case GDK_KEY_F23:
      return "F23";

    case GDK_KEY_F24:
      return "F24";

    case GDK_KEY_F25:
      return "F25";

    case GDK_KEY_F26:
      return "F26";

    case GDK_KEY_F27:
      return "F27";

    case GDK_KEY_F28:
      return "F28";

    case GDK_KEY_F29:
      return "F29";

    case GDK_KEY_F30:
      return "F30";

    case GDK_KEY_F31:
      return "F31";

    case GDK_KEY_F32:
      return "F32";

    case GDK_KEY_F33:
      return "F33";

    case GDK_KEY_F34:
      return "F34";

    case GDK_KEY_F35:
      return "F35";

    case GDK_KEY_Close:
      return "Close";

    case GDK_KEY_MailForward:
      return "MailForward";

    case GDK_KEY_Reply:
      return "MailReply";

    case GDK_KEY_Send:
      return "MailSend";

    case GDK_KEY_AudioForward:
      return "MediaFastForward";

    case GDK_KEY_AudioPause:
      return "MediaPause";

    case GDK_KEY_AudioPlay:
      return "MediaPlay";

    case GDK_KEY_AudioRecord:
      return "MediaRecord";

    case GDK_KEY_AudioRewind:
      return "MediaRewind";

    case GDK_KEY_AudioStop:
      return "MediaStop";

    case GDK_KEY_AudioNext:
      return "MediaTrackNext";

    case GDK_KEY_AudioPrev:
      return "MediaTrackPrevious";

    case GDK_KEY_New:
      return "New";

    case GDK_KEY_Open:
      return "Open";

    case GDK_KEY_Save:
      return "Save";

    case GDK_KEY_Spell:
      return "SpellCheck";

    case GDK_KEY_AudioLowerVolume:
      return "AudioVolumeDown";

    case GDK_KEY_AudioRaiseVolume:
      return "AudioVolumeUp";

    case GDK_KEY_AudioMute:
      return "AudioVolumeMute";

    case GDK_KEY_Calculator:
      return "LaunchCalculator";

    case GDK_KEY_Calendar:
      return "LaunchCalendar";

    case GDK_KEY_Mail:
      return "LaunchMail";

    case GDK_KEY_CD:
    case GDK_KEY_Video:
    case GDK_KEY_AudioMedia:
      return "LaunchMediaPlayer";

    case GDK_KEY_Music:
      return "LaunchMusicPlayer";

    case GDK_KEY_MyComputer:
    case GDK_KEY_Explorer:
      return "LaunchMyComputer";

    case GDK_KEY_ScreenSaver:
      return "LaunchScreenSaver";

    case GDK_KEY_Excel:
      return "LaunchSpreadsheet";

    case GDK_KEY_WWW:
      return "LaunchWebBrowser";

    case GDK_KEY_WebCam:
      return "LaunchWebCam";

    case GDK_KEY_Word:
      return "LaunchWordProcessor";

    case GDK_KEY_Launch0:
      return "LaunchApplication1";

    case GDK_KEY_Launch1:
      return "LaunchApplication2";

    case GDK_KEY_Launch2:
      return "LaunchApplication3";

    case GDK_KEY_Launch3:
      return "LaunchApplication4";

    case GDK_KEY_Launch4:
      return "LaunchApplication5";

    case GDK_KEY_Launch5:
      return "LaunchApplication6";

    case GDK_KEY_Launch6:
      return "LaunchApplication7";

    case GDK_KEY_Launch7:
      return "LaunchApplication8";

    case GDK_KEY_Launch8:
      return "LaunchApplication9";

    case GDK_KEY_Launch9:
      return "LaunchApplication10";

    case GDK_KEY_LaunchA:
      return "LaunchApplication11";

    case GDK_KEY_LaunchB:
      return "LaunchApplication12";

    case GDK_KEY_LaunchC:
      return "LaunchApplication13";

    case GDK_KEY_LaunchD:
      return "LaunchApplication14";

    case GDK_KEY_LaunchE:
      return "LaunchApplication15";

    case GDK_KEY_LaunchF:
      return "LaunchApplication16";

    case GDK_KEY_Back:
      return "BrowserBack";

    case GDK_KEY_Forward:
      return "BrowserForward";

    case GDK_KEY_HomePage:
      return "BrowserHome";

    case GDK_KEY_Refresh:
    case GDK_KEY_Reload:
      return "BrowserRefresh";

    case GDK_KEY_Search:
      return "BrowserSearch";

    case GDK_KEY_Stop:
      return "BrowserStop";

    case GDK_KEY_Red:
      return "ColorF0Red";

    case GDK_KEY_Green:
      return "ColorF1Green";

    case GDK_KEY_Yellow:
      return "ColorF2Yellow";

    case GDK_KEY_Blue:
      return "ColorF3Blue";

    case GDK_KEY_BrightnessAdjust:
      return "Dimmer";

    case GDK_KEY_AudioRandomPlay:
      return "RandomToggle";

    case GDK_KEY_Subtitle:
      return "Subtitle";

    case GDK_KEY_Next_VMode:
      return "VideoModeNext";

    default:
      return NULL;
  }
}

static gboolean
append_keyval_accessible_graph (guint    accelerator_key,
                                GString *gstring)
{
  gunichar ch;

  ch = gdk_keyval_to_unicode (accelerator_key);
  if (ch == ' ')
    {
      /* The [WAI-ARIA](https://www.w3.org/TR/wai-aria/#aria-keyshortcuts)
       * reference states that "Space" is used for the spacebar.
       */
      g_string_append (gstring, "Space");
      return TRUE;
    }
  else if (ch && g_unichar_isgraph (ch))
    {
      g_string_append_unichar (gstring, g_unichar_toupper (ch));
      return TRUE;
    }
  else
    {
      return FALSE;
    }
}

/* See https://www.w3.org/TR/uievents-key/#named-key-attribute-values for the
 * list of WAI-ARIA named key attribute values. */
static gboolean
append_keyval_accessible_name (guint    accelerator_key,
                               GString *gstring)
{
  const gchar *name;

  name = keyval_get_accessible_name (accelerator_key);
  if (name != NULL)
    {
      g_string_append (gstring, name);
      return TRUE;
    }

    return FALSE;
}
/**
 * gtk_accelerator_get_accessible_label:
 * @accelerator_key: accelerator keyval
 * @accelerator_mods: accelerator modifier mask
 *
 * Generates an accessible description of an accelerator.
 *
 * This function is similar to [func@Gtk.accelerator_get_label] but it is meant
 * for accessibility layers labels rather than user-facing labels. The output
 * of this function is fit for [enum@Gtk.AccessibleProperty.KEY_SHORTCUTS].
 *
 * For more information, see the [WAI-ARIA](https://www.w3.org/TR/wai-aria/#aria-keyshortcuts)
 * reference.
 *
 * Returns: (transfer full): a newly-allocated string representing the accelerator
 *
 * Since: 4.22
 */
char *
gtk_accelerator_get_accessible_label (guint           accelerator_key,
                                      GdkModifierType accelerator_mods)
{
  GString *gstring;
  gboolean seen_mod = FALSE;

  gstring = g_string_new (NULL);

  if (accelerator_mods & GDK_SHIFT_MASK)
    {
      g_string_append (gstring, "Shift");
      seen_mod = TRUE;
    }

  if (accelerator_mods & GDK_CONTROL_MASK)
    {
      if (seen_mod)
        g_string_append (gstring, "+");

      g_string_append (gstring, "Control");
      seen_mod = TRUE;
    }

  if (accelerator_mods & GDK_ALT_MASK)
    {
      if (seen_mod)
        g_string_append (gstring, "+");

      g_string_append (gstring, "Alt");
      seen_mod = TRUE;
    }

  if (accelerator_mods & GDK_SUPER_MASK)
    {
      if (seen_mod)
        g_string_append (gstring, "+");

      /* According to the [WAI-ARIA](https://www.w3.org/TR/uievents-key/#:~:text="Super")
       * reference, "Super" is considered a legacy key and its support isn't
       * required. */
      g_string_append (gstring, "Super");
      seen_mod = TRUE;
    }

  if (accelerator_mods & GDK_HYPER_MASK)
    {
      if (seen_mod)
        g_string_append (gstring, "+");

      /* According to the [WAI-ARIA](https://www.w3.org/TR/uievents-key/#:~:text="Hyper")
       * reference, "Hyper" is considered a legacy key and its support isn't
       * required. */
      g_string_append (gstring, "Hyper");
      seen_mod = TRUE;
    }

  if (accelerator_mods & GDK_META_MASK)
    {
      if (seen_mod)
        g_string_append (gstring, "+");

      g_string_append (gstring, "Meta");
      seen_mod = TRUE;
    }

  if (seen_mod)
    g_string_append (gstring, "+");

  if (!append_keyval_accessible_name (accelerator_key, gstring) &&
      !append_keyval_accessible_graph (accelerator_key, gstring))
    {
      g_string_append (gstring, "Unidentified");
    }

  return g_string_free (gstring, FALSE);
}

/**
 * gtk_accelerator_get_default_mod_mask:
 *
 * Gets the modifier mask.
 *
 * The modifier mask determines which modifiers are considered significant
 * for keyboard accelerators. This includes all keyboard modifiers except
 * for `GDK_LOCK_MASK`.
 *
 * Returns: the modifier mask for accelerators
 */
GdkModifierType
gtk_accelerator_get_default_mod_mask (void)
{
  return GDK_CONTROL_MASK|GDK_SHIFT_MASK|GDK_ALT_MASK|
         GDK_SUPER_MASK|GDK_HYPER_MASK|GDK_META_MASK;
}
