Blob Blame History Raw
/* dzl-shortcut-chord.c
 *
 * Copyright (C) 2017 Christian Hergert <chergert@redhat.com>
 *
 * 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/>.
 */

#define G_LOG_DOMAIN "dzl-shortcut-chord"

#include "config.h"

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

#include "shortcuts/dzl-shortcut-chord.h"
#include "shortcuts/dzl-shortcut-private.h"

#define MAX_CHORD_SIZE 4

G_DEFINE_BOXED_TYPE (DzlShortcutChord, dzl_shortcut_chord,
                     dzl_shortcut_chord_copy, dzl_shortcut_chord_free)
G_DEFINE_POINTER_TYPE (DzlShortcutChordTable, dzl_shortcut_chord_table)

typedef struct
{
  guint           keyval;
  GdkModifierType modifier;
} DzlShortcutKey;

struct _DzlShortcutChord
{
  DzlShortcutKey keys[MAX_CHORD_SIZE];
};

typedef struct
{
  DzlShortcutChord chord;
  gpointer data;
} DzlShortcutChordTableEntry;

struct _DzlShortcutChordTable
{
  DzlShortcutChordTableEntry *entries;
  GDestroyNotify              destroy;
  guint                       len;
  guint                       size;
};

static GdkModifierType
sanitize_modifier_mask (GdkModifierType mods)
{
  mods &= gtk_accelerator_get_default_mod_mask ();
  mods &= ~GDK_LOCK_MASK;

  return mods;
}

static gint
dzl_shortcut_chord_compare (const DzlShortcutChord *a,
                            const DzlShortcutChord *b)
{
  return memcmp (a, b, sizeof *a);
}

static gboolean
dzl_shortcut_chord_is_valid (DzlShortcutChord *self)
{
  g_assert (self != NULL);

  /* Ensure we got a valid first key at least */
  if (self->keys[0].keyval == 0)
    return FALSE;

  return TRUE;
}

DzlShortcutChord *
dzl_shortcut_chord_new_from_event (const GdkEventKey *key)
{
  DzlShortcutChord *self;

  g_return_val_if_fail (key != NULL, NULL);

  /* Ignore modifier keypresses */
  if (key->is_modifier)
    return NULL;

  self = g_slice_new0 (DzlShortcutChord);

  self->keys[0].keyval = gdk_keyval_to_lower (key->keyval);
  self->keys[0].modifier = sanitize_modifier_mask (key->state);

  if ((key->state & GDK_LOCK_MASK) == 0 &&
      self->keys[0].keyval != key->keyval)
    self->keys[0].modifier |= GDK_SHIFT_MASK;

  if (!dzl_shortcut_chord_is_valid (self))
    g_clear_pointer (&self, dzl_shortcut_chord_free);

  return self;
}

DzlShortcutChord *
dzl_shortcut_chord_new_from_string (const gchar *accelerator)
{
  DzlShortcutChord *self;
  g_auto(GStrv) parts = NULL;

  g_return_val_if_fail (accelerator != NULL, NULL);

  /* We might have a single key, or chord defined */
  parts = g_strsplit (accelerator, "|", 0);

  /* Make sure we won't overflow the keys array */
  if (g_strv_length (parts) > G_N_ELEMENTS (self->keys))
    return NULL;

  self = g_slice_new0 (DzlShortcutChord);

  /* Parse each section from the accelerator */
  for (guint i = 0; parts[i]; i++)
    gtk_accelerator_parse (parts[i], &self->keys[i].keyval, &self->keys[i].modifier);

  /* Ensure we got a valid first key at least */
  if (!dzl_shortcut_chord_is_valid (self))
    g_clear_pointer (&self, dzl_shortcut_chord_free);

  return self;
}

