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



commit a6e93573e3f36d24257e8cf659cb5cc46747b7fb
Author: Christian Hergert <chergert redhat com>
Date:   Sun Dec 4 14:12:53 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     |  667 ++++++++++++++++++++++
 libide/shortcuts/ide-shortcut-controller.h     |   51 ++
 libide/shortcuts/ide-shortcut-manager.c        |  450 +++++++++++++++
 libide/shortcuts/ide-shortcut-manager.h        |   49 ++
 libide/shortcuts/ide-shortcut-theme.c          |  248 ++++++++
 libide/shortcuts/ide-shortcut-theme.h          |   57 ++
 libide/workbench/ide-workbench.c               |   17 +-
 plugins/devhelp/gbp-devhelp-panel.c            |   34 ++
 plugins/devhelp/gbp-devhelp-workbench-addin.c  |   27 -
 plugins/terminal/gb-terminal-workbench-addin.c |   19 +-
 14 files changed, 2378 insertions(+), 39 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..7f26d26
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-controller.c
@@ -0,0 +1,667 @@
+/* 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 "ide-debug.h"
+
+#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 the IdeShortcutContext used for commands attached to the controller.
+   * Commands are operations which the user can override and will be activated
+   * after the current @context.
+   */
+  IdeShortcutContext *command_context;
+
+  /*
+   * This is an array of Command elements which are used to build the shortcuts
+   * window and list of commands that the user can override.
+   */
+  GArray *commands;
+
+  /*
+   * 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;
+
+typedef struct
+{
+  const gchar *id;
+  const gchar *group;
+  const gchar *title;
+  const gchar *subtitle;
+} Command;
+
+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             *previous_toplevel,
+                                                  GtkWidget             *widget)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (!previous_toplevel || GTK_IS_WIDGET (previous_toplevel));
+  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, NULL, 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_pointer (&priv->commands, g_array_unref);
+
+  g_clear_object (&priv->command_context);
+  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: (not nullable) (transfer none): An #IdeShortcutController or %NULL.
+ */
+IdeShortcutController *
+ide_shortcut_controller_find (GtkWidget *widget)
+{
+  IdeShortcutController *controller;
+
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  controller = g_object_get_qdata (G_OBJECT (widget), controller_quark);
+
+  if (controller == NULL)
+    {
+      /* We want to pass a borrowed reference */
+      g_object_unref (ide_shortcut_controller_new (widget));
+      controller = g_object_get_qdata (G_OBJECT (widget), controller_quark);
+    }
+
+  return controller;
+}
+
+/**
+ * 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;
+
+  IDE_ENTRY;
+
+  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))
+    IDE_RETURN (GDK_EVENT_STOP);
+
+  context = ide_shortcut_controller_get_context (self);
+
+  if (context != NULL)
+    {
+      if (ide_shortcut_context_activate (context, priv->widget, event))
+        IDE_RETURN (GDK_EVENT_STOP);
+    }
+
+  if (priv->command_context != NULL)
+    {
+      if (ide_shortcut_context_activate (priv->command_context, priv->widget, event))
+        IDE_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))
+        IDE_RETURN (GDK_EVENT_STOP);
+    }
+
+  IDE_RETURN (GDK_EVENT_PROPAGATE);
+}
+
+static IdeShortcutContext *
+ide_shortcut_controller_get_command_context (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+
+  if (priv->command_context == NULL)
+    priv->command_context = g_object_new (IDE_TYPE_SHORTCUT_CONTEXT,
+                                          "use-binding-set", FALSE,
+                                          NULL);
+
+  return priv->command_context;
+}
+
+/**
+ * ide_shortcut_controller_add_command_signal: (skip)
+ * @self: An #IdeShortcutController
+ * @command_id: the command-id such as "org.gnome.builder.plugins.foo.bar"
+ * @default_accel: the accelerator for the default key theme
+ * @group: the group to place the shortcut within the shortcuts overview
+ * @title: the title for the shortcut
+ * @subtitle: (nullable): an optional subtitle for the command
+ * @signal_name: the name of the signal to activate on the controllers widget
+ * @n_args: the number of argument pairs
+ *
+ * This adds a command to the controller which will activate the signal @signal_name
+ * on the attached #GtkWidget. Use @n_args followed by pairs of (#GType, value) to
+ * specify the arguments for the signal. This is similar to
+ * gtk_binding_entry_add_signal().
+ *
+ * By registering a command on a controller directly, the shortcuts overview can
+ * display the shortcut in the shortcuts window as well as allow the user to
+ * override the accelerator. Where as the user cannot override operations found
+ * directly in #IdeShortcutContext's as provided by themes.
+ */
+void
+ide_shortcut_controller_add_command_signal (IdeShortcutController *self,
+                                            const gchar           *command_id,
+                                            const gchar           *default_accel,
+                                            const gchar           *group,
+                                            const gchar           *title,
+                                            const gchar           *subtitle,
+                                            const gchar           *signal_name,
+                                            guint                  n_args,
+                                            ...)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  IdeShortcutContext *command_context;
+  Command command = { 0 };
+  va_list args;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_return_if_fail (command_id != NULL);
+  g_return_if_fail (group != NULL);
+  g_return_if_fail (title != NULL);
+  g_return_if_fail (signal_name != NULL);
+
+  command.id = g_intern_string (command_id);
+  command.group = g_intern_string (group);
+  command.title = g_intern_string (title);
+  command.subtitle = g_intern_string (subtitle);
+
+  if (priv->commands == NULL)
+    priv->commands = g_array_new (FALSE, FALSE, sizeof (Command));
+
+  g_array_append_val (priv->commands, command);
+
+  command_context = ide_shortcut_controller_get_command_context (self);
+
+  va_start (args, n_args);
+  ide_shortcut_context_add_signal_va_list (command_context,
+                                           default_accel,
+                                           signal_name,
+                                           n_args,
+                                           args);
+  va_end (args);
+}
diff --git a/libide/shortcuts/ide-shortcut-controller.h b/libide/shortcuts/ide-shortcut-controller.h
new file mode 100644
index 0000000..b99518f
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-controller.h
@@ -0,0 +1,51 @@
+/* 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);
+void                   ide_shortcut_controller_add_command_signal (IdeShortcutController *self,
+                                                                   const gchar           *command_id,
+                                                                   const gchar           *default_accel,
+                                                                   const gchar           *group,
+                                                                   const gchar           *title,
+                                                                   const gchar           *subtitle,
+                                                                   const gchar           *signal_name,
+                                                                   guint                  n_args,
+                                                                   ...);
+
+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..e255599
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-manager.c
@@ -0,0 +1,450 @@
+/* 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 "ide-debug.h"
+
+#include "shortcuts/ide-shortcut-controller.h"
+#include "shortcuts/ide-shortcut-manager.h"
+
+struct _IdeShortcutManager
+{
+  GObject object;
+};
+
+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);
+
+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;
+
+  IDE_ENTRY;
+
+  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)
+    IDE_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)
+    {
+      g_autoptr(GtkWidget) widget_hold = g_object_ref (widget);
+      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))
+            IDE_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)))
+                    IDE_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)))
+                IDE_RETURN (GDK_EVENT_STOP);
+            }
+        }
+
+      widget = gtk_widget_get_parent (widget);
+    }
+
+  IDE_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;
+}
+
+void
+ide_shortcut_manager_add_theme (IdeShortcutManager *self,
+                                IdeShortcutTheme   *theme)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+  guint position;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_MANAGER (self));
+  g_return_if_fail (IDE_IS_SHORTCUT_THEME (theme));
+
+  for (guint i = 0; i < priv->themes->len; i++)
+    {
+      if (g_ptr_array_index (priv->themes, i) == theme)
+        {
+          g_warning ("%s named %s has already been added",
+                     G_OBJECT_TYPE_NAME (theme),
+                     ide_shortcut_theme_get_name (theme));
+          return;
+        }
+    }
+
+  position = priv->themes->len;
+
+  g_ptr_array_add (priv->themes, g_object_ref (theme));
+
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+}
+
+void
+ide_shortcut_manager_remove_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));
+
+  for (guint i = 0; i < priv->themes->len; i++)
+    {
+      if (g_ptr_array_index (priv->themes, i) == theme)
+        {
+          g_ptr_array_remove_index (priv->themes, i);
+          g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+          break;
+        }
+    }
+}
diff --git a/libide/shortcuts/ide-shortcut-manager.h b/libide/shortcuts/ide-shortcut-manager.h
new file mode 100644
index 0000000..6f95a63
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-manager.h
@@ -0,0 +1,49 @@
+/* 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);
+void                ide_shortcut_manager_add_theme      (IdeShortcutManager *self,
+                                                         IdeShortcutTheme   *theme);
+void                ide_shortcut_manager_remove_theme   (IdeShortcutManager *self,
+                                                         IdeShortcutTheme   *theme);
+
+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..86c9b8e
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-theme.c
@@ -0,0 +1,248 @@
+/* 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 "shortcuts/ide-shortcut-theme.h"
+
+typedef struct
+{
+  gchar      *name;
+  gchar      *title;
+  gchar      *subtitle;
+  GHashTable *contexts;
+} IdeShortcutThemePrivate;
+
+enum {
+  PROP_0,
+  PROP_NAME,
+  PROP_SUBTITLE,
+  PROP_TITLE,
+  N_PROPS
+};
+
+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->title, g_free);
+  g_clear_pointer (&priv->subtitle, 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;
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      g_value_set_string (value, ide_shortcut_theme_get_name (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, priv->title);
+      break;
+
+    case PROP_SUBTITLE:
+      g_value_set_string (value, priv->subtitle);
+      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;
+
+    case PROP_TITLE:
+      priv->title = g_value_dup_string (value);
+      break;
+
+    case PROP_SUBTITLE:
+      priv->subtitle = 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));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the theme as used for UI elements",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SUBTITLE] =
+    g_param_spec_string ("subtitle",
+                         "Subtitle",
+                         "The subtitle of the theme as used for UI elements",
+                         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));
+}
+
+void
+ide_shortcut_theme_add_context (IdeShortcutTheme   *self,
+                                IdeShortcutContext *context)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+  const gchar *name;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_THEME (self));
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTEXT (context));
+
+  name = ide_shortcut_context_get_name (context);
+
+  g_return_if_fail (name != NULL);
+
+  g_hash_table_insert (priv->contexts, g_strdup (name), g_object_ref (context));
+}
+
+IdeShortcutTheme *
+ide_shortcut_theme_new (const gchar *name)
+{
+  return g_object_new (IDE_TYPE_SHORTCUT_THEME,
+                       "name", name,
+                       NULL);
+}
diff --git a/libide/shortcuts/ide-shortcut-theme.h b/libide/shortcuts/ide-shortcut-theme.h
new file mode 100644
index 0000000..13d7f14
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-theme.h
@@ -0,0 +1,57 @@
+/* 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_DERIVABLE_TYPE (IdeShortcutTheme, ide_shortcut_theme, IDE, SHORTCUT_THEME, GObject)
+
+struct _IdeShortcutThemeClass
+{
+  GObjectClass parent_class;
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+};
+
+IdeShortcutTheme   *ide_shortcut_theme_new                  (const gchar        *name);
+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);
+void                ide_shortcut_theme_add_context          (IdeShortcutTheme   *self,
+                                                             IdeShortcutContext *context);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_THEME_H */
diff --git a/libide/workbench/ide-workbench.c b/libide/workbench/ide-workbench.c
index 44923ac..3fee0f2 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:
@@ -952,7 +967,7 @@ void
 ide_workbench_focus (IdeWorkbench *self,
                      GtkWidget    *widget)
 {
-  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (!self || IDE_IS_WORKBENCH (self));
   g_return_if_fail (GTK_IS_WIDGET (widget));
 
   ide_workbench_show_parents (widget);
diff --git a/plugins/devhelp/gbp-devhelp-panel.c b/plugins/devhelp/gbp-devhelp-panel.c
index 7c33707..7c15420 100644
--- a/plugins/devhelp/gbp-devhelp-panel.c
+++ b/plugins/devhelp/gbp-devhelp-panel.c
@@ -41,7 +41,13 @@ enum {
   LAST_PROP
 };
 
+enum {
+  FOCUS_SEARCH,
+  N_SIGNALS
+};
+
 static GParamSpec *properties [LAST_PROP];
+static guint signals [N_SIGNALS];
 
 static void
 gbp_devhelp_panel_find_view (GtkWidget *widget,
@@ -186,12 +192,38 @@ gbp_devhelp_panel_class_init (GbpDevhelpPanelClass *klass)
                          (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  signals [FOCUS_SEARCH] =
+    g_signal_new_class_handler ("focus-search",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (gbp_devhelp_panel_focus_search),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE, 1, G_TYPE_STRING);
 }
 
 static void
 gbp_devhelp_panel_init (GbpDevhelpPanel *self)
 {
+  g_autoptr(IdeShortcutController) controller = NULL;
+
   g_object_set (self, "title", _("Documentation"), NULL);
+
+  controller = ide_shortcut_controller_new (GTK_WIDGET (self));
+
+  /*
+   * Register our commands which can be overriden easily by keythemes and by
+   * custom user overrides.
+   */
+  ide_shortcut_controller_add_command_signal (controller,
+                                              "org.gnome.builder.plugins.devhelp.focus-search",
+                                              "<primary><shift>f",
+                                              _("Documentation"),
+                                              _("Search documentation"),
+                                              NULL,
+                                              "focus-search",
+                                              1,
+                                              G_TYPE_STRING, NULL);
 }
 
 void
@@ -200,6 +232,8 @@ gbp_devhelp_panel_focus_search (GbpDevhelpPanel *self,
 {
   g_return_if_fail (GBP_IS_DEVHELP_PANEL (self));
 
+  ide_workbench_focus (NULL, GTK_WIDGET (self->sidebar));
+
   dh_sidebar_set_search_focus (self->sidebar);
 
   if (keyword)
diff --git a/plugins/devhelp/gbp-devhelp-workbench-addin.c b/plugins/devhelp/gbp-devhelp-workbench-addin.c
index f21ffc8..adde8bb 100644
--- a/plugins/devhelp/gbp-devhelp-workbench-addin.c
+++ b/plugins/devhelp/gbp-devhelp-workbench-addin.c
@@ -49,26 +49,12 @@ gbp_devhelp_workbench_addin_init (GbpDevhelpWorkbenchAddin *self)
 }
 
 static void
-focus_devhelp_search (GSimpleAction *action,
-                      GVariant      *param,
-                      gpointer       user_data)
-{
-  GbpDevhelpWorkbenchAddin *self = user_data;
-
-  g_assert (GBP_IS_DEVHELP_WORKBENCH_ADDIN (self));
-
-  gbp_devhelp_panel_focus_search (self->panel, NULL);
-}
-
-static void
 gbp_devhelp_workbench_addin_load (IdeWorkbenchAddin *addin,
                                   IdeWorkbench      *workbench)
 {
   GbpDevhelpWorkbenchAddin *self = (GbpDevhelpWorkbenchAddin *)addin;
   IdePerspective *perspective;
   GtkWidget *pane;
-  GSimpleAction *action;
-  const gchar *focus_accel[] = { "<control><shift>f", NULL };
 
   g_assert (IDE_IS_WORKBENCH_ADDIN (self));
   g_assert (IDE_IS_WORKBENCH (workbench));
@@ -88,13 +74,6 @@ gbp_devhelp_workbench_addin_load (IdeWorkbenchAddin *addin,
                               "visible", TRUE,
                               NULL);
   gtk_container_add (GTK_CONTAINER (pane), GTK_WIDGET (self->panel));
-
-  action = g_simple_action_new ("focus-devhelp-search", NULL);
-  g_signal_connect_object (action, "activate", G_CALLBACK (focus_devhelp_search), self, 0);
-  g_action_map_add_action (G_ACTION_MAP (workbench), G_ACTION (action));
-
-  gtk_application_set_accels_for_action (GTK_APPLICATION (IDE_APPLICATION_DEFAULT),
-                                         "win.focus-devhelp-search", focus_accel);
 }
 
 static void
@@ -104,7 +83,6 @@ gbp_devhelp_workbench_addin_unload (IdeWorkbenchAddin *addin,
   GbpDevhelpWorkbenchAddin *self = (GbpDevhelpWorkbenchAddin *)addin;
   IdePerspective *perspective;
   GtkWidget *pane;
-  const gchar *empty_accels[1] = { NULL };
 
   g_assert (IDE_IS_WORKBENCH_ADDIN (self));
   g_assert (IDE_IS_WORKBENCH (workbench));
@@ -119,11 +97,6 @@ gbp_devhelp_workbench_addin_unload (IdeWorkbenchAddin *addin,
 
   gtk_widget_destroy (GTK_WIDGET (self->panel));
   self->panel = NULL;
-
-  g_action_map_remove_action (G_ACTION_MAP (workbench), "focus-devhelp-search");
-
-  gtk_application_set_accels_for_action (GTK_APPLICATION (IDE_APPLICATION_DEFAULT),
-                                         "win.focus-devhelp-search", empty_accels);
 }
 
 static void
diff --git a/plugins/terminal/gb-terminal-workbench-addin.c b/plugins/terminal/gb-terminal-workbench-addin.c
index c970857..ebb5e38 100644
--- a/plugins/terminal/gb-terminal-workbench-addin.c
+++ b/plugins/terminal/gb-terminal-workbench-addin.c
@@ -50,10 +50,11 @@ G_DEFINE_TYPE_EXTENDED (GbTerminalWorkbenchAddin,
                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKBENCH_ADDIN, workbench_addin_iface_init))
 
 static void
-new_terminal_activate_cb (GSimpleAction            *action,
-                          GVariant                 *param,
-                          GbTerminalWorkbenchAddin *self)
+new_terminal_activate_cb (GSimpleAction *action,
+                          GVariant      *param,
+                          gpointer       user_data)
 {
+  GbTerminalWorkbenchAddin *self = user_data;
   GbTerminalView *view;
   IdePerspective *perspective;
 
@@ -159,7 +160,9 @@ gb_terminal_workbench_addin_load (IdeWorkbenchAddin *addin,
   GtkWidget *bottom_pane;
   IdeContext *context;
   IdeRunManager *run_manager;
-  g_autoptr(GSimpleAction) action = NULL;
+  static const GActionEntry actions[] = {
+    { "new-terminal", new_terminal_activate_cb },
+  };
 
   g_assert (GB_IS_TERMINAL_WORKBENCH_ADDIN (self));
   g_assert (IDE_IS_WORKBENCH (workbench));
@@ -168,13 +171,7 @@ gb_terminal_workbench_addin_load (IdeWorkbenchAddin *addin,
 
   ide_set_weak_pointer (&self->workbench, workbench);
 
-  action = g_simple_action_new ("new-terminal", NULL);
-  g_signal_connect_object (action,
-                           "activate",
-                           G_CALLBACK (new_terminal_activate_cb),
-                           self,
-                           0);
-  g_action_map_add_action (G_ACTION_MAP (workbench), G_ACTION (action));
+  g_action_map_add_action_entries (G_ACTION_MAP (workbench), actions, G_N_ELEMENTS (actions), self);
 
   if (self->panel_terminal == NULL)
     {



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