[gnome-builder/wip/chergert/shortcuts] shortcuts: start on new shortcuts engine



commit b8785feca859e366ce25fe04d73246bc22e18a55
Author: Christian Hergert <chergert redhat com>
Date:   Wed Nov 23 17:20:09 2016 -0800

    shortcuts: start on new shortcuts engine
    
    This starts laying the groundwork for the basic plumbing. In general, we
    have an IdeShortcutManager singleton for the process. This tracks the
    current keytheme and provides an easy entry point for shortcut consuming
    machinery.
    
    Widgets gain the concept of an IdeShortcutController, who processes
    incoming events and dispatches them to the current IdeShortcutContext
    for the controller. By using the "set-context" signal on the controller,
    widgets can change their context before processing the next signal
    (however, the signal chain will continue from the original context).
    
    The IdeWorkbench dispatches key-press-events to the shortcut manager who
    returns TRUE if they were handled. This could happen because of the current
    focus chain, or another widget in the hierarchy whose controller was
    registered with the toplevel.
    
    To chain multiple shortcut handlers together, simply call the _add_*()
    variant on IdeShortcutContext with your additional operation. All
    registered entries for that shortcut will be called in the order they were
    added to the context. So to call an action, then a signal, do:
    
      ide_shortcut_context_add_action(context, "<primary>c", ā€¦)
      ide_shortcut_context_add_signal(context, "<primary>c", ā€¦)
    
    Some things to think about still:
    
     - We need the concept of a "keybinding slot" so that it is easy to
       provide UI to override or change keybindings. That means, to some
       degree, providing title/subtitle information when registering
       accelerators. We also need to find a way to hook that up into the
       shortcuts window (so we need section/group as well).
    
     - We need code to integrate into the EggMenuManager and update the
       accelerator information so that menus are displayed with proper
       accel labels.
    
     - We need a new IdeShortcutLabel that can be used in tooltips and other
       places to show keybindings similar to the CSS I made for the
       shortcuts window.
    
     - Now that we will have acclerators registered at the toplevel, we can
       theoretically do a "highlight mode" that shows accelerators and what
       they affect in the UI. But doing this cleanly becomes a fun layout
       challenge more than anything.

 libide/Makefile.am                         |    8 +
 libide/ide.h                               |    4 +
 libide/shortcuts/ide-shortcut-context.c    |  726 ++++++++++++++++++++++++++++
 libide/shortcuts/ide-shortcut-context.h    |   60 +++
 libide/shortcuts/ide-shortcut-controller.c |  541 +++++++++++++++++++++
 libide/shortcuts/ide-shortcut-controller.h |   42 ++
 libide/shortcuts/ide-shortcut-manager.c    |  393 +++++++++++++++
 libide/shortcuts/ide-shortcut-manager.h    |   45 ++
 libide/shortcuts/ide-shortcut-theme.c      |  187 +++++++
 libide/shortcuts/ide-shortcut-theme.h      |   40 ++
 libide/workbench/ide-workbench.c           |   15 +
 11 files changed, 2061 insertions(+), 0 deletions(-)
---
diff --git a/libide/Makefile.am b/libide/Makefile.am
index 833a67e..ed6ce71 100644
--- a/libide/Makefile.am
+++ b/libide/Makefile.am
@@ -120,6 +120,10 @@ libide_1_0_la_public_headers =                            \
        search/ide-search-provider.h                      \
        search/ide-search-reducer.h                       \
        search/ide-search-result.h                        \
+       shortcuts/ide-shortcut-context.h                  \
+       shortcuts/ide-shortcut-controller.h               \
+       shortcuts/ide-shortcut-manager.h                  \
+       shortcuts/ide-shortcut-theme.h                    \
        snippets/ide-source-snippet-chunk.h               \
        snippets/ide-source-snippet-context.h             \
        snippets/ide-source-snippet.h                     \
@@ -291,6 +295,10 @@ libide_1_0_la_public_sources =                            \
        runtimes/ide-runtime.c                            \
        scripting/ide-script-manager.c                    \
        scripting/ide-script.c                            \
+       shortcuts/ide-shortcut-context.c                  \
+       shortcuts/ide-shortcut-controller.c               \
+       shortcuts/ide-shortcut-manager.c                  \
+       shortcuts/ide-shortcut-theme.c                    \
        search/ide-omni-search-display.c                  \
        search/ide-omni-search-entry.c                    \
        search/ide-omni-search-group.c                    \
diff --git a/libide/ide.h b/libide/ide.h
index fd397e6..05fed11 100644
--- a/libide/ide.h
+++ b/libide/ide.h
@@ -102,6 +102,10 @@ G_BEGIN_DECLS
 #include "runtimes/ide-runtime.h"
 #include "scripting/ide-script-manager.h"
 #include "scripting/ide-script.h"
+#include "shortcuts/ide-shortcut-context.h"
+#include "shortcuts/ide-shortcut-controller.h"
+#include "shortcuts/ide-shortcut-manager.h"
+#include "shortcuts/ide-shortcut-theme.h"
 #include "search/ide-omni-search-row.h"
 #include "search/ide-pattern-spec.h"
 #include "search/ide-search-context.h"
