[gnome-builder/wip/chergert/shortcuts: 2/2] shortcuts: start on new shortcuts engine
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder/wip/chergert/shortcuts: 2/2] shortcuts: start on new shortcuts engine
- Date: Fri, 25 Nov 2016 22:43:29 +0000 (UTC)
commit dbdc00de05a53dc3a769c1eb69a9f5f11c6a97af
Author: Christian Hergert <chergert redhat com>
Date: Fri Nov 25 14:43:12 2016 -0800
shortcuts: start on new shortcuts engine
This starts laying the groundwork for the basic plumbing. In general, we
have an IdeShortcutManager singleton for the process. This tracks the
current keytheme and provides an easy entry point for shortcut consuming
machinery.
Widgets gain the concept of an IdeShortcutController, who processes
incoming events and dispatches them to the current IdeShortcutContext
for the controller. By using the "set-context" signal on the controller,
widgets can change their context before processing the next signal
(however, the signal chain will continue from the original context).
The IdeWorkbench dispatches key-press-events to the shortcut manager who
returns TRUE if they were handled. This could happen because of the current
focus chain, or another widget in the hierarchy whose controller was
registered with the toplevel.
To chain multiple shortcut handlers together, simply call the _add_*()
variant on IdeShortcutContext with your additional operation. All
registered entries for that shortcut will be called in the order they were
added to the context. So to call an action, then a signal, do:
ide_shortcut_context_add_action(context, "<primary>c", ā¦)
ide_shortcut_context_add_signal(context, "<primary>c", ā¦)
Some things to think about still:
- We need the concept of a "keybinding slot" so that it is easy to
provide UI to override or change keybindings. That means, to some
degree, providing title/subtitle information when registering
accelerators. We also need to find a way to hook that up into the
shortcuts window (so we need section/group as well).
- We need code to integrate into the EggMenuManager and update the
accelerator information so that menus are displayed with proper
accel labels.
- We need a new IdeShortcutLabel that can be used in tooltips and other
places to show keybindings similar to the CSS I made for the
shortcuts window.
- Now that we will have acclerators registered at the toplevel, we can
theoretically do a "highlight mode" that shows accelerators and what
they affect in the UI. But doing this cleanly becomes a fun layout
challenge more than anything.
libide/Makefile.am | 8 +
libide/ide.h | 4 +
libide/shortcuts/ide-shortcut-context.c | 726 ++++++++++++++++++++++++++++
libide/shortcuts/ide-shortcut-context.h | 60 +++
libide/shortcuts/ide-shortcut-controller.c | 541 +++++++++++++++++++++
libide/shortcuts/ide-shortcut-controller.h | 42 ++
libide/shortcuts/ide-shortcut-manager.c | 441 +++++++++++++++++
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 | 15 +
11 files changed, 2191 insertions(+), 0 deletions(-)
---
diff --git a/libide/Makefile.am b/libide/Makefile.am
index 833a67e..ed6ce71 100644
--- a/libide/Makefile.am
+++ b/libide/Makefile.am
@@ -120,6 +120,10 @@ libide_1_0_la_public_headers = \
search/ide-search-provider.h \
search/ide-search-reducer.h \
search/ide-search-result.h \
+ shortcuts/ide-shortcut-context.h \
+ shortcuts/ide-shortcut-controller.h \
+ shortcuts/ide-shortcut-manager.h \
+ shortcuts/ide-shortcut-theme.h \
snippets/ide-source-snippet-chunk.h \
snippets/ide-source-snippet-context.h \
snippets/ide-source-snippet.h \
@@ -291,6 +295,10 @@ libide_1_0_la_public_sources = \
runtimes/ide-runtime.c \
scripting/ide-script-manager.c \
scripting/ide-script.c \
+ shortcuts/ide-shortcut-context.c \
+ shortcuts/ide-shortcut-controller.c \
+ shortcuts/ide-shortcut-manager.c \
+ shortcuts/ide-shortcut-theme.c \
search/ide-omni-search-display.c \
search/ide-omni-search-entry.c \
search/ide-omni-search-group.c \
diff --git a/libide/ide.h b/libide/ide.h
index fd397e6..05fed11 100644
--- a/libide/ide.h
+++ b/libide/ide.h
@@ -102,6 +102,10 @@ G_BEGIN_DECLS
#include "runtimes/ide-runtime.h"
#include "scripting/ide-script-manager.h"
#include "scripting/ide-script.h"
+#include "shortcuts/ide-shortcut-context.h"
+#include "shortcuts/ide-shortcut-controller.h"
+#include "shortcuts/ide-shortcut-manager.h"
+#include "shortcuts/ide-shortcut-theme.h"
#include "search/ide-omni-search-row.h"
#include "search/ide-pattern-spec.h"
#include "search/ide-search-context.h"
diff --git a/libide/shortcuts/ide-shortcut-context.c b/libide/shortcuts/ide-shortcut-context.c
new file mode 100644
index 0000000..e72d97e
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-context.c
@@ -0,0 +1,726 @@
+/* ide-shortcut-context.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-context"
+
+#include <gobject/gvaluecollector.h>
+#include <string.h>
+
+#include "ide-macros.h"
+
+#include "shortcuts/ide-shortcut-context.h"
+#include "shortcuts/ide-shortcut-controller.h"
+
+typedef enum
+{
+ SHORTCUT_ACTION = 1,
+ SHORTCUT_SIGNAL,
+} ShortcutType;
+
+typedef struct _Shortcut
+{
+ ShortcutType type;
+ GdkModifierType modifier;
+ guint keyval;
+ union {
+ struct {
+ const gchar *prefix;
+ const gchar *name;
+ GVariant *param;
+ } action;
+ struct {
+ const gchar *name;
+ GQuark detail;
+ GArray *params;
+ } signal;
+ };
+ struct _Shortcut *next;
+} Shortcut;
+
+typedef struct
+{
+ gchar *name;
+ GHashTable *keymap;
+ guint use_binding_sets : 1;
+} IdeShortcutContextPrivate;
+
+enum {
+ PROP_0,
+ PROP_NAME,
+ PROP_USE_BINDING_SETS,
+ N_PROPS
+};
+
+struct _IdeShortcutContext { GObject object; };
+G_DEFINE_TYPE_WITH_PRIVATE (IdeShortcutContext, ide_shortcut_context, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+translate_keyval (guint *keyval,
+ GdkModifierType *modifier)
+{
+ *modifier = *modifier & gtk_accelerator_get_default_mod_mask () & ~GDK_RELEASE_MASK;
+
+ if (*modifier & GDK_SHIFT_MASK)
+ {
+ if (*keyval == GDK_KEY_Tab)
+ *keyval = GDK_KEY_ISO_Left_Tab;
+ else
+ *keyval = gdk_keyval_to_upper (*keyval);
+ }
+}
+
+static void
+shortcut_free (gpointer data)
+{
+ Shortcut *shortcut = data;
+
+ if (shortcut != NULL)
+ {
+ g_clear_pointer (&shortcut->next, shortcut_free);
+
+ switch (shortcut->type)
+ {
+ case SHORTCUT_ACTION:
+ g_clear_pointer (&shortcut->action.param, g_variant_unref);
+ break;
+
+ case SHORTCUT_SIGNAL:
+ g_array_unref (shortcut->signal.params);
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+
+ g_slice_free (Shortcut, shortcut);
+ }
+}
+
+static guint
+shortcut_hash (gconstpointer data)
+{
+ const Shortcut *shortcut = data;
+
+ return shortcut->keyval ^ shortcut->modifier;
+}
+
+static gboolean
+shortcut_equal (gconstpointer a,
+ gconstpointer b)
+{
+ const Shortcut *as = a;
+ const Shortcut *bs = b;
+
+ return as->keyval == bs->keyval && as->modifier == bs->modifier;
+}
+
+static gboolean
+widget_action (GtkWidget *widget,
+ const gchar *prefix,
+ const gchar *action_name,
+ GVariant *parameter)
+{
+ GtkWidget *toplevel;
+ GApplication *app;
+ GActionGroup *group = NULL;
+
+ g_assert (GTK_IS_WIDGET (widget));
+ g_assert (prefix != NULL);
+ g_assert (action_name != NULL);
+
+ app = g_application_get_default ();
+ toplevel = gtk_widget_get_toplevel (widget);
+
+ while ((group == NULL) && (widget != NULL))
+ {
+ group = gtk_widget_get_action_group (widget, prefix);
+
+ if G_UNLIKELY (GTK_IS_POPOVER (widget))
+ {
+ GtkWidget *relative_to;
+
+ relative_to = gtk_popover_get_relative_to (GTK_POPOVER (widget));
+
+ if (relative_to != NULL)
+ widget = relative_to;
+ else
+ widget = gtk_widget_get_parent (widget);
+ }
+ else
+ {
+ widget = gtk_widget_get_parent (widget);
+ }
+ }
+
+ if (!group && g_str_equal (prefix, "win") && G_IS_ACTION_GROUP (toplevel))
+ group = G_ACTION_GROUP (toplevel);
+
+ if (!group && g_str_equal (prefix, "app") && G_IS_ACTION_GROUP (app))
+ group = G_ACTION_GROUP (app);
+
+ if (group && g_action_group_has_action (group, action_name))
+ {
+ g_action_group_activate_action (group, action_name, parameter);
+ return TRUE;
+ }
+
+ g_warning ("Failed to locate action %s.%s", prefix, action_name);
+
+ return FALSE;
+}
+
+static gboolean
+shortcut_action_activate (Shortcut *shortcut,
+ GtkWidget *widget,
+ const GdkEventKey *event)
+{
+ g_assert (shortcut != NULL);
+ g_assert (GTK_IS_WIDGET (widget));
+ g_assert (event != NULL);
+
+ return widget_action (widget,
+ shortcut->action.prefix,
+ shortcut->action.name,
+ shortcut->action.param);
+}
+
+static gboolean
+find_instance_and_signal (GtkWidget *widget,
+ const gchar *signal_name,
+ gpointer *instance,
+ GSignalQuery *query)
+{
+ IdeShortcutController *controller;
+
+ g_assert (GTK_IS_WIDGET (widget));
+ g_assert (signal_name != NULL);
+ g_assert (instance != NULL);
+ g_assert (query != NULL);
+
+ *instance = NULL;
+
+ /*
+ * First we want to see if we can resolve the signal on the widgets
+ * controller (if there is one). This allows us to change contexts
+ * from signals without installing signals on the actual widgets.
+ */
+
+ controller = ide_shortcut_controller_find (widget);
+
+ if (controller != NULL)
+ {
+ guint signal_id;
+
+ signal_id = g_signal_lookup (signal_name, G_OBJECT_TYPE (controller));
+
+ if (signal_id != 0)
+ {
+ g_signal_query (signal_id, query);
+ *instance = controller;
+ return TRUE;
+ }
+ }
+
+ /*
+ * This diverts from Gtk signal keybindings a bit in that we
+ * allow you to activate a signal on any widget in the focus
+ * hierarchy starting from the provided widget up.
+ */
+
+ while (widget != NULL)
+ {
+ guint signal_id;
+
+ signal_id = g_signal_lookup (signal_name, G_OBJECT_TYPE (widget));
+
+ if (signal_id != 0)
+ {
+ g_signal_query (signal_id, query);
+ *instance = widget;
+ return TRUE;
+ }
+
+ widget = gtk_widget_get_parent (widget);
+ }
+
+ return FALSE;
+}
+
+static gboolean
+shortcut_signal_activate (Shortcut *shortcut,
+ GtkWidget *widget,
+ const GdkEventKey *event)
+{
+ GValue *params;
+ GValue return_value = { 0 };
+ GSignalQuery query;
+ gpointer instance = NULL;
+
+ g_assert (shortcut != NULL);
+ g_assert (GTK_IS_WIDGET (widget));
+ g_assert (event != NULL);
+
+ if (!find_instance_and_signal (widget, shortcut->signal.name, &instance, &query))
+ {
+ g_warning ("Failed to locate signal %s in hierarchy of %s",
+ shortcut->signal.name, G_OBJECT_TYPE_NAME (widget));
+ return TRUE;
+ }
+
+ if (query.n_params != shortcut->signal.params->len)
+ goto parameter_mismatch;
+
+ for (guint i = 0; i < query.n_params; i++)
+ {
+ if (!G_VALUE_HOLDS (&g_array_index (shortcut->signal.params, GValue, i), query.param_types[i]))
+ goto parameter_mismatch;
+ }
+
+ params = g_new0 (GValue, 1 + query.n_params);
+ g_value_init_from_instance (¶ms[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 (¶ms[1+i], G_VALUE_TYPE (src_value));
+ g_value_copy (src_value, ¶ms[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 (¶ms[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 (¶ms);
+
+ ide_shortcut_context_add (self, shortcut);
+}
+
+void
+ide_shortcut_context_add_signal (IdeShortcutContext *self,
+ const gchar *accel,
+ const gchar *signal_name,
+ guint n_args,
+ ...)
+{
+ va_list args;
+
+ va_start (args, n_args);
+ ide_shortcut_context_add_signal_va_list (self, accel, signal_name, n_args, args);
+ va_end (args);
+}
+
+gboolean
+ide_shortcut_context_remove (IdeShortcutContext *self,
+ const gchar *accel)
+{
+ IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+ Shortcut lookup = { 0 };
+
+ g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), FALSE);
+ g_return_val_if_fail (accel != NULL, FALSE);
+
+ gtk_accelerator_parse (accel, &lookup.keyval, &lookup.modifier);
+
+ translate_keyval (&lookup.keyval, &lookup.modifier);
+
+ return g_hash_table_remove (priv->keymap, &lookup);
+}
+
+gboolean
+ide_shortcut_context_load_from_data (IdeShortcutContext *self,
+ const gchar *data,
+ gssize len,
+ GError **error)
+{
+ g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), FALSE);
+ g_return_val_if_fail (data != NULL, FALSE);
+
+ if (len < 0)
+ len = strlen (data);
+
+ g_set_error (error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "Failed to parse shortcut data");
+
+ return FALSE;
+}
+
+gboolean
+ide_shortcut_context_load_from_resource (IdeShortcutContext *self,
+ const gchar *resource_path,
+ GError **error)
+{
+ g_autoptr(GBytes) bytes = NULL;
+ const gchar *endptr = NULL;
+ const gchar *data;
+ gsize len;
+
+ g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), FALSE);
+
+ if (NULL == (bytes = g_resources_lookup_data (resource_path, 0, error)))
+ return FALSE;
+
+ data = g_bytes_get_data (bytes, &len);
+
+ if (!g_utf8_validate (data, len, &endptr))
+ {
+ g_set_error (error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "Invalid UTF-8 at offset %u",
+ (guint)(endptr - data));
+ return FALSE;
+ }
+
+ return ide_shortcut_context_load_from_data (self, data, len, error);
+}
diff --git a/libide/shortcuts/ide-shortcut-context.h b/libide/shortcuts/ide-shortcut-context.h
new file mode 100644
index 0000000..3bcbb48
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-context.h
@@ -0,0 +1,60 @@
+/* ide-shortcut-context.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_SHORTCUT_CONTEXT_H
+#define IDE_SHORTCUT_CONTEXT_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_CONTEXT (ide_shortcut_context_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutContext, ide_shortcut_context, IDE, SHORTCUT_CONTEXT, GObject)
+
+IdeShortcutContext *ide_shortcut_context_new (const gchar *name);
+const gchar *ide_shortcut_context_get_name (IdeShortcutContext *self);
+gboolean ide_shortcut_context_activate (IdeShortcutContext *self,
+ GtkWidget *widget,
+ const GdkEventKey *event);
+void ide_shortcut_context_add_action (IdeShortcutContext *self,
+ const gchar *accel,
+ const gchar *detailed_action_name);
+void ide_shortcut_context_add_signal (IdeShortcutContext *self,
+ const gchar *accel,
+ const gchar *signal_name,
+ guint n_args,
+ ...);
+void ide_shortcut_context_add_signal_va_list (IdeShortcutContext *self,
+ const gchar *accel,
+ const gchar *signal_name,
+ guint n_args,
+ va_list args);
+gboolean ide_shortcut_context_remove (IdeShortcutContext *self,
+ const gchar *accel);
+gboolean ide_shortcut_context_load_from_data (IdeShortcutContext *self,
+ const gchar *data,
+ gssize len,
+ GError **error);
+gboolean ide_shortcut_context_load_from_resource (IdeShortcutContext *self,
+ const gchar *resource_path,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_CONTEXT_H */
diff --git a/libide/shortcuts/ide-shortcut-controller.c b/libide/shortcuts/ide-shortcut-controller.c
new file mode 100644
index 0000000..96f5f05
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-controller.c
@@ -0,0 +1,541 @@
+/* ide-shortcut-controller.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-controller"
+
+#include "shortcuts/ide-shortcut-context.h"
+#include "shortcuts/ide-shortcut-controller.h"
+#include "shortcuts/ide-shortcut-manager.h"
+
+typedef struct
+{
+ /*
+ * This is the widget for which we are the shortcut controller. There are
+ * zero or one shortcut controller for a given widget. These are persistent
+ * and dispatch events to the current IdeShortcutContext (which can be
+ * changed upon theme changes or shortcuts emitting the ::set-context signal.
+ */
+ GtkWidget *widget;
+
+ /*
+ * This is the current context for the controller. These are collections of
+ * shortcuts to signals, actions, etc. The context can be changed in reaction
+ * to different events.
+ */
+ IdeShortcutContext *context;
+
+ /*
+ * This is a pointer to the root controller for the window. We register with
+ * the root controller so that keybindings can be activated even when the
+ * focus widget is somewhere else.
+ */
+ IdeShortcutController *root;
+
+ /*
+ * The root controller keeps track of the children controllers in the window.
+ * Instead of allocating GList entries, we use an inline GList for the Queue
+ * link nodes.
+ */
+ GQueue descendants;
+ GList descendants_link;
+
+ /*
+ * Signal handlers to react to various changes in the system.
+ */
+ gulong hierarchy_changed_handler;
+ gulong widget_destroy_handler;
+} IdeShortcutControllerPrivate;
+
+enum {
+ PROP_0,
+ PROP_CONTEXT,
+ PROP_WIDGET,
+ N_PROPS
+};
+
+enum {
+ RESET,
+ SET_CONTEXT_NAMED,
+ N_SIGNALS
+};
+
+struct _IdeShortcutController { GObject object; };
+G_DEFINE_TYPE_WITH_PRIVATE (IdeShortcutController, ide_shortcut_controller, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+static GQuark root_quark;
+static GQuark controller_quark;
+
+static void ide_shortcut_controller_connect (IdeShortcutController *self);
+static void ide_shortcut_controller_disconnect (IdeShortcutController *self);
+
+static gboolean
+ide_shortcut_controller_is_mapped (IdeShortcutController *self)
+{
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+ return priv->widget != NULL && gtk_widget_get_mapped (priv->widget);
+}
+
+static void
+ide_shortcut_controller_add (IdeShortcutController *self,
+ IdeShortcutController *descendant)
+{
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+ IdeShortcutControllerPrivate *dpriv = ide_shortcut_controller_get_instance_private (descendant);
+
+ g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+ g_assert (IDE_IS_SHORTCUT_CONTROLLER (descendant));
+
+ g_object_ref (descendant);
+
+ if (ide_shortcut_controller_is_mapped (descendant))
+ g_queue_push_head_link (&priv->descendants, &dpriv->descendants_link);
+ else
+ g_queue_push_tail_link (&priv->descendants, &dpriv->descendants_link);
+}
+
+static void
+ide_shortcut_controller_remove (IdeShortcutController *self,
+ IdeShortcutController *descendant)
+{
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+ IdeShortcutControllerPrivate *dpriv = ide_shortcut_controller_get_instance_private (descendant);
+
+ g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+ g_assert (IDE_IS_SHORTCUT_CONTROLLER (descendant));
+
+ g_queue_unlink (&priv->descendants, &dpriv->descendants_link);
+}
+
+static void
+ide_shortcut_controller_widget_destroy (IdeShortcutController *self,
+ GtkWidget *widget)
+{
+ g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+ g_assert (GTK_IS_WIDGET (widget));
+
+ ide_shortcut_controller_disconnect (self);
+}
+
+static void
+ide_shortcut_controller_widget_hierarchy_changed (IdeShortcutController *self,
+ GtkWidget *widget)
+{
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+ GtkWidget *toplevel;
+
+ g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+ g_assert (GTK_IS_WIDGET (widget));
+
+ g_object_ref (self);
+
+ /*
+ * Here we register our controller with the toplevel controller. If that
+ * widget doesn't yet have a placeholder toplevel controller, then we
+ * create that and attach to it.
+ *
+ * The toplevel controller is used to dispatch events from the window
+ * to any controller that could be activating for the window.
+ */
+
+ if (priv->root != NULL)
+ {
+ ide_shortcut_controller_remove (priv->root, self);
+ g_clear_object (&priv->root);
+ }
+
+ toplevel = gtk_widget_get_toplevel (widget);
+
+ if (toplevel != widget)
+ {
+ priv->root = g_object_get_qdata (G_OBJECT (toplevel), root_quark);
+ if (priv->root == NULL)
+ priv->root = ide_shortcut_controller_new (toplevel);
+ ide_shortcut_controller_add (priv->root, self);
+ }
+
+ g_object_unref (self);
+}
+
+static void
+ide_shortcut_controller_disconnect (IdeShortcutController *self)
+{
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+ g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+ g_assert (GTK_IS_WIDGET (priv->widget));
+
+ g_signal_handler_disconnect (priv->widget, priv->widget_destroy_handler);
+ priv->widget_destroy_handler = 0;
+
+ g_signal_handler_disconnect (priv->widget, priv->hierarchy_changed_handler);
+ priv->hierarchy_changed_handler = 0;
+}
+
+static void
+ide_shortcut_controller_connect (IdeShortcutController *self)
+{
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+ g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+ g_assert (GTK_IS_WIDGET (priv->widget));
+
+ priv->widget_destroy_handler =
+ g_signal_connect_swapped (priv->widget,
+ "destroy",
+ G_CALLBACK (ide_shortcut_controller_widget_destroy),
+ self);
+
+ priv->hierarchy_changed_handler =
+ g_signal_connect_swapped (priv->widget,
+ "hierarchy-changed",
+ G_CALLBACK (ide_shortcut_controller_widget_hierarchy_changed),
+ self);
+
+ ide_shortcut_controller_widget_hierarchy_changed (self, priv->widget);
+}
+
+static void
+ide_shortcut_controller_set_widget (IdeShortcutController *self,
+ GtkWidget *widget)
+{
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+ g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+ g_assert (GTK_IS_WIDGET (widget));
+
+ if (widget != priv->widget)
+ {
+ if (priv->widget != NULL)
+ {
+ ide_shortcut_controller_disconnect (self);
+ g_object_remove_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget);
+ priv->widget = NULL;
+ }
+
+ if (widget != NULL && widget != priv->widget)
+ {
+ priv->widget = widget;
+ g_object_add_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget);
+ ide_shortcut_controller_connect (self);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WIDGET]);
+ }
+}
+
+static void
+ide_shortcut_controller_emit_reset (IdeShortcutController *self)
+{
+ g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+
+ g_signal_emit (self, signals[RESET], 0);
+}
+
+void
+ide_shortcut_controller_set_context (IdeShortcutController *self,
+ IdeShortcutContext *context)
+{
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self));
+ g_return_if_fail (!context || IDE_IS_SHORTCUT_CONTEXT (context));
+
+ if (g_set_object (&priv->context, context))
+ {
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CONTEXT]);
+ ide_shortcut_controller_emit_reset (self);
+ }
+}
+
+static void
+ide_shortcut_controller_real_set_context_named (IdeShortcutController *self,
+ const gchar *name)
+{
+ g_autoptr(IdeShortcutContext) context = NULL;
+ IdeShortcutManager *manager;
+ IdeShortcutTheme *theme;
+
+ g_return_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self));
+ g_return_if_fail (name != NULL);
+
+ manager = ide_shortcut_manager_get_default ();
+ theme = ide_shortcut_manager_get_theme (manager);
+ context = ide_shortcut_theme_find_context_by_name (theme, name);
+
+ ide_shortcut_controller_set_context (self, context);
+}
+
+static void
+ide_shortcut_controller_finalize (GObject *object)
+{
+ IdeShortcutController *self = (IdeShortcutController *)object;
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+ if (priv->widget != NULL)
+ {
+ g_object_remove_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget);
+ priv->widget = NULL;
+ }
+
+ g_clear_object (&priv->context);
+ g_clear_object (&priv->root);
+
+ while (priv->descendants.length > 0)
+ g_queue_unlink (&priv->descendants, priv->descendants.head);
+
+ G_OBJECT_CLASS (ide_shortcut_controller_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_controller_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeShortcutController *self = (IdeShortcutController *)object;
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_CONTEXT:
+ g_value_set_object (value, priv->context);
+ break;
+
+ case PROP_WIDGET:
+ g_value_set_object (value, priv->widget);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_shortcut_controller_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IdeShortcutController *self = (IdeShortcutController *)object;
+
+ switch (prop_id)
+ {
+ case PROP_CONTEXT:
+ ide_shortcut_controller_set_context (self, g_value_get_object (value));
+ break;
+
+ case PROP_WIDGET:
+ ide_shortcut_controller_set_widget (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_shortcut_controller_class_init (IdeShortcutControllerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = ide_shortcut_controller_finalize;
+ object_class->get_property = ide_shortcut_controller_get_property;
+ object_class->set_property = ide_shortcut_controller_set_property;
+
+ properties [PROP_CONTEXT] =
+ g_param_spec_object ("context",
+ "Context",
+ "The current context of the controller",
+ IDE_TYPE_SHORTCUT_CONTEXT,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_WIDGET] =
+ g_param_spec_object ("widget",
+ "Widget",
+ "The widget for which the controller attached",
+ GTK_TYPE_WIDGET,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ /**
+ * IdeShortcutController::reset:
+ *
+ * This signal is emitted when the shortcut controller is requesting
+ * the widget to reset any state it may have regarding the shortcut
+ * controller. Such an example might be a modal system that lives
+ * outside the controller whose state should be cleared in response
+ * to the controller changing modes.
+ */
+ signals [RESET] =
+ g_signal_new_class_handler ("reset",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ NULL, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+ /**
+ * IdeShortcutController::set-context-named:
+ * @self: An #IdeShortcutController
+ * @name: The name of the context
+ *
+ * This changes the current context on the #IdeShortcutController to be the
+ * context matching @name. This is found by looking up the context by name
+ * in the active #IdeShortcutTheme.
+ */
+ signals [SET_CONTEXT_NAMED] =
+ g_signal_new_class_handler ("set-context",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ G_CALLBACK (ide_shortcut_controller_real_set_context_named),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 1, G_TYPE_STRING);
+
+ controller_quark = g_quark_from_static_string ("IDE_SHORTCUT_CONTROLLER");
+ root_quark = g_quark_from_static_string ("IDE_SHORTCUT_CONTROLLER_ROOT");
+}
+
+static void
+ide_shortcut_controller_init (IdeShortcutController *self)
+{
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+ g_queue_init (&priv->descendants);
+
+ priv->descendants_link.data = self;
+}
+
+IdeShortcutController *
+ide_shortcut_controller_new (GtkWidget *widget)
+{
+ IdeShortcutController *ret;
+
+ g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+ if (NULL != (ret = g_object_get_qdata (G_OBJECT (widget), controller_quark)))
+ return g_object_ref (ret);
+
+ ret = g_object_new (IDE_TYPE_SHORTCUT_CONTROLLER,
+ "widget", widget,
+ NULL);
+
+ g_object_set_qdata_full (G_OBJECT (widget),
+ controller_quark,
+ g_object_ref (ret),
+ g_object_unref);
+
+ return ret;
+}
+
+/**
+ * ide_shortcut_controller_find:
+ *
+ * Finds the registered #IdeShortcutController for a widget.
+ *
+ * Returns: (nullable) (transfer none): An #IdeShortcutController or %NULL.
+ */
+IdeShortcutController *
+ide_shortcut_controller_find (GtkWidget *widget)
+{
+ g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+ return g_object_get_qdata (G_OBJECT (widget), controller_quark);
+}
+
+/**
+ * ide_shortcut_controller_get_context:
+ * @self: An #IdeShortcutController
+ *
+ * This function gets the #IdeShortcutController:context property, which
+ * is the current context to dispatch events to. An #IdeShortcutContext
+ * is a group of keybindings that may be activated in response to a
+ * single or series of #GdkEventKey.
+ *
+ * Returns: (transfer none) (nullable): An #IdeShortcutContext or %NULL.
+ */
+IdeShortcutContext *
+ide_shortcut_controller_get_context (IdeShortcutController *self)
+{
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self), NULL);
+
+ if (priv->widget == NULL)
+ return NULL;
+
+ if (priv->context == NULL)
+ {
+ IdeShortcutManager *manager;
+ IdeShortcutTheme *theme;
+
+ manager = ide_shortcut_manager_get_default ();
+ theme = ide_shortcut_manager_get_theme (manager);
+
+ priv->context = ide_shortcut_theme_find_default_context (theme, priv->widget);
+ }
+
+ return priv->context;
+}
+
+/**
+ * ide_shortcut_controller_handle_event:
+ * @self: An #IdeShortcutController
+ * @event: A #GdkEventKey
+ *
+ * This function uses @event to determine if the current context has a shortcut
+ * registered matching the event. If so, the shortcut will be dispatched and
+ * %TRUE is returned.
+ *
+ * Otherwise, %FALSE is returned.
+ *
+ * Returns: %TRUE if @event has been handled, otherwise %FALSE.
+ */
+gboolean
+ide_shortcut_controller_handle_event (IdeShortcutController *self,
+ const GdkEventKey *event)
+{
+ IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+ IdeShortcutContext *context;
+
+ g_return_val_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self), FALSE);
+ g_return_val_if_fail (event != NULL, FALSE);
+
+ if (priv->widget == NULL ||
+ !gtk_widget_get_visible (priv->widget) ||
+ !gtk_widget_is_sensitive (priv->widget))
+ return FALSE;
+
+ context = ide_shortcut_controller_get_context (self);
+
+ if (context != NULL)
+ {
+ if (ide_shortcut_context_activate (context, priv->widget, event))
+ return GDK_EVENT_STOP;
+ }
+
+ for (GList *iter = priv->descendants.head; iter != NULL; iter = iter->next)
+ {
+ IdeShortcutController *descendant = iter->data;
+
+ if (ide_shortcut_controller_handle_event (descendant, event))
+ return GDK_EVENT_STOP;
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
diff --git a/libide/shortcuts/ide-shortcut-controller.h b/libide/shortcuts/ide-shortcut-controller.h
new file mode 100644
index 0000000..68c6307
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-controller.h
@@ -0,0 +1,42 @@
+/* ide-shortcut-controller.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_SHORTCUT_CONTROLLER_H
+#define IDE_SHORTCUT_CONTROLLER_H
+
+#include <gtk/gtk.h>
+
+#include "ide-shortcut-context.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_CONTROLLER (ide_shortcut_controller_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutController, ide_shortcut_controller, IDE, SHORTCUT_CONTROLLER, GObject)
+
+IdeShortcutController *ide_shortcut_controller_new (GtkWidget *widget);
+gboolean ide_shortcut_controller_handle_event (IdeShortcutController *self,
+ const GdkEventKey *event);
+IdeShortcutController *ide_shortcut_controller_find (GtkWidget *widget);
+IdeShortcutContext *ide_shortcut_controller_get_context (IdeShortcutController *self);
+void ide_shortcut_controller_set_context (IdeShortcutController *self,
+ IdeShortcutContext *context);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_CONTROLLER_H */
diff --git a/libide/shortcuts/ide-shortcut-manager.c b/libide/shortcuts/ide-shortcut-manager.c
new file mode 100644
index 0000000..89e8c4a
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-manager.c
@@ -0,0 +1,441 @@
+/* ide-shortcut-manager.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-manager.h"
+
+#include "shortcuts/ide-shortcut-controller.h"
+#include "shortcuts/ide-shortcut-manager.h"
+
+typedef struct
+{
+ IdeShortcutTheme *theme;
+ GPtrArray *themes;
+} IdeShortcutManagerPrivate;
+
+enum {
+ PROP_0,
+ PROP_THEME,
+ PROP_THEME_NAME,
+ N_PROPS
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+struct _IdeShortcutManager { GObject object; };
+G_DEFINE_TYPE_WITH_CODE (IdeShortcutManager, ide_shortcut_manager, G_TYPE_OBJECT,
+ G_ADD_PRIVATE (IdeShortcutManager)
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_shortcut_manager_finalize (GObject *object)
+{
+ IdeShortcutManager *self = (IdeShortcutManager *)object;
+ IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+ g_clear_pointer (&priv->themes, g_ptr_array_unref);
+ g_clear_object (&priv->theme);
+
+ G_OBJECT_CLASS (ide_shortcut_manager_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_manager_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeShortcutManager *self = (IdeShortcutManager *)object;
+
+ switch (prop_id)
+ {
+ case PROP_THEME:
+ g_value_set_object (value, ide_shortcut_manager_get_theme (self));
+ break;
+
+ case PROP_THEME_NAME:
+ g_value_set_string (value, ide_shortcut_manager_get_theme_name (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_shortcut_manager_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IdeShortcutManager *self = (IdeShortcutManager *)object;
+
+ switch (prop_id)
+ {
+ case PROP_THEME:
+ ide_shortcut_manager_set_theme (self, g_value_get_object (value));
+ break;
+
+ case PROP_THEME_NAME:
+ ide_shortcut_manager_set_theme_name (self, g_value_get_string (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_shortcut_manager_class_init (IdeShortcutManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = ide_shortcut_manager_finalize;
+ object_class->get_property = ide_shortcut_manager_get_property;
+ object_class->set_property = ide_shortcut_manager_set_property;
+
+ properties [PROP_THEME] =
+ g_param_spec_object ("theme",
+ "Theme",
+ "The current key theme.",
+ IDE_TYPE_SHORTCUT_THEME,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_THEME_NAME] =
+ g_param_spec_string ("theme-name",
+ "Theme Name",
+ "The name of the current theme",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_shortcut_manager_init (IdeShortcutManager *self)
+{
+ IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+ priv->themes = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+/**
+ * ide_shortcut_manager_get_default:
+ *
+ * Gets the singleton #IdeShortcutManager for the process.
+ *
+ * Returns: (transfer none) (not nullable): An #IdeShortcutManager.
+ */
+IdeShortcutManager *
+ide_shortcut_manager_get_default (void)
+{
+ static IdeShortcutManager *instance;
+
+ if (instance == NULL)
+ {
+ instance = g_object_new (IDE_TYPE_SHORTCUT_MANAGER, NULL);
+ g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance);
+ }
+
+ return instance;
+}
+
+/**
+ * ide_shortcut_manager_get_theme:
+ *
+ * Gets the "theme" property.
+ *
+ * Returns: (transfer none) (not nullable): An #IdeShortcutTheme.
+ */
+IdeShortcutTheme *
+ide_shortcut_manager_get_theme (IdeShortcutManager *self)
+{
+ IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), NULL);
+
+ if (priv->theme == NULL)
+ priv->theme = g_object_new (IDE_TYPE_SHORTCUT_THEME,
+ "name", "default",
+ NULL);
+
+ return priv->theme;
+}
+
+/**
+ * ide_shortcut_manager_set_theme:
+ * @self: An #IdeShortcutManager
+ * @theme: (not nullable): An #IdeShortcutTheme
+ *
+ * Sets the theme for the shortcut manager.
+ */
+void
+ide_shortcut_manager_set_theme (IdeShortcutManager *self,
+ IdeShortcutTheme *theme)
+{
+ IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_SHORTCUT_MANAGER (self));
+ g_return_if_fail (IDE_IS_SHORTCUT_THEME (theme));
+
+ /*
+ * It is important that IdeShortcutController instances watch for
+ * notify::theme so that they can reset their state. Otherwise, we
+ * could be transitioning between incorrect contexts.
+ */
+
+ if (g_set_object (&priv->theme, theme))
+ {
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME]);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME_NAME]);
+ }
+}
+
+/**
+ * ide_shortcut_manager_handle_event:
+ * @self: (nullable): An #IdeShortcutManager
+ * @toplevel: A #GtkWidget or %NULL.
+ * @event: A #GdkEventKey event to handle.
+ *
+ * This function will try to dispatch @event to the proper widget and
+ * #IdeShortcutContext. If the event is handled, then %TRUE is returned.
+ *
+ * You should call this from #GtkWidget::key-press-event handler in your
+ * #GtkWindow toplevel.
+ *
+ * Returns: %TRUE if the event was handled.
+ */
+gboolean
+ide_shortcut_manager_handle_event (IdeShortcutManager *self,
+ const GdkEventKey *event,
+ GtkWidget *toplevel)
+{
+ GtkWidget *widget;
+ GtkWidget *focus;
+ GdkModifierType modifier;
+
+ if (self == NULL)
+ self = ide_shortcut_manager_get_default ();
+
+ g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), FALSE);
+ g_return_val_if_fail (GTK_IS_WINDOW (toplevel), FALSE);
+ g_return_val_if_fail (event != NULL, FALSE);
+
+ if (event->type != GDK_KEY_PRESS)
+ return GDK_EVENT_PROPAGATE;
+
+ modifier = event->state & gtk_accelerator_get_default_mod_mask ();
+ widget = focus = gtk_window_get_focus (GTK_WINDOW (toplevel));
+
+ while (widget != NULL)
+ {
+ IdeShortcutController *controller;
+ gboolean use_binding_sets = TRUE;
+
+ if (NULL != (controller = ide_shortcut_controller_find (widget)))
+ {
+ IdeShortcutContext *context = ide_shortcut_controller_get_context (controller);
+
+ /*
+ * Fetch this property first as the controller context could change
+ * during activation of the handle_event().
+ */
+ if (context != NULL)
+ g_object_get (context,
+ "use-binding-sets", &use_binding_sets,
+ NULL);
+
+ /*
+ * Now try to activate the event using the controller.
+ */
+ if (ide_shortcut_controller_handle_event (controller, event))
+ return GDK_EVENT_STOP;
+ }
+
+ /*
+ * If the current context at activation indicates that we can
+ * dispatch using the default binding sets for the widget, go
+ * ahead and try to do that.
+ */
+ if (use_binding_sets)
+ {
+ GtkStyleContext *style_context;
+ g_autoptr(GPtrArray) sets = NULL;
+
+ style_context = gtk_widget_get_style_context (widget);
+ gtk_style_context_get (style_context,
+ gtk_style_context_get_state (style_context),
+ "-gtk-key-bindings", &sets,
+ NULL);
+
+ if (sets != NULL)
+ {
+ for (guint i = 0; i < sets->len; i++)
+ {
+ GtkBindingSet *set = g_ptr_array_index (sets, i);
+
+ if (gtk_binding_set_activate (set, event->keyval, modifier, G_OBJECT (widget)))
+ return GDK_EVENT_STOP;
+ }
+ }
+
+ /*
+ * Only if this widget is also our focus, try to activate the default
+ * keybindings for the widget.
+ */
+ if (widget == focus)
+ {
+ GtkBindingSet *set = gtk_binding_set_by_class (G_OBJECT_GET_CLASS (widget));
+
+ if (gtk_binding_set_activate (set, event->keyval, modifier, G_OBJECT (widget)))
+ return GDK_EVENT_STOP;
+ }
+ }
+
+ widget = gtk_widget_get_parent (widget);
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+const gchar *
+ide_shortcut_manager_get_theme_name (IdeShortcutManager *self)
+{
+ IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+ const gchar *ret = NULL;
+
+ g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), NULL);
+
+ if (priv->theme != NULL)
+ ret = ide_shortcut_theme_get_name (priv->theme);
+
+ return ret;
+}
+
+void
+ide_shortcut_manager_set_theme_name (IdeShortcutManager *self,
+ const gchar *name)
+{
+ IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_SHORTCUT_MANAGER (self));
+
+ if (name == NULL)
+ name = "default";
+
+ for (guint i = 0; i < priv->themes->len; i++)
+ {
+ IdeShortcutTheme *theme = g_ptr_array_index (priv->themes, i);
+ const gchar *theme_name = ide_shortcut_theme_get_name (theme);
+
+ if (g_strcmp0 (name, theme_name) == 0)
+ {
+ ide_shortcut_manager_set_theme (self, theme);
+ return;
+ }
+ }
+
+ g_warning ("No such shortcut theme ā%sā", name);
+}
+
+static guint
+ide_shortcut_manager_get_n_items (GListModel *model)
+{
+ IdeShortcutManager *self = (IdeShortcutManager *)model;
+ IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), 0);
+
+ return priv->themes->len;
+}
+
+static GType
+ide_shortcut_manager_get_item_type (GListModel *model)
+{
+ return IDE_TYPE_SHORTCUT_THEME;
+}
+
+static gpointer
+ide_shortcut_manager_get_item (GListModel *model,
+ guint position)
+{
+ IdeShortcutManager *self = (IdeShortcutManager *)model;
+ IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), NULL);
+ g_return_val_if_fail (position < priv->themes->len, NULL);
+
+ return g_object_ref (g_ptr_array_index (priv->themes, position));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_n_items = ide_shortcut_manager_get_n_items;
+ iface->get_item_type = ide_shortcut_manager_get_item_type;
+ iface->get_item = ide_shortcut_manager_get_item;
+}
+
+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..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]