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



commit 64fd447b7f45aeb4b5d80d582309344109291ee3
Author: Christian Hergert <chergert redhat com>
Date:   Tue Nov 22 12:51:28 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.
    
    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).
    
     - IdeShortcutContext does not yet allow multi-signal chains like the
       GtkBindingSet system does. We need to add this, just a bit of work.
    
     - 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    |  596 ++++++++++++++++++++++++++++
 libide/shortcuts/ide-shortcut-context.h    |   51 +++
 libide/shortcuts/ide-shortcut-controller.c |  462 +++++++++++++++++++++
 libide/shortcuts/ide-shortcut-controller.h |   37 ++
 libide/shortcuts/ide-shortcut-manager.c    |  265 ++++++++++++
 libide/shortcuts/ide-shortcut-manager.h    |   42 ++
 libide/shortcuts/ide-shortcut-theme.c      |  187 +++++++++
 libide/shortcuts/ide-shortcut-theme.h      |   40 ++
 libide/workbench/ide-workbench.c           |   15 +
 11 files changed, 1707 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..18e1fa1
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-context.c
@@ -0,0 +1,596 @@
+/* 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-shortcut-context.h"
+#include "ide-shortcut-controller.h"
+
+typedef enum
+{
+  SHORTCUT_ACTION = 1,
+  SHORTCUT_SIGNAL,
+} ShortcutType;
+
+typedef struct
+{
+  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;
+  };
+} Shortcut;
+
+typedef struct
+{
+  gchar      *name;
+  GHashTable *keymap;
+} IdeShortcutContextPrivate;
+
+enum {
+  PROP_0,
+  PROP_NAME,
+  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)
+    {
+      if (shortcut->type == SHORTCUT_ACTION)
+        g_clear_pointer (&shortcut->action.param, g_variant_unref);
+      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)
+{
+  g_assert (shortcut != NULL);
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (event != NULL);
+
+  switch (shortcut->type)
+    {
+    case SHORTCUT_ACTION:
+      return shortcut_action_activate (shortcut, widget, event);
+
+    case SHORTCUT_SIGNAL:
+      return shortcut_signal_activate (shortcut, widget, event);
+
+    default:
+      g_assert_not_reached ();
+      return FALSE;
+    }
+}
+
+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;
+
+    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;
+
+    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));
+  
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_shortcut_context_init (IdeShortcutContext *self)
+{
+}
+
+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);
+
+  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);
+
+  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));
+
+  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);
+}
diff --git a/libide/shortcuts/ide-shortcut-context.h b/libide/shortcuts/ide-shortcut-context.h
new file mode 100644
index 0000000..737d61a
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-context.h
@@ -0,0 +1,51 @@
+/* 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);
+
+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..b6758ee
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-controller.c
@@ -0,0 +1,462 @@
+/* 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-shortcut-context.h"
+#include "ide-shortcut-controller.h"
+#include "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_WIDGET,
+  N_PROPS
+};
+
+enum {
+  SET_CONTEXT,
+  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_real_set_context (IdeShortcutController *self,
+                                          const gchar           *name)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  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);
+
+  g_set_object (&priv->context, 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_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_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_WIDGET] =
+    g_param_spec_object ("widget",
+                         "Widget",
+                         "The widget for the controller",
+                         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:set-context:
+   * @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] =
+    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),
+                                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);
+}
+
+static 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;
+}
+
+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..dc71823
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-controller.h
@@ -0,0 +1,37 @@
+/* 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>
+
+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);
+
+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..fee21ca
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-manager.c
@@ -0,0 +1,265 @@
+/* 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-shortcut-controller.h"
+#include "ide-shortcut-manager.h"
+
+typedef struct
+{
+  IdeShortcutTheme *theme;
+} IdeShortcutManagerPrivate;
+
+enum {
+  PROP_0,
+  PROP_THEME,
+  N_PROPS
+};
+
+struct _IdeShortcutManager { GObject object; };
+G_DEFINE_TYPE_WITH_PRIVATE (IdeShortcutManager, ide_shortcut_manager, G_TYPE_OBJECT)
+
+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_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;
+
+    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;
+
+    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));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_shortcut_manager_init (IdeShortcutManager *self)
+{
+}
+
+/**
+ * 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]);
+}
+
+/**
+ * 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)
+    {
+      g_autoptr(GPtrArray) sets = NULL;
+      IdeShortcutController *controller;
+      GtkStyleContext *style_context;
+
+      if (NULL != (controller = ide_shortcut_controller_find (widget)))
+        {
+          if (ide_shortcut_controller_handle_event (controller, event))
+            return GDK_EVENT_STOP;
+        }
+
+      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;
+            }
+        }
+
+      /*
+       * If this widget is also our focus, then try to activate the
+       * default keybindings for the widget.
+       *
+       * TODO: We should probably allow contexts to override this.
+       */
+      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;
+}
diff --git a/libide/shortcuts/ide-shortcut-manager.h b/libide/shortcuts/ide-shortcut-manager.h
new file mode 100644
index 0000000..4ec0194
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-manager.h
@@ -0,0 +1,42 @@
+/* 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);
+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]