gboolean
dzl_shortcut_chord_append_event (DzlShortcutChord  *self,
                                 const GdkEventKey *key)
{
  guint i;

  g_return_val_if_fail (self != NULL, FALSE);
  g_return_val_if_fail (key != NULL, FALSE);

  for (i = 0; i < G_N_ELEMENTS (self->keys); i++)
    {
      if (self->keys[i].keyval == 0)
        {
          self->keys[i].keyval = gdk_keyval_to_lower (key->keyval);
          self->keys[i].modifier = sanitize_modifier_mask (key->state);

          if ((key->state & GDK_LOCK_MASK) == 0 &&
              self->keys[i].keyval != key->keyval)
            self->keys[i].modifier |= GDK_SHIFT_MASK;

          return TRUE;
        }
    }

  return FALSE;
}

static inline guint
dzl_shortcut_chord_count_keys (const DzlShortcutChord *self)
{
  guint count = 0;

  for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++)
    {
      if (self->keys[i].keyval != 0)
        count++;
      else
        break;
    }

  return count;
}

DzlShortcutMatch
dzl_shortcut_chord_match (const DzlShortcutChord *self,
                          const DzlShortcutChord *other)
{
  guint self_count = 0;
  guint other_count = 0;

  g_return_val_if_fail (self != NULL, DZL_SHORTCUT_MATCH_NONE);
  g_return_val_if_fail (other != NULL, DZL_SHORTCUT_MATCH_NONE);

  self_count = dzl_shortcut_chord_count_keys (self);
  other_count = dzl_shortcut_chord_count_keys (other);

  if (self_count > other_count)
    return DZL_SHORTCUT_MATCH_NONE;

  if (0 == memcmp (self->keys, other->keys, sizeof (DzlShortcutKey) * self_count))
    return self_count == other_count ? DZL_SHORTCUT_MATCH_EQUAL : DZL_SHORTCUT_MATCH_PARTIAL;

  return DZL_SHORTCUT_MATCH_NONE;
}

gchar *
dzl_shortcut_chord_to_string (const DzlShortcutChord *self)
{
  GString *str;

  if (self == NULL || self->keys[0].keyval == 0)
    return NULL;

  str = g_string_new (NULL);

  for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++)
    {
      const DzlShortcutKey *key = &self->keys[i];
      g_autofree gchar *name = NULL;

      if (key->keyval == 0 && key->modifier == 0)
        break;

      name = gtk_accelerator_name (key->keyval, key->modifier);

      if (i != 0)
        g_string_append_c (str, '|');

      g_string_append (str, name);
    }

  return g_string_free (str, FALSE);
}

gchar *
dzl_shortcut_chord_get_label (const DzlShortcutChord *self)
{
  GString *str;

  if (self == NULL || self->keys[0].keyval == 0)
    return NULL;

  str = g_string_new (NULL);

  for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++)
    {
      const DzlShortcutKey *key = &self->keys[i];
      g_autofree gchar *name = NULL;

      if (key->keyval == 0 && key->modifier == 0)
        break;

      name = gtk_accelerator_get_label (key->keyval, key->modifier);

      if (i != 0)
        g_string_append_c (str, ' ');

      g_string_append (str, name);
    }

  return g_string_free (str, FALSE);
}

DzlShortcutChord *
dzl_shortcut_chord_copy (const DzlShortcutChord *self)
{
  DzlShortcutChord *copy;

  if (self == NULL)
    return NULL;

  copy = g_slice_new (DzlShortcutChord);
  memcpy (copy, self, sizeof *copy);

  return copy;
}

guint
dzl_shortcut_chord_hash (gconstpointer data)
{
  const DzlShortcutChord *self = data;
  guint hash = 0;

  for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++)
    {
      const DzlShortcutKey *key = &self->keys[i];

      hash ^= key->keyval;
      hash ^= key->modifier;
    }

  return hash;
}

gboolean
dzl_shortcut_chord_equal (gconstpointer data1,
                          gconstpointer data2)
{
  if (data1 == data2)
    return TRUE;
  else if (data1 == NULL || data2 == NULL)
    return FALSE;

  return 0 == memcmp (((const DzlShortcutChord *)data1)->keys,
                      ((const DzlShortcutChord *)data2)->keys,
                      sizeof (DzlShortcutChord));
}