diff --git a/libide/shortcuts/ide-shortcut-context.c b/libide/shortcuts/ide-shortcut-context.c
new file mode 100644
index 0000000..e72d97e
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-context.c
@@ -0,0 +1,726 @@
+/* ide-shortcut-context.c
+ *
+ * Copyright (C) 2016 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 3 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 "ide-shortcut-context"
+
+#include <gobject/gvaluecollector.h>
+#include <string.h>
+
+#include "ide-macros.h"
+
+#include "shortcuts/ide-shortcut-context.h"
+#include "shortcuts/ide-shortcut-controller.h"
+
+typedef enum
+{
+  SHORTCUT_ACTION = 1,
+  SHORTCUT_SIGNAL,
+} ShortcutType;
+
+typedef struct _Shortcut
+{
+  ShortcutType    type;
+  GdkModifierType modifier;
+  guint           keyval;
+  union {
+    struct {
+      const gchar *prefix;
+      const gchar *name;
+      GVariant    *param;
+    } action;
+    struct {
+      const gchar *name;
+      GQuark       detail;
+      GArray      *params;
+    } signal;
+  };
+  struct _Shortcut *next;
+} Shortcut;
+
+typedef struct
+{
+  gchar      *name;
+  GHashTable *keymap;
+  guint       use_binding_sets : 1;
+} IdeShortcutContextPrivate;
+
+enum {
+  PROP_0,
+  PROP_NAME,
+  PROP_USE_BINDING_SETS,
+  N_PROPS
+};
+
+struct _IdeShortcutContext { GObject object; };
+G_DEFINE_TYPE_WITH_PRIVATE (IdeShortcutContext, ide_shortcut_context, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+translate_keyval (guint             *keyval,
+                  GdkModifierType   *modifier)
+{
+  *modifier = *modifier & gtk_accelerator_get_default_mod_mask () & ~GDK_RELEASE_MASK;
+
+  if (*modifier & GDK_SHIFT_MASK)
+    {
+      if (*keyval == GDK_KEY_Tab)
+        *keyval = GDK_KEY_ISO_Left_Tab;
+      else
+        *keyval = gdk_keyval_to_upper (*keyval);
+    }
+}
+
+static void
+shortcut_free (gpointer data)
+{
+  Shortcut *shortcut = data;
+
+  if (shortcut != NULL)
+    {
+      g_clear_pointer (&shortcut->next, shortcut_free);
+
+      switch (shortcut->type)
+        {
+        case SHORTCUT_ACTION:
+          g_clear_pointer (&shortcut->action.param, g_variant_unref);
+          break;
+
+        case SHORTCUT_SIGNAL:
+          g_array_unref (shortcut->signal.params);
+          break;
+
+        default:
+          g_assert_not_reached ();
+        }
+
+      g_slice_free (Shortcut, shortcut);
+    }
+}
+
+static guint
+shortcut_hash (gconstpointer data)
+{
+  const Shortcut *shortcut = data;
+
+  return shortcut->keyval ^ shortcut->modifier;
+}
+
+static gboolean
+shortcut_equal (gconstpointer a,
+                gconstpointer b)
+{
+  const Shortcut *as = a;
+  const Shortcut *bs = b;
+
+  return as->keyval == bs->keyval && as->modifier == bs->modifier;
+}
+
+static gboolean
+widget_action (GtkWidget   *widget,
+               const gchar *prefix,
+               const gchar *action_name,
+               GVariant    *parameter)
+{
+  GtkWidget *toplevel;
+  GApplication *app;
+  GActionGroup *group = NULL;
+
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (prefix != NULL);
+  g_assert (action_name != NULL);
+
+  app = g_application_get_default ();
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  while ((group == NULL) && (widget != NULL))
+    {
+      group = gtk_widget_get_action_group (widget, prefix);
+
+      if G_UNLIKELY (GTK_IS_POPOVER (widget))
+        {
+          GtkWidget *relative_to;
+
+          relative_to = gtk_popover_get_relative_to (GTK_POPOVER (widget));
+
+          if (relative_to != NULL)
+            widget = relative_to;
+          else
+            widget = gtk_widget_get_parent (widget);
+        }
+      else
+        {
+          widget = gtk_widget_get_parent (widget);
+        }
+    }
+
+  if (!group && g_str_equal (prefix, "win") && G_IS_ACTION_GROUP (toplevel))
+    group = G_ACTION_GROUP (toplevel);
+
+  if (!group && g_str_equal (prefix, "app") && G_IS_ACTION_GROUP (app))
+    group = G_ACTION_GROUP (app);
+
+  if (group && g_action_group_has_action (group, action_name))
+    {
+      g_action_group_activate_action (group, action_name, parameter);
+      return TRUE;
+    }
+
+  g_warning ("Failed to locate action %s.%s", prefix, action_name);
+
+  return FALSE;
+}
+
+static gboolean
+shortcut_action_activate (Shortcut          *shortcut,
+                          GtkWidget         *widget,
+                          const GdkEventKey *event)
+{
+  g_assert (shortcut != NULL);
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (event != NULL);
+
+  return widget_action (widget,
+                        shortcut->action.prefix,
+                        shortcut->action.name,
+                        shortcut->action.param);
+}
+
+static gboolean
+find_instance_and_signal (GtkWidget          *widget,
+                          const gchar        *signal_name,
+                          gpointer           *instance,
+                          GSignalQuery       *query)
+{
+  IdeShortcutController *controller;
+
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (signal_name != NULL);
+  g_assert (instance != NULL);
+  g_assert (query != NULL);
+
+  *instance = NULL;
+
+  /*
+   * First we want to see if we can resolve the signal on the widgets
+   * controller (if there is one). This allows us to change contexts
+   * from signals without installing signals on the actual widgets.
+   */
+
+  controller = ide_shortcut_controller_find (widget);
+
+  if (controller != NULL)
+    {
+      guint signal_id;
+
+      signal_id = g_signal_lookup (signal_name, G_OBJECT_TYPE (controller));
+
+      if (signal_id != 0)
+        {
+          g_signal_query (signal_id, query);
+          *instance = controller;
+          return TRUE;
+        }
+    }
+
+  /*
+   * This diverts from Gtk signal keybindings a bit in that we
+   * allow you to activate a signal on any widget in the focus
+   * hierarchy starting from the provided widget up.
+   */
+
+  while (widget != NULL)
+    {
+      guint signal_id;
+
+      signal_id = g_signal_lookup (signal_name, G_OBJECT_TYPE (widget));
+
+      if (signal_id != 0)
+        {
+          g_signal_query (signal_id, query);
+          *instance = widget;
+          return TRUE;
+        }
+
+      widget = gtk_widget_get_parent (widget);
+    }
+
+  return FALSE;
+}
+
+static gboolean
+shortcut_signal_activate (Shortcut          *shortcut,
+                          GtkWidget         *widget,
+                          const GdkEventKey *event)
+{
+  GValue *params;
+  GValue return_value = { 0 };
+  GSignalQuery query;
+  gpointer instance = NULL;
+
+  g_assert (shortcut != NULL);
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (event != NULL);
+
+  if (!find_instance_and_signal (widget, shortcut->signal.name, &instance, &query))
+    {
+      g_warning ("Failed to locate signal %s in hierarchy of %s",
+                 shortcut->signal.name, G_OBJECT_TYPE_NAME (widget));
+      return TRUE;
+    }
+
+  if (query.n_params != shortcut->signal.params->len)
+    goto parameter_mismatch;
+
+  for (guint i = 0; i < query.n_params; i++)
+    {
+      if (!G_VALUE_HOLDS (&g_array_index (shortcut->signal.params, GValue, i), query.param_types[i]))
+        goto parameter_mismatch;
+    }
+
+  params = g_new0 (GValue, 1 + query.n_params);
+  g_value_init_from_instance (&params[0], instance);
+  for (guint i = 0; i < query.n_params; i++)
+    {
+      GValue *src_value = &g_array_index (shortcut->signal.params, GValue, i);
+
+      g_value_init (&params[1+i], G_VALUE_TYPE (src_value));
+      g_value_copy (src_value, &params[1+i]);
+    }
+
+  if (query.return_type != G_TYPE_NONE)
+    g_value_init (&return_value, query.return_type);
+
+  g_signal_emitv (params, query.signal_id, shortcut->signal.detail, &return_value);
+
+  for (guint i = 0; i < query.n_params + 1; i++)
+    g_value_unset (&params[i]);
+  g_free (params);
+
+  return GDK_EVENT_STOP;
+
+parameter_mismatch:
+  g_warning ("The parameters are not correct for signal %s",
+             shortcut->signal.name);
+
+  /*
+   * If there was a bug with the signal descriptor, we still want
+   * to swallow the event to keep it from propagating further.
+   */
+
+  return GDK_EVENT_STOP;
+}
+
+static gboolean
+shortcut_activate (Shortcut          *shortcut,
+                   GtkWidget         *widget,
+                   const GdkEventKey *event)
+{
+  gboolean handled = FALSE;
+
+  g_assert (shortcut != NULL);
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (event != NULL);
+
+  for (; shortcut != NULL; shortcut = shortcut->next)
+    {
+      switch (shortcut->type)
+        {
+        case SHORTCUT_ACTION:
+          handled |= shortcut_action_activate (shortcut, widget, event);
+          break;
+
+        case SHORTCUT_SIGNAL:
+          handled |= shortcut_signal_activate (shortcut, widget, event);
+          break;
+
+        default:
+          g_assert_not_reached ();
+          return FALSE;
+        }
+    }
+
+  return handled;
+}
+
+static void
+ide_shortcut_context_finalize (GObject *object)
+{
+  IdeShortcutContext *self = (IdeShortcutContext *)object;
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+
+  g_clear_pointer (&priv->name, g_free);
+  g_clear_pointer (&priv->keymap, g_hash_table_unref);
+
+  G_OBJECT_CLASS (ide_shortcut_context_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_context_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  IdeShortcutContext *self = (IdeShortcutContext *)object;
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      g_value_set_string (value, priv->name);
+      break;
+
+    case PROP_USE_BINDING_SETS:
+      g_value_set_boolean (value, priv->use_binding_sets);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_context_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  IdeShortcutContext *self = (IdeShortcutContext *)object;
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      priv->name = g_value_dup_string (value);
+      break;
+
+    case PROP_USE_BINDING_SETS:
+      priv->use_binding_sets = g_value_get_boolean (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_context_class_init (IdeShortcutContextClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_shortcut_context_finalize;
+  object_class->get_property = ide_shortcut_context_get_property;
+  object_class->set_property = ide_shortcut_context_set_property;
+
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "Name",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_USE_BINDING_SETS] =
+    g_param_spec_boolean ("use-binding-sets",
+                          "Use Binding Sets",
+                          "If the context should allow activation using binding sets",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+  
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_shortcut_context_init (IdeShortcutContext *self)
+{
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+
+  priv->use_binding_sets = TRUE;
+}
+
+IdeShortcutContext *
+ide_shortcut_context_new (const gchar *name)
+{
+  return g_object_new (IDE_TYPE_SHORTCUT_CONTEXT,
+                       "name", name,
+                       NULL);
+}
+
+const gchar *
+ide_shortcut_context_get_name (IdeShortcutContext *self)
+{
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), NULL);
+
+  return priv->name;
+}
+
+gboolean
+ide_shortcut_context_activate (IdeShortcutContext *self,
+                               GtkWidget          *widget,
+                               const GdkEventKey  *event)
+{
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), FALSE);
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE);
+  g_return_val_if_fail (event != NULL, FALSE);
+
+  if (priv->keymap != NULL)
+    {
+      Shortcut lookup = { 0 };
+      Shortcut *shortcut;
+
+      lookup.keyval = event->keyval;
+      lookup.modifier = event->state;
+
+      translate_keyval (&lookup.keyval, &lookup.modifier);
+
+      shortcut = g_hash_table_lookup (priv->keymap, &lookup);
+
+      if (shortcut != NULL)
+        return shortcut_activate (shortcut, widget, event);
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_shortcut_context_add (IdeShortcutContext *self,
+                          Shortcut           *shortcut)
+{
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+  Shortcut *head;
+
+  g_assert (IDE_IS_SHORTCUT_CONTEXT (self));
+  g_assert (shortcut != NULL);
+
+  if (priv->keymap == NULL)
+    priv->keymap = g_hash_table_new_full (shortcut_hash, shortcut_equal, NULL, shortcut_free);
+
+  translate_keyval (&shortcut->keyval, &shortcut->modifier);
+
+  /*
+   * If we find that there is another entry for this shortcut, we chain onto
+   * the end of that item. This allows us to call multiple signals, or
+   * interleave signals and actions.
+   */
+
+  if (g_hash_table_lookup_extended (priv->keymap, shortcut, (gpointer *)&head, NULL))
+    {
+      while (head->next != NULL)
+        head = head->next;
+      head->next = shortcut;
+    }
+  else
+    {
+      g_hash_table_insert (priv->keymap, shortcut, shortcut);
+    }
+}
+
+void
+ide_shortcut_context_add_action (IdeShortcutContext *self,
+                                 const gchar        *accel,
+                                 const gchar        *detailed_action_name)
+{
+  Shortcut *shortcut;
+  g_autofree gchar *action_name = NULL;
+  g_autofree gchar *prefix = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) action_target = NULL;
+  const gchar *dot;
+  const gchar *name;
+  GdkModifierType modifier = 0;
+  guint keyval = 0;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTEXT (self));
+  g_return_if_fail (accel != NULL);
+  g_return_if_fail (detailed_action_name != NULL);
+
+  gtk_accelerator_parse (accel, &keyval, &modifier);
+
+  if (!g_action_parse_detailed_name (detailed_action_name, &action_name, &action_target, &error))
+    {
+      g_warning ("%s", error->message);
+      return;
+    }
+
+  if (NULL != (dot = strchr (action_name, '.')))
+    {
+      name = &dot[1];
+      prefix = g_strndup (action_name, dot - action_name);
+    }
+  else
+    {
+      name = action_name;
+      prefix = NULL;
+    }
+
+  shortcut = g_slice_new0 (Shortcut);
+  shortcut->type = SHORTCUT_ACTION;
+  shortcut->keyval = keyval;
+  shortcut->modifier = modifier;
+  shortcut->action.prefix = prefix ? g_intern_string (prefix) : NULL;
+  shortcut->action.name = g_intern_string (name);
+  shortcut->action.param = g_steal_pointer (&action_target);
+
+  ide_shortcut_context_add (self, shortcut);
+}
+
+void
+ide_shortcut_context_add_signal_va_list (IdeShortcutContext *self,
+                                         const gchar        *accel,
+                                         const gchar        *signal_name,
+                                         guint               n_args,
+                                         va_list             args)
+{
+  g_autoptr(GArray) params = NULL;
+  g_autofree gchar *truncated_name = NULL;
+  const gchar *detail_str;
+  Shortcut *shortcut;
+  GdkModifierType modifier = 0;
+  guint keyval = 0;
+  GQuark detail = 0;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTEXT (self));
+  g_return_if_fail (accel != NULL);
+  g_return_if_fail (signal_name != NULL);
+
+  gtk_accelerator_parse (accel, &keyval, &modifier);
+
+  if (NULL != (detail_str = strstr (signal_name, "::")))
+    {
+      truncated_name = g_strndup (signal_name, detail_str - signal_name);
+      signal_name = truncated_name;
+      detail_str = &detail_str[2];
+      detail = g_quark_try_string (detail_str);
+    }
+
+  params = g_array_new (FALSE, FALSE, sizeof (GValue));
+  g_array_set_clear_func (params, (GDestroyNotify)g_value_unset);
+
+  for (; n_args > 0; n_args--)
+    {
+      g_autofree gchar *errstr = NULL;
+      GValue value = { 0 };
+      GType type;
+
+      type = va_arg (args, GType);
+
+      G_VALUE_COLLECT_INIT (&value, type, args, 0, &errstr);
+
+      if (errstr != NULL)
+        {
+          g_warning ("%s", errstr);
+          break;
+        }
+
+      g_array_append_val (params, value);
+    }
+
+  shortcut = g_slice_new0 (Shortcut);
+  shortcut->type = SHORTCUT_SIGNAL;
+  shortcut->keyval = keyval;
+  shortcut->modifier = modifier;
+  shortcut->signal.name = g_intern_string (signal_name);
+  shortcut->signal.detail = detail;
+  shortcut->signal.params = g_steal_pointer (&params);
+
+  ide_shortcut_context_add (self, shortcut);
+}
+
+void
+ide_shortcut_context_add_signal (IdeShortcutContext *self,
+                                 const gchar        *accel,
+                                 const gchar        *signal_name,
+                                 guint               n_args,
+                                 ...)
+{
+  va_list args;
+
+  va_start (args, n_args);
+  ide_shortcut_context_add_signal_va_list (self, accel, signal_name, n_args, args);
+  va_end (args);
+}
+
+gboolean
+ide_shortcut_context_remove (IdeShortcutContext *self,
+                             const gchar        *accel)
+{
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+  Shortcut lookup = { 0 };
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), FALSE);
+  g_return_val_if_fail (accel != NULL, FALSE);
+
+  gtk_accelerator_parse (accel, &lookup.keyval, &lookup.modifier);
+
+  translate_keyval (&lookup.keyval, &lookup.modifier);
+
+  return g_hash_table_remove (priv->keymap, &lookup);
+}
+
+gboolean
+ide_shortcut_context_load_from_data (IdeShortcutContext  *self,
+                                     const gchar         *data,
+                                     gssize               len,
+                                     GError             **error)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), FALSE);
+  g_return_val_if_fail (data != NULL, FALSE);
+
+  if (len < 0)
+    len = strlen (data);
+
+  g_set_error (error,
+               G_IO_ERROR,
+               G_IO_ERROR_INVALID_DATA,
+               "Failed to parse shortcut data");
+
+  return FALSE;
+}
+
+gboolean
+ide_shortcut_context_load_from_resource (IdeShortcutContext  *self,
+                                         const gchar         *resource_path,
+                                         GError             **error)
+{
+  g_autoptr(GBytes) bytes = NULL;
+  const gchar *endptr = NULL;
+  const gchar *data;
+  gsize len;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), FALSE);
+
+  if (NULL == (bytes = g_resources_lookup_data (resource_path, 0, error)))
+    return FALSE;
+
+  data = g_bytes_get_data (bytes, &len);
+
+  if (!g_utf8_validate (data, len, &endptr))
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_DATA,
+                   "Invalid UTF-8 at offset %u",
+                   (guint)(endptr - data));
+      return FALSE;
+    }
+
+  return ide_shortcut_context_load_from_data (self, data, len, error);
+}
diff --git a/libide/shortcuts/ide-shortcut-context.h b/libide/shortcuts/ide-shortcut-context.h
new file mode 100644
index 0000000..3bcbb48
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-context.h
@@ -0,0 +1,60 @@
+/* ide-shortcut-context.h
+ *
+ * Copyright (C) 2016 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 3 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/>.
+ */
+
+#ifndef IDE_SHORTCUT_CONTEXT_H
+#define IDE_SHORTCUT_CONTEXT_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_CONTEXT (ide_shortcut_context_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutContext, ide_shortcut_context, IDE, SHORTCUT_CONTEXT, GObject)
+
+IdeShortcutContext *ide_shortcut_context_new                (const gchar        *name);
+const gchar        *ide_shortcut_context_get_name           (IdeShortcutContext *self);
+gboolean            ide_shortcut_context_activate           (IdeShortcutContext *self,
+                                                             GtkWidget          *widget,
+                                                             const GdkEventKey  *event);
+void                ide_shortcut_context_add_action         (IdeShortcutContext *self,
+                                                             const gchar        *accel,
+                                                             const gchar        *detailed_action_name);
+void                ide_shortcut_context_add_signal         (IdeShortcutContext *self,
+                                                             const gchar        *accel,
+                                                             const gchar        *signal_name,
+                                                             guint               n_args,
+                                                             ...);
+void                ide_shortcut_context_add_signal_va_list (IdeShortcutContext *self,
+                                                             const gchar        *accel,
+                                                             const gchar        *signal_name,
+                                                             guint               n_args,
+                                                             va_list             args);
+gboolean            ide_shortcut_context_remove             (IdeShortcutContext *self,
+                                                             const gchar        *accel);
+gboolean            ide_shortcut_context_load_from_data     (IdeShortcutContext  *self,
+                                                             const gchar         *data,
+                                                             gssize               len,
+                                                             GError             **error);
+gboolean            ide_shortcut_context_load_from_resource (IdeShortcutContext  *self,
+                                                             const gchar         *resource_path,
+                                                             GError             **error);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_CONTEXT_H */
diff --git a/libide/shortcuts/ide-shortcut-controller.c b/libide/shortcuts/ide-shortcut-controller.c
new file mode 100644
index 0000000..96f5f05
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-controller.c
@@ -0,0 +1,541 @@
+/* ide-shortcut-controller.c
+ *
+ * Copyright (C) 2016 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 3 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 "ide-shortcut-controller"
+
+#include "shortcuts/ide-shortcut-context.h"
+#include "shortcuts/ide-shortcut-controller.h"
+#include "shortcuts/ide-shortcut-manager.h"
+
+typedef struct
+{
+  /*
+   * This is the widget for which we are the shortcut controller. There are
+   * zero or one shortcut controller for a given widget. These are persistent
+   * and dispatch events to the current IdeShortcutContext (which can be
+   * changed upon theme changes or shortcuts emitting the ::set-context signal.
+   */
+  GtkWidget *widget;
+
+  /*
+   * This is the current context for the controller. These are collections of
+   * shortcuts to signals, actions, etc. The context can be changed in reaction
+   * to different events.
+   */
+  IdeShortcutContext *context;
+
+  /*
+   * This is a pointer to the root controller for the window. We register with
+   * the root controller so that keybindings can be activated even when the
+   * focus widget is somewhere else.
+   */
+  IdeShortcutController *root;
+
+  /*
+   * The root controller keeps track of the children controllers in the window.
+   * Instead of allocating GList entries, we use an inline GList for the Queue
+   * link nodes.
+   */
+  GQueue descendants;
+  GList  descendants_link;
+
+  /*
+   * Signal handlers to react to various changes in the system.
+   */
+  gulong hierarchy_changed_handler;
+  gulong widget_destroy_handler;
+} IdeShortcutControllerPrivate;
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_WIDGET,
+  N_PROPS
+};
+
+enum {
+  RESET,
+  SET_CONTEXT_NAMED,
+  N_SIGNALS
+};
+
+struct _IdeShortcutController { GObject object; };
+G_DEFINE_TYPE_WITH_PRIVATE (IdeShortcutController, ide_shortcut_controller, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+static guint       signals [N_SIGNALS];
+static GQuark      root_quark;
+static GQuark      controller_quark;
+
+static void ide_shortcut_controller_connect    (IdeShortcutController *self);
+static void ide_shortcut_controller_disconnect (IdeShortcutController *self);
+
+static gboolean
+ide_shortcut_controller_is_mapped (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  return priv->widget != NULL && gtk_widget_get_mapped (priv->widget);
+}
+
+static void
+ide_shortcut_controller_add (IdeShortcutController *self,
+                             IdeShortcutController *descendant)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  IdeShortcutControllerPrivate *dpriv = ide_shortcut_controller_get_instance_private (descendant);
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (descendant));
+
+  g_object_ref (descendant);
+
+  if (ide_shortcut_controller_is_mapped (descendant))
+    g_queue_push_head_link (&priv->descendants, &dpriv->descendants_link);
+  else
+    g_queue_push_tail_link (&priv->descendants, &dpriv->descendants_link);
+}
+
+static void
+ide_shortcut_controller_remove (IdeShortcutController *self,
+                                IdeShortcutController *descendant)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  IdeShortcutControllerPrivate *dpriv = ide_shortcut_controller_get_instance_private (descendant);
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (descendant));
+
+  g_queue_unlink (&priv->descendants, &dpriv->descendants_link);
+}
+
+static void
+ide_shortcut_controller_widget_destroy (IdeShortcutController *self,
+                                        GtkWidget             *widget)
+{
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  ide_shortcut_controller_disconnect (self);
+}
+
+static void
+ide_shortcut_controller_widget_hierarchy_changed (IdeShortcutController *self,
+                                                  GtkWidget             *widget)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  g_object_ref (self);
+
+  /*
+   * Here we register our controller with the toplevel controller. If that
+   * widget doesn't yet have a placeholder toplevel controller, then we
+   * create that and attach to it.
+   *
+   * The toplevel controller is used to dispatch events from the window
+   * to any controller that could be activating for the window.
+   */
+
+  if (priv->root != NULL)
+    {
+      ide_shortcut_controller_remove (priv->root, self);
+      g_clear_object (&priv->root);
+    }
+
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  if (toplevel != widget)
+    {
+      priv->root = g_object_get_qdata (G_OBJECT (toplevel), root_quark);
+      if (priv->root == NULL)
+        priv->root = ide_shortcut_controller_new (toplevel);
+      ide_shortcut_controller_add (priv->root, self);
+    }
+
+  g_object_unref (self);
+}
+
+static void
+ide_shortcut_controller_disconnect (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (GTK_IS_WIDGET (priv->widget));
+
+  g_signal_handler_disconnect (priv->widget, priv->widget_destroy_handler);
+  priv->widget_destroy_handler = 0;
+
+  g_signal_handler_disconnect (priv->widget, priv->hierarchy_changed_handler);
+  priv->hierarchy_changed_handler = 0;
+}
+
+static void
+ide_shortcut_controller_connect (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (GTK_IS_WIDGET (priv->widget));
+
+  priv->widget_destroy_handler =
+    g_signal_connect_swapped (priv->widget,
+                              "destroy",
+                              G_CALLBACK (ide_shortcut_controller_widget_destroy),
+                              self);
+
+  priv->hierarchy_changed_handler =
+    g_signal_connect_swapped (priv->widget,
+                              "hierarchy-changed",
+                              G_CALLBACK (ide_shortcut_controller_widget_hierarchy_changed),
+                              self);
+
+  ide_shortcut_controller_widget_hierarchy_changed (self, priv->widget);
+}
+
+static void
+ide_shortcut_controller_set_widget (IdeShortcutController *self,
+                                    GtkWidget             *widget)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  if (widget != priv->widget)
+    {
+      if (priv->widget != NULL)
+        {
+          ide_shortcut_controller_disconnect (self);
+          g_object_remove_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget);
+          priv->widget = NULL;
+        }
+
+      if (widget != NULL && widget != priv->widget)
+        {
+          priv->widget = widget;
+          g_object_add_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget);
+          ide_shortcut_controller_connect (self);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WIDGET]);
+    }
+}
+
+static void
+ide_shortcut_controller_emit_reset (IdeShortcutController *self)
+{
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+
+  g_signal_emit (self, signals[RESET], 0);
+}
+
+void
+ide_shortcut_controller_set_context (IdeShortcutController *self,
+                                     IdeShortcutContext    *context)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_return_if_fail (!context || IDE_IS_SHORTCUT_CONTEXT (context));
+
+  if (g_set_object (&priv->context, context))
+    {
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CONTEXT]);
+      ide_shortcut_controller_emit_reset (self);
+    }
+}
+
+static void
+ide_shortcut_controller_real_set_context_named (IdeShortcutController *self,
+                                                const gchar           *name)
+{
+  g_autoptr(IdeShortcutContext) context = NULL;
+  IdeShortcutManager *manager;
+  IdeShortcutTheme *theme;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_return_if_fail (name != NULL);
+
+  manager = ide_shortcut_manager_get_default ();
+  theme = ide_shortcut_manager_get_theme (manager);
+  context = ide_shortcut_theme_find_context_by_name (theme, name);
+
+  ide_shortcut_controller_set_context (self, context);
+}
+
+static void
+ide_shortcut_controller_finalize (GObject *object)
+{
+  IdeShortcutController *self = (IdeShortcutController *)object;
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  if (priv->widget != NULL)
+    {
+      g_object_remove_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget);
+      priv->widget = NULL;
+    }
+
+  g_clear_object (&priv->context);
+  g_clear_object (&priv->root);
+
+  while (priv->descendants.length > 0)
+    g_queue_unlink (&priv->descendants, priv->descendants.head);
+
+  G_OBJECT_CLASS (ide_shortcut_controller_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_controller_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeShortcutController *self = (IdeShortcutController *)object;
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, priv->context);
+      break;
+
+    case PROP_WIDGET:
+      g_value_set_object (value, priv->widget);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_controller_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  IdeShortcutController *self = (IdeShortcutController *)object;
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      ide_shortcut_controller_set_context (self, g_value_get_object (value));
+      break;
+
+    case PROP_WIDGET:
+      ide_shortcut_controller_set_widget (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_controller_class_init (IdeShortcutControllerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_shortcut_controller_finalize;
+  object_class->get_property = ide_shortcut_controller_get_property;
+  object_class->set_property = ide_shortcut_controller_set_property;
+
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The current context of the controller",
+                         IDE_TYPE_SHORTCUT_CONTEXT,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_WIDGET] =
+    g_param_spec_object ("widget",
+                         "Widget",
+                         "The widget for which the controller attached",
+                         GTK_TYPE_WIDGET,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeShortcutController::reset:
+   *
+   * This signal is emitted when the shortcut controller is requesting
+   * the widget to reset any state it may have regarding the shortcut
+   * controller. Such an example might be a modal system that lives
+   * outside the controller whose state should be cleared in response
+   * to the controller changing modes.
+   */
+  signals [RESET] =
+    g_signal_new_class_handler ("reset",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                NULL, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  /**
+   * IdeShortcutController::set-context-named:
+   * @self: An #IdeShortcutController
+   * @name: The name of the context
+   *
+   * This changes the current context on the #IdeShortcutController to be the
+   * context matching @name. This is found by looking up the context by name
+   * in the active #IdeShortcutTheme.
+   */
+  signals [SET_CONTEXT_NAMED] =
+    g_signal_new_class_handler ("set-context",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (ide_shortcut_controller_real_set_context_named),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE, 1, G_TYPE_STRING);
+
+  controller_quark = g_quark_from_static_string ("IDE_SHORTCUT_CONTROLLER");
+  root_quark = g_quark_from_static_string ("IDE_SHORTCUT_CONTROLLER_ROOT");
+}
+
+static void
+ide_shortcut_controller_init (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_queue_init (&priv->descendants);
+
+  priv->descendants_link.data = self;
+}
+
+IdeShortcutController *
+ide_shortcut_controller_new (GtkWidget *widget)
+{
+  IdeShortcutController *ret;
+
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  if (NULL != (ret = g_object_get_qdata (G_OBJECT (widget), controller_quark)))
+    return g_object_ref (ret);
+
+  ret = g_object_new (IDE_TYPE_SHORTCUT_CONTROLLER,
+                      "widget", widget,
+                      NULL);
+
+  g_object_set_qdata_full (G_OBJECT (widget),
+                           controller_quark,
+                           g_object_ref (ret),
+                           g_object_unref);
+
+  return ret;
+}
+
+/**
+ * ide_shortcut_controller_find:
+ *
+ * Finds the registered #IdeShortcutController for a widget.
+ *
+ * Returns: (nullable) (transfer none): An #IdeShortcutController or %NULL.
+ */
+IdeShortcutController *
+ide_shortcut_controller_find (GtkWidget *widget)
+{
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  return g_object_get_qdata (G_OBJECT (widget), controller_quark);
+}
+
+/**
+ * ide_shortcut_controller_get_context:
+ * @self: An #IdeShortcutController
+ *
+ * This function gets the #IdeShortcutController:context property, which
+ * is the current context to dispatch events to. An #IdeShortcutContext
+ * is a group of keybindings that may be activated in response to a
+ * single or series of #GdkEventKey.
+ *
+ * Returns: (transfer none) (nullable): An #IdeShortcutContext or %NULL.
+ */
+IdeShortcutContext *
+ide_shortcut_controller_get_context (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self), NULL);
+
+  if (priv->widget == NULL)
+    return NULL;
+
+  if (priv->context == NULL)
+    {
+      IdeShortcutManager *manager;
+      IdeShortcutTheme *theme;
+
+      manager = ide_shortcut_manager_get_default ();
+      theme = ide_shortcut_manager_get_theme (manager);
+
+      priv->context = ide_shortcut_theme_find_default_context (theme, priv->widget);
+    }
+
+  return priv->context;
+}
+
+/**
+ * ide_shortcut_controller_handle_event:
+ * @self: An #IdeShortcutController
+ * @event: A #GdkEventKey
+ *
+ * This function uses @event to determine if the current context has a shortcut
+ * registered matching the event. If so, the shortcut will be dispatched and
+ * %TRUE is returned.
+ *
+ * Otherwise, %FALSE is returned.
+ *
+ * Returns: %TRUE if @event has been handled, otherwise %FALSE.
+ */
+gboolean
+ide_shortcut_controller_handle_event (IdeShortcutController *self,
+                                      const GdkEventKey     *event)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  IdeShortcutContext *context;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self), FALSE);
+  g_return_val_if_fail (event != NULL, FALSE);
+
+  if (priv->widget == NULL ||
+      !gtk_widget_get_visible (priv->widget) ||
+      !gtk_widget_is_sensitive (priv->widget))
+    return FALSE;
+
+  context = ide_shortcut_controller_get_context (self);
+
+  if (context != NULL)
+    {
+      if (ide_shortcut_context_activate (context, priv->widget, event))
+        return GDK_EVENT_STOP;
+    }
+
+  for (GList *iter = priv->descendants.head; iter != NULL; iter = iter->next)
+    {
+      IdeShortcutController *descendant = iter->data;
+
+      if (ide_shortcut_controller_handle_event (descendant, event))
+        return GDK_EVENT_STOP;
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
diff --git a/libide/shortcuts/ide-shortcut-controller.h b/libide/shortcuts/ide-shortcut-controller.h
new file mode 100644
index 0000000..68c6307
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-controller.h
@@ -0,0 +1,42 @@
+/* ide-shortcut-controller.h
+ *
+ * Copyright (C) 2016 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 3 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/>.
+ */
+
+#ifndef IDE_SHORTCUT_CONTROLLER_H
+#define IDE_SHORTCUT_CONTROLLER_H
+
+#include <gtk/gtk.h>
+
+#include "ide-shortcut-context.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_CONTROLLER (ide_shortcut_controller_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutController, ide_shortcut_controller, IDE, SHORTCUT_CONTROLLER, GObject)
+
+IdeShortcutController *ide_shortcut_controller_new          (GtkWidget             *widget);
+gboolean               ide_shortcut_controller_handle_event (IdeShortcutController *self,
+                                                             const GdkEventKey     *event);
+IdeShortcutController *ide_shortcut_controller_find         (GtkWidget             *widget);
+IdeShortcutContext    *ide_shortcut_controller_get_context  (IdeShortcutController *self);
+void                   ide_shortcut_controller_set_context  (IdeShortcutController *self,
+                                                             IdeShortcutContext    *context);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_CONTROLLER_H */
diff --git a/libide/shortcuts/ide-shortcut-manager.c b/libide/shortcuts/ide-shortcut-manager.c
new file mode 100644
index 0000000..1cf06d7
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-manager.c
@@ -0,0 +1,393 @@
+/* ide-shortcut-manager.c
+ *
+ * Copyright (C) 2016 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 3 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 "ide-shortcut-manager.h"
+
+#include "shortcuts/ide-shortcut-controller.h"
+#include "shortcuts/ide-shortcut-manager.h"
+
+typedef struct
+{
+  IdeShortcutTheme *theme;
+  GPtrArray        *themes;
+} IdeShortcutManagerPrivate;
+
+enum {
+  PROP_0,
+  PROP_THEME,
+  PROP_THEME_NAME,
+  N_PROPS
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+struct _IdeShortcutManager { GObject object; };
+G_DEFINE_TYPE_WITH_CODE (IdeShortcutManager, ide_shortcut_manager, G_TYPE_OBJECT,
+                         G_ADD_PRIVATE (IdeShortcutManager)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_shortcut_manager_finalize (GObject *object)
+{
+  IdeShortcutManager *self = (IdeShortcutManager *)object;
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_clear_pointer (&priv->themes, g_ptr_array_unref);
+  g_clear_object (&priv->theme);
+
+  G_OBJECT_CLASS (ide_shortcut_manager_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_manager_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  IdeShortcutManager *self = (IdeShortcutManager *)object;
+
+  switch (prop_id)
+    {
+    case PROP_THEME:
+      g_value_set_object (value, ide_shortcut_manager_get_theme (self));
+      break;
+
+    case PROP_THEME_NAME:
+      g_value_set_string (value, ide_shortcut_manager_get_theme_name (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_manager_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  IdeShortcutManager *self = (IdeShortcutManager *)object;
+
+  switch (prop_id)
+    {
+    case PROP_THEME:
+      ide_shortcut_manager_set_theme (self, g_value_get_object (value));
+      break;
+
+    case PROP_THEME_NAME:
+      ide_shortcut_manager_set_theme_name (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_manager_class_init (IdeShortcutManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_shortcut_manager_finalize;
+  object_class->get_property = ide_shortcut_manager_get_property;
+  object_class->set_property = ide_shortcut_manager_set_property;
+
+  properties [PROP_THEME] =
+    g_param_spec_object ("theme",
+                         "Theme",
+                         "The current key theme.",
+                         IDE_TYPE_SHORTCUT_THEME,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_THEME_NAME] =
+    g_param_spec_string ("theme-name",
+                         "Theme Name",
+                         "The name of the current theme",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+  
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_shortcut_manager_init (IdeShortcutManager *self)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  priv->themes = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+/**
+ * ide_shortcut_manager_get_default:
+ *
+ * Gets the singleton #IdeShortcutManager for the process.
+ *
+ * Returns: (transfer none) (not nullable): An #IdeShortcutManager.
+ */
+IdeShortcutManager *
+ide_shortcut_manager_get_default (void)
+{
+  static IdeShortcutManager *instance;
+
+  if (instance == NULL)
+    {
+      instance = g_object_new (IDE_TYPE_SHORTCUT_MANAGER, NULL);
+      g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance);
+    }
+
+  return instance;
+}
+
+/**
+ * ide_shortcut_manager_get_theme:
+ *
+ * Gets the "theme" property.
+ *
+ * Returns: (transfer none) (not nullable): An #IdeShortcutTheme.
+ */
+IdeShortcutTheme *
+ide_shortcut_manager_get_theme (IdeShortcutManager *self)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), NULL);
+
+  if (priv->theme == NULL)
+    priv->theme = g_object_new (IDE_TYPE_SHORTCUT_THEME,
+                                "name", "default",
+                                NULL);
+
+  return priv->theme;
+}
+
+/**
+ * ide_shortcut_manager_set_theme:
+ * @self: An #IdeShortcutManager
+ * @theme: (not nullable): An #IdeShortcutTheme
+ *
+ * Sets the theme for the shortcut manager.
+ */
+void
+ide_shortcut_manager_set_theme (IdeShortcutManager *self,
+                                IdeShortcutTheme   *theme)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SHORTCUT_MANAGER (self));
+  g_return_if_fail (IDE_IS_SHORTCUT_THEME (theme));
+
+  /*
+   * It is important that IdeShortcutController instances watch for
+   * notify::theme so that they can reset their state. Otherwise, we
+   * could be transitioning between incorrect contexts.
+   */
+
+  if (g_set_object (&priv->theme, theme))
+    {
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME_NAME]);
+    }
+}
+
+/**
+ * ide_shortcut_manager_handle_event:
+ * @self: (nullable): An #IdeShortcutManager
+ * @toplevel: A #GtkWidget or %NULL.
+ * @event: A #GdkEventKey event to handle.
+ *
+ * This function will try to dispatch @event to the proper widget and
+ * #IdeShortcutContext. If the event is handled, then %TRUE is returned.
+ *
+ * You should call this from #GtkWidget::key-press-event handler in your
+ * #GtkWindow toplevel.
+ *
+ * Returns: %TRUE if the event was handled.
+ */
+gboolean
+ide_shortcut_manager_handle_event (IdeShortcutManager *self,
+                                   const GdkEventKey  *event,
+                                   GtkWidget          *toplevel)
+{
+  GtkWidget *widget;
+  GtkWidget *focus;
+  GdkModifierType modifier;
+
+  if (self == NULL)
+    self = ide_shortcut_manager_get_default ();
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), FALSE);
+  g_return_val_if_fail (GTK_IS_WINDOW (toplevel), FALSE);
+  g_return_val_if_fail (event != NULL, FALSE);
+
+  if (event->type != GDK_KEY_PRESS)
+    return GDK_EVENT_PROPAGATE;
+
+  modifier = event->state & gtk_accelerator_get_default_mod_mask ();
+  widget = focus = gtk_window_get_focus (GTK_WINDOW (toplevel));
+
+  while (widget != NULL)
+    {
+      IdeShortcutController *controller;
+      gboolean use_binding_sets = TRUE;
+
+      if (NULL != (controller = ide_shortcut_controller_find (widget)))
+        {
+          IdeShortcutContext *context = ide_shortcut_controller_get_context (controller);
+
+          /*
+           * Fetch this property first as the controller context could change
+           * during activation of the handle_event().
+           */
+          if (context != NULL)
+            g_object_get (context,
+                          "use-binding-sets", &use_binding_sets,
+                          NULL);
+
+          /*
+           * Now try to activate the event using the controller.
+           */
+          if (ide_shortcut_controller_handle_event (controller, event))
+            return GDK_EVENT_STOP;
+        }
+
+      /*
+       * If the current context at activation indicates that we can
+       * dispatch using the default binding sets for the widget, go
+       * ahead and try to do that.
+       */
+      if (use_binding_sets)
+        {
+          GtkStyleContext *style_context;
+          g_autoptr(GPtrArray) sets = NULL;
+
+          style_context = gtk_widget_get_style_context (widget);
+          gtk_style_context_get (style_context,
+                                 gtk_style_context_get_state (style_context),
+                                 "-gtk-key-bindings", &sets,
+                                 NULL);
+
+          if (sets != NULL)
+            {
+              for (guint i = 0; i < sets->len; i++)
+                {
+                  GtkBindingSet *set = g_ptr_array_index (sets, i);
+
+                  if (gtk_binding_set_activate (set, event->keyval, modifier, G_OBJECT (widget)))
+                    return GDK_EVENT_STOP;
+                }
+            }
+
+          /*
+           * Only if this widget is also our focus, try to activate the default
+           * keybindings for the widget.
+           */
+          if (widget == focus)
+            {
+              GtkBindingSet *set = gtk_binding_set_by_class (G_OBJECT_GET_CLASS (widget));
+
+              if (gtk_binding_set_activate (set, event->keyval, modifier, G_OBJECT (widget)))
+                return GDK_EVENT_STOP;
+            }
+        }
+
+      widget = gtk_widget_get_parent (widget);
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+const gchar *
+ide_shortcut_manager_get_theme_name (IdeShortcutManager *self)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+  const gchar *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), NULL);
+
+  if (priv->theme != NULL)
+    ret = ide_shortcut_theme_get_name (priv->theme);
+
+  return ret;
+}
+
+void
+ide_shortcut_manager_set_theme_name (IdeShortcutManager *self,
+                                     const gchar        *name)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SHORTCUT_MANAGER (self));
+
+  if (name == NULL)
+    name = "default";
+
+  for (guint i = 0; i < priv->themes->len; i++)
+    {
+      IdeShortcutTheme *theme = g_ptr_array_index (priv->themes, i);
+      const gchar *theme_name = ide_shortcut_theme_get_name (theme);
+
+      if (g_strcmp0 (name, theme_name) == 0)
+        {
+          ide_shortcut_manager_set_theme (self, theme);
+          return;
+        }
+    }
+
+  g_warning ("No such shortcut theme ā€œ%sā€", name);
+}
+
+static guint
+ide_shortcut_manager_get_n_items (GListModel *model)
+{
+  IdeShortcutManager *self = (IdeShortcutManager *)model;
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), 0);
+
+  return priv->themes->len;
+}
+
+static GType
+ide_shortcut_manager_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_SHORTCUT_THEME;
+}
+
+static gpointer
+ide_shortcut_manager_get_item (GListModel *model,
+                               guint       position)
+{
+  IdeShortcutManager *self = (IdeShortcutManager *)model;
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), NULL);
+  g_return_val_if_fail (position < priv->themes->len, NULL);
+
+  return g_object_ref (g_ptr_array_index (priv->themes, position));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_n_items = ide_shortcut_manager_get_n_items;
+  iface->get_item_type = ide_shortcut_manager_get_item_type;
+  iface->get_item = ide_shortcut_manager_get_item;
+}
diff --git a/libide/shortcuts/ide-shortcut-manager.h b/libide/shortcuts/ide-shortcut-manager.h
new file mode 100644
index 0000000..97dd2d5
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-manager.h
@@ -0,0 +1,45 @@
+/* ide-shortcut-manager.c
+ *
+ * Copyright (C) 2016 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 3 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/>.
+ */
+
+#ifndef IDE_SHORTCUT_MANAGER_H
+#define IDE_SHORTCUT_MANAGER_H
+
+#include <gtk/gtk.h>
+
+#include "ide-shortcut-theme.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_MANAGER (ide_shortcut_manager_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutManager, ide_shortcut_manager, IDE, SHORTCUT_MANAGER, GObject)
+
+IdeShortcutManager *ide_shortcut_manager_get_default    (void);
+IdeShortcutTheme   *ide_shortcut_manager_get_theme      (IdeShortcutManager *self);
+void                ide_shortcut_manager_set_theme      (IdeShortcutManager *self,
+                                                         IdeShortcutTheme   *theme);
+const gchar        *ide_shortcut_manager_get_theme_name (IdeShortcutManager *self);
+void                ide_shortcut_manager_set_theme_name (IdeShortcutManager *self,
+                                                         const gchar        *theme_name);
+gboolean            ide_shortcut_manager_handle_event   (IdeShortcutManager *self,
+                                                         const GdkEventKey  *event,
+                                                         GtkWidget          *toplevel);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_MANAGER_H */
diff --git a/libide/shortcuts/ide-shortcut-theme.c b/libide/shortcuts/ide-shortcut-theme.c
new file mode 100644
index 0000000..20c9697
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-theme.c
@@ -0,0 +1,187 @@
+/* ide-shortcut-theme.c
+ *
+ * Copyright (C) 2016 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 3 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 "ide-shortcut-theme"
+
+#include "ide-shortcut-theme.h"
+
+typedef struct
+{
+  gchar      *name;
+  GHashTable *contexts;
+} IdeShortcutThemePrivate;
+
+enum {
+  PROP_0,
+  PROP_NAME,
+  N_PROPS
+};
+
+struct _IdeShortcutTheme { GObject object; };
+G_DEFINE_TYPE_WITH_PRIVATE (IdeShortcutTheme, ide_shortcut_theme, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_shortcut_theme_finalize (GObject *object)
+{
+  IdeShortcutTheme *self = (IdeShortcutTheme *)object;
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  g_clear_pointer (&priv->name, g_free);
+  g_clear_pointer (&priv->contexts, g_hash_table_unref);
+
+  G_OBJECT_CLASS (ide_shortcut_theme_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_theme_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeShortcutTheme *self = (IdeShortcutTheme *)object;
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      g_value_set_string (value, ide_shortcut_theme_get_name (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_theme_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeShortcutTheme *self = (IdeShortcutTheme *)object;
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      priv->name = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_theme_class_init (IdeShortcutThemeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_shortcut_theme_finalize;
+  object_class->get_property = ide_shortcut_theme_get_property;
+  object_class->set_property = ide_shortcut_theme_set_property;
+
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "The name of the theme",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+  
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_shortcut_theme_init (IdeShortcutTheme *self)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  priv->contexts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+}
+
+const gchar *
+ide_shortcut_theme_get_name (IdeShortcutTheme *self)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+
+  return priv->name;
+}
+
+/**
+ * ide_shortcut_theme_find_context_by_name:
+ * @self: An #IdeShortcutContext
+ * @name: The name of the context
+ *
+ * Gets the context named @name. If the context does not exist, it will
+ * be created.
+ *
+ * Returns: (not nullable) (transfer full): An #IdeShortcutContext
+ */
+IdeShortcutContext *
+ide_shortcut_theme_find_context_by_name (IdeShortcutTheme *self,
+                                         const gchar      *name)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+  IdeShortcutContext *ret;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+  g_return_val_if_fail (name != NULL, NULL);
+
+  if (NULL == (ret = g_hash_table_lookup (priv->contexts, name)))
+    {
+      ret = ide_shortcut_context_new (name);
+      g_hash_table_insert (priv->contexts, g_strdup (name), ret);
+    }
+
+  return g_object_ref (ret);
+}
+
+static IdeShortcutContext *
+ide_shortcut_theme_find_default_context_by_type (IdeShortcutTheme *self,
+                                                 GType             type)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+  g_autofree gchar *name = NULL;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+  g_return_val_if_fail (g_type_is_a (type, GTK_TYPE_WIDGET), NULL);
+
+  name = g_strdup_printf ("%s::%s::default", priv->name, g_type_name (type));
+
+  return ide_shortcut_theme_find_context_by_name (self, name);
+}
+
+/**
+ * ide_shortcut_theme_find_default_context:
+ *
+ * Finds the default context in the theme for @widget.
+ *
+ * Returns: (nullable) (transfer full): An #IdeShortcutContext or %NULL.
+ */
+IdeShortcutContext *
+ide_shortcut_theme_find_default_context (IdeShortcutTheme *self,
+                                         GtkWidget        *widget)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  return ide_shortcut_theme_find_default_context_by_type (self, G_OBJECT_TYPE (widget));
+}
diff --git a/libide/shortcuts/ide-shortcut-theme.h b/libide/shortcuts/ide-shortcut-theme.h
new file mode 100644
index 0000000..d002c49
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-theme.h
@@ -0,0 +1,40 @@
+/* ide-shortcut-theme.h
+ *
+ * Copyright (C) 2016 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 3 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/>.
+ */
+
+#ifndef IDE_SHORTCUT_THEME_H
+#define IDE_SHORTCUT_THEME_H
+
+#include <gtk/gtk.h>
+
+#include "ide-shortcut-context.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_THEME (ide_shortcut_theme_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutTheme, ide_shortcut_theme, IDE, SHORTCUT_THEME, GObject)
+
+const gchar        *ide_shortcut_theme_get_name             (IdeShortcutTheme *self);
+IdeShortcutContext *ide_shortcut_theme_find_default_context (IdeShortcutTheme *self,
+                                                             GtkWidget        *widget);
+IdeShortcutContext *ide_shortcut_theme_find_context_by_name (IdeShortcutTheme *self,
+                                                             const gchar      *name);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_THEME_H */
diff --git a/libide/workbench/ide-workbench.c b/libide/workbench/ide-workbench.c
index 44923ac..9193d96 100644
--- a/libide/workbench/ide-workbench.c
+++ b/libide/workbench/ide-workbench.c
@@ -28,6 +28,7 @@
 #include "editor/ide-editor-perspective.h"
 #include "greeter/ide-greeter-perspective.h"
 #include "preferences/ide-preferences-perspective.h"
+#include "shortcuts/ide-shortcut-manager.h"
 #include "util/ide-gtk.h"
 #include "util/ide-window-settings.h"
 #include "workbench/ide-layout-pane.h"
@@ -191,6 +192,19 @@ ide_workbench_delete_event (GtkWidget   *widget,
   return GDK_EVENT_PROPAGATE;
 }
 
+static gboolean
+ide_workbench_key_press_event (GtkWidget   *widget,
+                               GdkEventKey *event)
+{
+  g_assert (IDE_IS_WORKBENCH (widget));
+  g_assert (event != NULL);
+
+  if (ide_shortcut_manager_handle_event (NULL, event, widget))
+    return GDK_EVENT_STOP;
+
+  return GTK_WIDGET_CLASS (ide_workbench_parent_class)->key_press_event (widget, event);
+}
+
 static void
 ide_workbench_constructed (GObject *object)
 {
@@ -300,6 +314,7 @@ ide_workbench_class_init (IdeWorkbenchClass *klass)
   object_class->set_property = ide_workbench_set_property;
 
   widget_class->delete_event = ide_workbench_delete_event;
+  widget_class->key_press_event = ide_workbench_key_press_event;
 
   /**
    * IdeWorkbench:context:


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]