void
dzl_shortcut_chord_free (DzlShortcutChord *self)
{
  if (self != NULL)
    g_slice_free (DzlShortcutChord, self);
}

GType
dzl_shortcut_match_get_type (void)
{
  static GType type_id;

  if (g_once_init_enter (&type_id))
    {
      static GEnumValue values[] = {
        { DZL_SHORTCUT_MATCH_NONE, "DZL_SHORTCUT_MATCH_NONE", "none" },
        { DZL_SHORTCUT_MATCH_EQUAL, "DZL_SHORTCUT_MATCH_EQUAL", "equal" },
        { DZL_SHORTCUT_MATCH_PARTIAL, "DZL_SHORTCUT_MATCH_PARTIAL", "partial" },
        { 0 }
      };
      GType _type_id = g_enum_register_static ("DzlShortcutMatch", values);
      g_once_init_leave (&type_id, _type_id);
    }

  return type_id;
}

static gint
dzl_shortcut_chord_table_sort (gconstpointer a,
                               gconstpointer b)
{
  const DzlShortcutChordTableEntry *keya = a;
  const DzlShortcutChordTableEntry *keyb = b;

  return dzl_shortcut_chord_compare (&keya->chord, &keyb->chord);
}

/**
 * dzl_shortcut_chord_table_new: (skip)
 */
DzlShortcutChordTable *
dzl_shortcut_chord_table_new (void)
{
  DzlShortcutChordTable *table;

  table = g_slice_new0 (DzlShortcutChordTable);
  table->len = 0;
  table->size = 4;
  table->destroy = NULL;
  table->entries = g_new0 (DzlShortcutChordTableEntry, table->size);

  return table;
}

void
dzl_shortcut_chord_table_free (DzlShortcutChordTable *self)
{
  if (self != NULL)
    {
      if (self->destroy != NULL)
        {
          for (guint i = 0; i < self->len; i++)
            self->destroy (self->entries[i].data);
        }
      g_free (self->entries);
      g_slice_free (DzlShortcutChordTable, self);
    }
}

void
dzl_shortcut_chord_table_add (DzlShortcutChordTable  *self,
                              const DzlShortcutChord *chord,
                              gpointer                data)
{
  g_return_if_fail (self != NULL);
  g_return_if_fail (chord != NULL);

  if (self->len == self->size)
    {
      self->size *= 2;
      self->entries = g_renew (DzlShortcutChordTableEntry, self->entries, self->size);
    }

  self->entries[self->len].chord = *chord;
  self->entries[self->len].data = data;

  self->len++;

  qsort (self->entries,
         self->len,
         sizeof (DzlShortcutChordTableEntry),
         dzl_shortcut_chord_table_sort);
}

static void
dzl_shortcut_chord_table_remove_index (DzlShortcutChordTable *self,
                                       guint                  position)
{
  const DzlShortcutChordTableEntry *entry;
  gpointer data;

  g_assert (self != NULL);
  g_assert (position < self->len);

  entry = &self->entries[position];
  data = entry->data;

  if (position + 1 < self->len)
    memmove ((gpointer)entry,
             entry + 1,
             sizeof *entry * (self->len - position - 1));

  self->len--;

  if (self->destroy != NULL)
    self->destroy (data);
}

gboolean
dzl_shortcut_chord_table_remove (DzlShortcutChordTable  *self,
                                 const DzlShortcutChord *chord)
{
  g_return_val_if_fail (self != NULL, FALSE);

  if (chord == NULL)
    return FALSE;

  for (guint i = 0; i < self->len; i++)
    {
      const DzlShortcutChordTableEntry *ele = &self->entries[i];

      if (dzl_shortcut_chord_equal (&ele->chord, chord))
        {
          dzl_shortcut_chord_table_remove_index (self, i);
          return TRUE;
        }
    }

  return FALSE;
}

gboolean
dzl_shortcut_chord_table_remove_data (DzlShortcutChordTable *self,
                                      gpointer               data)
{
  g_return_val_if_fail (self != NULL, FALSE);

  for (guint i = 0; i < self->len; i++)
    {
      const DzlShortcutChordTableEntry *ele = &self->entries[i];

      if (ele->data == data)
        {
          dzl_shortcut_chord_table_remove_index (self, i);
          return TRUE;
        }
    }

  return FALSE;
}

const DzlShortcutChord *
dzl_shortcut_chord_table_lookup_data (DzlShortcutChordTable *self,
                                      gpointer               data)
{
  if (self == NULL)
    return NULL;

  for (guint i = 0; i < self->len; i++)
    {
      const DzlShortcutChordTableEntry *ele = &self->entries[i];

      if (ele->data == data)
        return &ele->chord;
    }

  return NULL;
}

static gint
dzl_shortcut_chord_find_partial (gconstpointer a,
                                 gconstpointer b)
{
  const DzlShortcutChord *key = a;
  const DzlShortcutChordTableEntry *element = b;

  /*
   * We are only looking for a partial match here so that we can walk backwards
   * after the bsearch to the first partial match.
   */
  if (dzl_shortcut_chord_match (key, &element->chord) != DZL_SHORTCUT_MATCH_NONE)
    return 0;

  return dzl_shortcut_chord_compare (key, &element->chord);
}

DzlShortcutMatch
dzl_shortcut_chord_table_lookup (DzlShortcutChordTable  *self,
                                 const DzlShortcutChord *chord,
                                 gpointer               *data)
{
  const DzlShortcutChordTableEntry *match;

  if (data != NULL)
    *data = NULL;

  if (self == NULL)
    return DZL_SHORTCUT_MATCH_NONE;

  if (chord == NULL)
    return DZL_SHORTCUT_MATCH_NONE;

  if (self->len == 0)
    return DZL_SHORTCUT_MATCH_NONE;

  /*
   * This function works by performing a binary search to locate ourself
   * somewhere within a match zone of the array. Once we are there, we walk
   * back to the first item that is a partial match.  After that, we walk
   * through every potential match looking for an exact match until we reach a
   * non-partial-match or the end of the array.
   *
   * Based on our findings, we return the appropriate DzlShortcutMatch.
   */

  match = bsearch (chord, self->entries, self->len, sizeof (DzlShortcutChordTableEntry),
                   dzl_shortcut_chord_find_partial);

  if (match != NULL)
    {
      const DzlShortcutChordTableEntry *begin = self->entries;
      const DzlShortcutChordTableEntry *end = self->entries + self->len;
      DzlShortcutMatch ret = DZL_SHORTCUT_MATCH_PARTIAL;

      /* Find the first patial match */
      while ((match - 1) >= begin &&
             dzl_shortcut_chord_match (chord, &(match - 1)->chord) != DZL_SHORTCUT_MATCH_NONE)
        match--;

      g_assert (match >= begin);

      /* Now walk forward to see if we have an exact match */
      while (DZL_SHORTCUT_MATCH_NONE != (ret = dzl_shortcut_chord_match (chord, &match->chord)))
        {
          if (ret == DZL_SHORTCUT_MATCH_EQUAL)
            {
              if (data != NULL)
                *data = match->data;
              return DZL_SHORTCUT_MATCH_EQUAL;
            }

          match++;

          g_assert (match <= end);

          if (ret == 0 || match == end)
            break;
        }

      return DZL_SHORTCUT_MATCH_PARTIAL;
    }

  return DZL_SHORTCUT_MATCH_NONE;
}

void
dzl_shortcut_chord_table_set_free_func (DzlShortcutChordTable *self,
                                        GDestroyNotify         destroy)
{
  g_return_if_fail (self != NULL);

  self->destroy = destroy;
}

guint
dzl_shortcut_chord_table_size (const DzlShortcutChordTable *self)
{
  /* I know this is confusing, but len is the number of items in the
   * table (which consumers think of as size), and @size is the allocated
   * size of the ^2 growing array.
   */
  return self ? self->len : 0;
}

void
dzl_shortcut_chord_table_printf (const DzlShortcutChordTable *self)
{
  if (self == NULL)
    return;

  for (guint i = 0; i < self->len; i++)
    {
      const DzlShortcutChordTableEntry *entry = &self->entries[i];
      g_autofree gchar *str = dzl_shortcut_chord_to_string (&entry->chord);

      g_print ("%s\n", str);
    }
}

void
_dzl_shortcut_chord_table_iter_init (DzlShortcutChordTableIter *iter,
                                     DzlShortcutChordTable     *table)
{
  g_return_if_fail (iter != NULL);

  iter->table = table;
  iter->position = 0;
}

gboolean
_dzl_shortcut_chord_table_iter_next (DzlShortcutChordTableIter  *iter,
                                     const DzlShortcutChord    **chord,
                                     gpointer                   *value)
{
  g_return_val_if_fail (iter != NULL, FALSE);

  /*
   * Be safe against NULL tables which we allow in
   * _dzl_shortcut_chord_table_iter_init() for convenience.
   */
  if (iter->table == NULL)
    return FALSE;

  if (iter->position < iter->table->len)
    {
      *chord = &iter->table->entries[iter->position].chord;
      *value = iter->table->entries[iter->position].data;
      iter->position++;
      return TRUE;
    }

  return FALSE;
}

void
_dzl_shortcut_chord_table_iter_steal (DzlShortcutChordTableIter  *iter)
{
  g_return_if_fail (iter != NULL);
  g_return_if_fail (iter->table != NULL);

  if (iter->position > 0 && iter->position < iter->table->len)
    {
      dzl_shortcut_chord_table_remove_index (iter->table, --iter->position);
      return;
    }

  g_warning ("Attempt to steal item from table that does not exist");
}

gboolean
dzl_shortcut_chord_has_modifier (const DzlShortcutChord *self)
{
  g_return_val_if_fail (self != NULL, FALSE);

  return self->keys[0].modifier != 0;
}

guint
dzl_shortcut_chord_get_length (const DzlShortcutChord *self)
{
  if (self != NULL)
    {
      for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++)
        {
          if (self->keys[i].keyval == 0)
            return i;
        }

      return G_N_ELEMENTS (self->keys);
    }

  return 0;
}

void
dzl_shortcut_chord_get_nth_key (const DzlShortcutChord *self,
                                guint                   nth,
                                guint                  *keyval,
                                GdkModifierType        *modifier)
{
  if (nth < G_N_ELEMENTS (self->keys))
    {
      if (keyval)
        *keyval = self->keys[nth].keyval;
      if (modifier)
        *modifier = self->keys[nth].modifier;
    }
  else
    {
      if (keyval)
        *keyval = 0;
      if (modifier)
        *modifier = 0;
    }
}

/**
 * dzl_shortcut_chord_table_foreach:
 * @self: a #DzlShortcutChordTable
 * @foreach_func: (scope call) (closure foreach_data): A callback for each chord
 * @foreach_data: user data for @foreach_func
 *
 * This function will call @foreach_func for each chord in the table.
 */
void
dzl_shortcut_chord_table_foreach (const DzlShortcutChordTable  *self,
                                  DzlShortcutChordTableForeach  foreach_func,
                                  gpointer                      foreach_data)
{
  g_return_if_fail (foreach_func != NULL);

  if (self == NULL)
    return;

  /*
   * Walk backwards just in case the caller somehow thinks it is okay to
   * remove items while iterating the list. We don't officially support that
   * (which is why self is const), but this is just defensive.
   */

  for (guint i = self->len; i > 0; i--)
    {
      const DzlShortcutChordTableEntry *entry = &self->entries[i-1];

      foreach_func (&entry->chord, entry->data, foreach_data);
    }
}