/* dzl-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 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#define G_LOG_DOMAIN "dzl-shortcut-controller"
#include "config.h"
#include <stdlib.h>
#include <string.h>
#include "dzl-debug.h"
#include "shortcuts/dzl-shortcut-closure-chain.h"
#include "shortcuts/dzl-shortcut-context.h"
#include "shortcuts/dzl-shortcut-controller.h"
#include "shortcuts/dzl-shortcut-manager.h"
#include "shortcuts/dzl-shortcut-private.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 DzlShortcutContext (which can be
* changed upon theme changes or shortcuts emitting the ::set-context signal.
*/
GtkWidget *widget;
/*
* This is the name of the current context. Contexts are resolved at runtime
* by locating them within the theme (or inherited theme). They are interned
* strings to avoid lots of allocations between widgets.
*/
const gchar *context_name;
/*
* If we are building a chord, it will be tracked here. Each incoming
* GdkEventKey will contribute to the creation of this chord.
*/
DzlShortcutChord *current_chord;
/*
* 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.
*/
DzlShortcutController *root;
/*
* The commands that are attached to this controller including callbacks,
* signals, or actions. We use the commands_table to get a chord to the
* intern'd string containing the command id (for direct comparisons).
*/
GHashTable *commands;
/*
* The command table is used to provide a mapping from accelerator/chord
* to the key for @commands. The data for each chord is an interned string
* which can be used as a direct pointer for lookups in @commands.
*/
DzlShortcutChordTable *commands_table;
/*
* The root controller may have a manager associated with it to determine
* what themes and shortcuts are available.
*/
DzlShortcutManager *manager;
/*
* 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;
/*
* To avoid allocating GList nodes for controllers, we just inline a link
* here and attach it to @descendants when necessary.
*/
GList descendants_link;
/* Signal handlers to react to various changes in the system. */
gulong hierarchy_changed_handler;
gulong widget_destroy_handler;
gulong manager_changed_handler;
/* If we have any global shortcuts registered */
guint have_global : 1;
} DzlShortcutControllerPrivate;
enum {
PROP_0,
PROP_CONTEXT,
PROP_CURRENT_CHORD,
PROP_MANAGER,
PROP_WIDGET,
N_PROPS
};
enum {
RESET,
SET_CONTEXT_NAMED,
N_SIGNALS
};
struct _DzlShortcutController { GObject object; };
G_DEFINE_TYPE_WITH_PRIVATE (DzlShortcutController, dzl_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 dzl_shortcut_controller_connect (DzlShortcutController *self);
static void dzl_shortcut_controller_disconnect (DzlShortcutController *self);
static void
dzl_shortcut_controller_emit_reset (DzlShortcutController *self)
{
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_signal_emit (self, signals[RESET], 0);
}
static inline gboolean
dzl_shortcut_controller_is_root (DzlShortcutController *self)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
return priv->root == NULL;
}
/**
* dzl_shortcut_controller_get_manager:
* @self: a #DzlShortcutController
*
* Gets the #DzlShortcutManager associated with this controller.
*
* Generally, this will look for the root controller's manager as mixing and
* matching managers in a single window hierarchy is not supported.
*
* Returns: (not nullable) (transfer none): A #DzlShortcutManager.
*/
DzlShortcutManager *
dzl_shortcut_controller_get_manager (DzlShortcutController *self)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
if (priv->root != NULL)
return dzl_shortcut_controller_get_manager (priv->root);
if (priv->manager != NULL)
return priv->manager;
return dzl_shortcut_manager_get_default ();
}
/**
* dzl_shortcut_controller_set_manager:
* @self: a #DzlShortcutController
* @manager: (nullable): A #DzlShortcutManager or %NULL
*
* Sets the #DzlShortcutController:manager property.
*
* If you set this to %NULL, it will revert to the default #DzlShortcutManager
* for the process.
*/
void
dzl_shortcut_controller_set_manager (DzlShortcutController *self,
DzlShortcutManager *manager)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self));
g_return_if_fail (!manager || DZL_IS_SHORTCUT_MANAGER (manager));
if (g_set_object (&priv->manager, manager))
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MANAGER]);
}
static gboolean
dzl_shortcut_controller_is_mapped (DzlShortcutController *self)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
return priv->widget != NULL && gtk_widget_get_mapped (priv->widget);
}
static void
dzl_shortcut_controller_add (DzlShortcutController *self,
DzlShortcutController *descendant)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
DzlShortcutControllerPrivate *dpriv = dzl_shortcut_controller_get_instance_private (descendant);
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_assert (DZL_IS_SHORTCUT_CONTROLLER (descendant));
g_object_ref (descendant);
if (dzl_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
dzl_shortcut_controller_remove (DzlShortcutController *self,
DzlShortcutController *descendant)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
DzlShortcutControllerPrivate *dpriv = dzl_shortcut_controller_get_instance_private (descendant);
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_assert (DZL_IS_SHORTCUT_CONTROLLER (descendant));
g_queue_unlink (&priv->descendants, &dpriv->descendants_link);
g_object_unref (descendant);
}
static void
dzl_shortcut_controller_on_manager_changed (DzlShortcutController *self,
DzlShortcutManager *manager)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_assert (DZL_IS_SHORTCUT_MANAGER (manager));
priv->context_name = NULL;
_dzl_shortcut_controller_clear (self);
dzl_shortcut_controller_emit_reset (self);
}
static void
dzl_shortcut_controller_widget_destroy (DzlShortcutController *self,
GtkWidget *widget)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_assert (GTK_IS_WIDGET (widget));
dzl_shortcut_controller_disconnect (self);
g_clear_weak_pointer (&priv->widget);
if (priv->root != NULL)
{
dzl_shortcut_controller_remove (priv->root, self);
g_clear_object (&priv->root);
}
}
static void
dzl_shortcut_controller_widget_hierarchy_changed (DzlShortcutController *self,
GtkWidget *previous_toplevel,
GtkWidget *widget)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
GtkWidget *toplevel;
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_assert (!previous_toplevel || GTK_IS_WIDGET (previous_toplevel));
g_assert (GTK_IS_WIDGET (widget));
/*
* We attach our controller to the root controller if we have shortcuts in
* the global activation phase. That allows the bubble/capture phase to
* potentially dispatch to our controller.
*/
g_object_ref (self);
if (priv->root != NULL)
{
dzl_shortcut_controller_remove (priv->root, self);
g_clear_object (&priv->root);
}
if (priv->have_global)
{
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 = dzl_shortcut_controller_new (toplevel);
dzl_shortcut_controller_add (priv->root, self);
}
}
g_object_unref (self);
}
static void
dzl_shortcut_controller_disconnect (DzlShortcutController *self)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
DzlShortcutManager *manager;
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_assert (GTK_IS_WIDGET (priv->widget));
manager = dzl_shortcut_controller_get_manager (self);
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;
g_signal_handler_disconnect (manager, priv->manager_changed_handler);
priv->manager_changed_handler = 0;
}
static void
dzl_shortcut_controller_connect (DzlShortcutController *self)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
DzlShortcutManager *manager;
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_assert (GTK_IS_WIDGET (priv->widget));
manager = dzl_shortcut_controller_get_manager (self);
g_clear_pointer (&priv->current_chord, dzl_shortcut_chord_free);
priv->context_name = NULL;
priv->widget_destroy_handler =
g_signal_connect_swapped (priv->widget,
"destroy",
G_CALLBACK (dzl_shortcut_controller_widget_destroy),
self);
priv->hierarchy_changed_handler =
g_signal_connect_swapped (priv->widget,
"hierarchy-changed",
G_CALLBACK (dzl_shortcut_controller_widget_hierarchy_changed),
self);
priv->manager_changed_handler =
g_signal_connect_swapped (manager,
"changed",
G_CALLBACK (dzl_shortcut_controller_on_manager_changed),
self);
dzl_shortcut_controller_widget_hierarchy_changed (self, NULL, priv->widget);
}
static void
dzl_shortcut_controller_set_widget (DzlShortcutController *self,
GtkWidget *widget)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_assert (GTK_IS_WIDGET (widget));
if (widget != priv->widget)
{
if (priv->widget != NULL)
{
dzl_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);
dzl_shortcut_controller_connect (self);
}
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WIDGET]);
}
}
/**
* dzl_shortcut_controller_set_context_by_name:
* @self: a #DzlShortcutController
* @name: (nullable): The name of the context
*
* Changes the context for the controller to the context matching @name.
*
* Contexts are resolved at runtime through the current theme (and possibly
* a parent theme if it inherits from one).
*
* Since: 3.26
*/
void
dzl_shortcut_controller_set_context_by_name (DzlShortcutController *self,
const gchar *name)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self));
name = g_intern_string (name);
if (name != priv->context_name)
{
priv->context_name = name;
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONTEXT]);
dzl_shortcut_controller_emit_reset (self);
}
}
static void
dzl_shortcut_controller_real_set_context_named (DzlShortcutController *self,
const gchar *name)
{
g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self));
dzl_shortcut_controller_set_context_by_name (self, name);
}
static void
dzl_shortcut_controller_finalize (GObject *object)
{
DzlShortcutController *self = (DzlShortcutController *)object;
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
if (priv->widget != NULL)
{
g_object_remove_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget);
priv->widget = NULL;
}
g_clear_pointer (&priv->commands, g_hash_table_unref);
g_clear_pointer (&priv->commands_table, dzl_shortcut_chord_table_free);
g_clear_object (&priv->root);
while (priv->descendants.length > 0)
g_queue_unlink (&priv->descendants, priv->descendants.head);
priv->context_name = NULL;
G_OBJECT_CLASS (dzl_shortcut_controller_parent_class)->finalize (object);
}
static void
dzl_shortcut_controller_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
DzlShortcutController *self = (DzlShortcutController *)object;
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
switch (prop_id)
{
case PROP_CONTEXT:
g_value_set_object (value, dzl_shortcut_controller_get_context (self));
break;
case PROP_CURRENT_CHORD:
g_value_set_boxed (value, dzl_shortcut_controller_get_current_chord (self));
break;
case PROP_MANAGER:
g_value_set_object (value, dzl_shortcut_controller_get_manager (self));
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
dzl_shortcut_controller_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
DzlShortcutController *self = (DzlShortcutController *)object;
switch (prop_id)
{
case PROP_MANAGER:
dzl_shortcut_controller_set_manager (self, g_value_get_object (value));
break;
case PROP_WIDGET:
dzl_shortcut_controller_set_widget (self, g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
dzl_shortcut_controller_class_init (DzlShortcutControllerClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = dzl_shortcut_controller_finalize;
object_class->get_property = dzl_shortcut_controller_get_property;
object_class->set_property = dzl_shortcut_controller_set_property;
properties [PROP_CURRENT_CHORD] =
g_param_spec_boxed ("current-chord",
"Current Chord",
"The current chord for the controller",
DZL_TYPE_SHORTCUT_CHORD,
(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
properties [PROP_CONTEXT] =
g_param_spec_object ("context",
"Context",
"The current context of the controller, for dispatch phase",
DZL_TYPE_SHORTCUT_CONTEXT,
(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
properties [PROP_MANAGER] =
g_param_spec_object ("manager",
"Manager",
"The shortcut manager",
DZL_TYPE_SHORTCUT_MANAGER,
(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);
/**
* DzlShortcutController::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);
/**
* DzlShortcutController::set-context-named:
* @self: An #DzlShortcutController
* @name: The name of the context
*
* This changes the current context on the #DzlShortcutController to be the
* context matching @name. This is found by looking up the context by name
* in the active #DzlShortcutTheme.
*/
signals [SET_CONTEXT_NAMED] =
g_signal_new_class_handler ("set-context-named",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_CALLBACK (dzl_shortcut_controller_real_set_context_named),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_STRING);
controller_quark = g_quark_from_static_string ("DZL_SHORTCUT_CONTROLLER");
root_quark = g_quark_from_static_string ("DZL_SHORTCUT_CONTROLLER_ROOT");
}
static void
dzl_shortcut_controller_init (DzlShortcutController *self)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_queue_init (&priv->descendants);
priv->descendants_link.data = self;
}
DzlShortcutController *
dzl_shortcut_controller_new (GtkWidget *widget)
{
DzlShortcutController *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 (DZL_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;
}
/**
* dzl_shortcut_controller_try_find:
*
* Finds the registered #DzlShortcutController for a widget.
*
* If no controller is found, %NULL is returned.
*
* Returns: (nullable) (transfer none): An #DzlShortcutController or %NULL.
*/
DzlShortcutController *
dzl_shortcut_controller_try_find (GtkWidget *widget)
{
g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
return g_object_get_qdata (G_OBJECT (widget), controller_quark);
}
/**
* dzl_shortcut_controller_find:
*
* Finds the registered #DzlShortcutController for a widget.
*
* The controller is created if it does not already exist.
*
* Returns: (not nullable) (transfer none): An #DzlShortcutController or %NULL.
*/
DzlShortcutController *
dzl_shortcut_controller_find (GtkWidget *widget)
{
DzlShortcutController *controller;
g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
controller = g_object_get_qdata (G_OBJECT (widget), controller_quark);
if (controller == NULL)
{
/* We want to pass a borrowed reference */
g_object_unref (dzl_shortcut_controller_new (widget));
controller = g_object_get_qdata (G_OBJECT (widget), controller_quark);
}
return controller;
}
static DzlShortcutContext *
_dzl_shortcut_controller_get_context_for_phase (DzlShortcutController *self,
DzlShortcutTheme *theme,
DzlShortcutPhase phase)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_autofree gchar *phased_name = NULL;
DzlShortcutContext *ret;
const gchar *name;
g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), NULL);
g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (theme), NULL);
if (priv->widget == NULL)
return NULL;
name = priv->context_name ? priv->context_name : G_OBJECT_TYPE_NAME (priv->widget);
g_return_val_if_fail (name != NULL, NULL);
/* If we are in dispatch phase, we use our direct context */
if (phase == DZL_SHORTCUT_PHASE_BUBBLE)
name = phased_name = g_strdup_printf ("%s:bubble", name);
else if (phase == DZL_SHORTCUT_PHASE_CAPTURE)
name = phased_name = g_strdup_printf ("%s:capture", name);
ret = _dzl_shortcut_theme_try_find_context_by_name (theme, name);
g_return_val_if_fail (!ret || DZL_IS_SHORTCUT_CONTEXT (ret), NULL);
return ret;
}
/**
* dzl_shortcut_controller_get_context_for_phase:
* @self: a #DzlShortcutController
* @phase: the phase for the shorcut delivery
*
* Controllers can have a different context for a particular phase, which allows
* them to activate different keybindings depending if the event in capture,
* bubble, or dispatch.
*
* Returns: (transfer none) (nullable): A #DzlShortcutContext or %NULL.
*
* Since: 3.26
*/
DzlShortcutContext *
dzl_shortcut_controller_get_context_for_phase (DzlShortcutController *self,
DzlShortcutPhase phase)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
DzlShortcutManager *manager;
DzlShortcutTheme *theme;
g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), NULL);
if (NULL == priv->widget ||
NULL == (manager = dzl_shortcut_controller_get_manager (self)) ||
NULL == (theme = dzl_shortcut_manager_get_theme (manager)))
return NULL;
return _dzl_shortcut_controller_get_context_for_phase (self, theme, phase);
}
/**
* dzl_shortcut_controller_get_context:
* @self: An #DzlShortcutController
*
* This function gets the #DzlShortcutController:context property, which
* is the current context to dispatch events to. An #DzlShortcutContext
* is a group of keybindings that may be activated in response to a
* single or series of #GdkEventKey.
*
* Returns: (transfer none) (nullable): A #DzlShortcutContext or %NULL.
*
* Since: 3.26
*/
DzlShortcutContext *
dzl_shortcut_controller_get_context (DzlShortcutController *self)
{
g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), NULL);
return dzl_shortcut_controller_get_context_for_phase (self, DZL_SHORTCUT_PHASE_DISPATCH);
}
static DzlShortcutContext *
dzl_shortcut_controller_get_inherited_context (DzlShortcutController *self,
DzlShortcutPhase phase)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
DzlShortcutManager *manager;
DzlShortcutContext *ret;
DzlShortcutTheme *theme;
DzlShortcutTheme *parent;
const gchar *parent_name = NULL;
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
if (NULL == priv->widget ||
NULL == (manager = dzl_shortcut_controller_get_manager (self)) ||
NULL == (theme = dzl_shortcut_manager_get_theme (manager)) ||
NULL == (parent_name = dzl_shortcut_theme_get_parent_name (theme)) ||
NULL == (parent = dzl_shortcut_manager_get_theme_by_name (manager, parent_name)))
return NULL;
ret = _dzl_shortcut_controller_get_context_for_phase (self, parent, phase);
g_return_val_if_fail (!ret || DZL_IS_SHORTCUT_CONTEXT (ret), NULL);
return ret;
}
static DzlShortcutMatch
dzl_shortcut_controller_process (DzlShortcutController *self,
const DzlShortcutChord *chord,
DzlShortcutPhase phase)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
DzlShortcutContext *context;
DzlShortcutMatch match = DZL_SHORTCUT_MATCH_NONE;
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_assert (chord != NULL);
/* Try to activate our current context */
if (match == DZL_SHORTCUT_MATCH_NONE &&
NULL != (context = dzl_shortcut_controller_get_context_for_phase (self, phase)))
match = dzl_shortcut_context_activate (context, priv->widget, chord);
/* If we didn't get a match, locate the context within the parent theme */
if (match == DZL_SHORTCUT_MATCH_NONE &&
NULL != (context = dzl_shortcut_controller_get_inherited_context (self, phase)))
match = dzl_shortcut_context_activate (context, priv->widget, chord);
return match;
}
static void
dzl_shortcut_controller_do_global_chain (DzlShortcutController *self,
DzlShortcutClosureChain *chain,
GtkWidget *widget,
GList *next)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_assert (chain != NULL);
g_assert (GTK_IS_WIDGET (widget));
/*
* If this is an action chain, the best we're going to be able to do is
* activate the action from the current widget. For commands, we can try to
* resolve them by locating commands within our registered controllers.
*/
if (chain->type != DZL_SHORTCUT_CLOSURE_COMMAND)
{
dzl_shortcut_closure_chain_execute (chain, widget);
return;
}
if (priv->commands != NULL &&
g_hash_table_contains (priv->commands, chain->command.name))
{
dzl_shortcut_closure_chain_execute (chain, priv->widget);
return;
}
if (next == NULL)
{
dzl_shortcut_closure_chain_execute (chain, widget);
return;
}
dzl_shortcut_controller_do_global_chain (next->data, chain, widget, next->next);
}
static DzlShortcutMatch
dzl_shortcut_controller_do_global (DzlShortcutController *self,
const DzlShortcutChord *chord,
DzlShortcutPhase phase,
GtkWidget *widget)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
DzlShortcutClosureChain *chain = NULL;
DzlShortcutManager *manager;
DzlShortcutTheme *theme;
DzlShortcutMatch match;
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_assert (chord != NULL);
g_assert ((phase & DZL_SHORTCUT_PHASE_GLOBAL) != 0);
g_assert (GTK_IS_WIDGET (widget));
manager = dzl_shortcut_controller_get_manager (self);
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
theme = dzl_shortcut_manager_get_theme (manager);
g_assert (DZL_IS_SHORTCUT_THEME (theme));
/* See if we have a chain for this chord */
match = _dzl_shortcut_theme_match (theme, phase, chord, &chain);
/* If we matched, execute the chain, trying to locate the proper widget for
* the event delivery.
*/
if (match == DZL_SHORTCUT_MATCH_EQUAL && chain->phase == phase)
dzl_shortcut_controller_do_global_chain (self, chain, widget, priv->descendants.head);
return match;
}
/**
* _dzl_shortcut_controller_handle:
* @self: An #DzlShortcutController
* @event: A #GdkEventKey
* @chord: the current chord for the toplevel
* @phase: the dispatch phase
* @widget: the widget receiving @event
*
* 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.
*
* @chord is used to track the current chord from the toplevel. Chord tracking
* is done in a single place to avoid inconsistencies between controllers.
*
* @phase should indicate the phase of the event dispatch. Capture is used
* to capture events before the destination #GdkWindow can process them, and
* bubble is to allow the destination window to handle it before processing
* the result afterwards if not yet handled.
*
* Returns: A #DzlShortcutMatch based on if the event was dispatched.
*
* Since: 3.26
*/
DzlShortcutMatch
_dzl_shortcut_controller_handle (DzlShortcutController *self,
const GdkEventKey *event,
const DzlShortcutChord *chord,
DzlShortcutPhase phase,
GtkWidget *widget)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
DzlShortcutMatch match = DZL_SHORTCUT_MATCH_NONE;
DZL_ENTRY;
g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), FALSE);
g_return_val_if_fail (event != NULL, FALSE);
g_return_val_if_fail (chord != NULL, FALSE);
g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE);
/* Nothing to do if the widget isn't visible/mapped/etc */
if (priv->widget == NULL ||
!gtk_widget_get_visible (priv->widget) ||
!gtk_widget_get_child_visible (priv->widget) ||
!gtk_widget_is_sensitive (priv->widget))
DZL_RETURN (DZL_SHORTCUT_MATCH_NONE);
DZL_TRACE_MSG ("widget = %s, phase = %d", G_OBJECT_TYPE_NAME (priv->widget), phase);
/* Try to dispatch our capture global shortcuts first */
if (phase == (DZL_SHORTCUT_PHASE_CAPTURE | DZL_SHORTCUT_PHASE_GLOBAL) &&
dzl_shortcut_controller_is_root (self))
match = dzl_shortcut_controller_do_global (self, chord, phase, widget);
/*
* This function processes a particular phase for the event. If our phase
* is DZL_SHORTCUT_PHASE_CAPTURE, that means we are in the process of working
* our way from the toplevel down to the widget containing the event window.
*
* If our phase is DZL_SHORTCUT_PHASE_BUBBLE, then we are working our way
* up from the widget containing the event window to the toplevel. This is
* the phase where most activations should occur.
*
* During the capture phase, we look for a context matching the current
* context, but with a suffix on the context name like ":capture". So for
* the default GtkEntry, the capture context name would be something like
* "GtkEntry:capture". The bubble phase does not have a suffix.
*
* Toplevel Global Capture Accels
* Toplevel Capture
* - Child 1 Capture
* - Grandchild 1 Capture
* - Grandchild 1 Bubble
* - Child 1 Bubble
* Toplevel Bubble
* Toplevel Global Bubble Accels
*
* If we come across a keybinding that is a partial match, we assume that
* is the closest match in the dispatch chain and stop processing further.
* Overlapping and conflicting keybindings are considered undefined behavior
* and this falls under such a situation.
*
* Note that we do not perform the bubble/capture phase here, that is handled
* by our caller in DzlShortcutManager.
*/
if (match == DZL_SHORTCUT_MATCH_NONE)
match = dzl_shortcut_controller_process (self, chord, phase);
/* Try to dispatch our capture global shortcuts first */
if (match == DZL_SHORTCUT_MATCH_NONE &&
dzl_shortcut_controller_is_root (self) &&
phase == (DZL_SHORTCUT_PHASE_BUBBLE | DZL_SHORTCUT_PHASE_GLOBAL))
match = dzl_shortcut_controller_do_global (self, chord, phase, widget);
DZL_TRACE_MSG ("match = %s",
match == DZL_SHORTCUT_MATCH_NONE ? "none" :
match == DZL_SHORTCUT_MATCH_PARTIAL ? "partial" : "equal");
DZL_RETURN (match);
}
/**
* dzl_shortcut_controller_get_current_chord:
* @self: a #DzlShortcutController
*
* This method gets the #DzlShortcutController:current-chord property.
* This is useful if you want to monitor in-progress chord building.
*
* Note that this value will only be valid on the controller for the
* toplevel widget (a #GtkWindow). Chords are not tracked at the
* individual widget controller level.
*
* Returns: (transfer none) (nullable): A #DzlShortcutChord or %NULL.
*/
const DzlShortcutChord *
dzl_shortcut_controller_get_current_chord (DzlShortcutController *self)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), NULL);
return priv->current_chord;
}
/**
* dzl_shortcut_controller_execute_command:
* @self: a #DzlShortcutController
* @command: the id of the command
*
* This method will locate and execute the command matching the id @command.
*
* If the command is not found, %FALSE is returned.
*
* Returns: %TRUE if the command was found and executed.
*/
gboolean
dzl_shortcut_controller_execute_command (DzlShortcutController *self,
const gchar *command)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), FALSE);
g_return_val_if_fail (command != NULL, FALSE);
if (priv->commands != NULL)
{
DzlShortcutClosureChain *chain;
chain = g_hash_table_lookup (priv->commands, g_intern_string (command));
if (chain != NULL)
return dzl_shortcut_closure_chain_execute (chain, priv->widget);
}
for (const GList *iter = priv->descendants.head; iter != NULL; iter = iter->next)
{
DzlShortcutController *descendant = iter->data;
if (dzl_shortcut_controller_execute_command (descendant, command))
return TRUE;
}
return FALSE;
}
static void
dzl_shortcut_controller_add_command (DzlShortcutController *self,
const gchar *command_id,
const gchar *default_accel,
DzlShortcutPhase phase,
DzlShortcutClosureChain *chain)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_autoptr(DzlShortcutChord) chord = NULL;
DzlShortcutManager *manager;
DzlShortcutTheme *theme;
g_assert (DZL_IS_SHORTCUT_CONTROLLER (self));
g_assert (command_id != NULL);
g_assert (chain != NULL);
/* Always use interned strings for command ids */
command_id = g_intern_string (command_id);
/*
* Set the phase on the closure chain so we know what phase we are allowed
* to execute the chain within during capture/dispatch/bubble. There is no
* "global + dispatch" phase, so if global is set, default to bubble.
*/
if (phase == DZL_SHORTCUT_PHASE_GLOBAL)
phase |= DZL_SHORTCUT_PHASE_BUBBLE;
chain->phase = phase;
/* Add the closure chain to our set of commands. */
if (priv->commands == NULL)
priv->commands = g_hash_table_new_full (NULL, NULL, NULL,
(GDestroyNotify)dzl_shortcut_closure_chain_free);
g_hash_table_insert (priv->commands, (gpointer)command_id, chain);
/*
* If this command can be executed in the global phase, we need to be
* sure that the root controller knows that we must be checked during
* global activation checks.
*/
if ((phase & DZL_SHORTCUT_PHASE_GLOBAL) != 0)
{
if (priv->have_global != TRUE)
{
priv->have_global = TRUE;
if (priv->widget != NULL)
dzl_shortcut_controller_widget_hierarchy_changed (self, NULL, priv->widget);
}
}
/* If an accel was provided, we need to register it in various places */
if (default_accel != NULL)
{
/* Make sure this is a valid accelerator */
chord = dzl_shortcut_chord_new_from_string (default_accel);
if (chord != NULL)
{
DzlShortcutContext *context;
/* Add the chord to our chord table for lookups */
if (priv->commands_table == NULL)
priv->commands_table = dzl_shortcut_chord_table_new ();
dzl_shortcut_chord_table_add (priv->commands_table, chord, (gpointer)command_id);
/* Set the value in the theme so it can have overrides by users */
manager = dzl_shortcut_controller_get_manager (self);
theme = _dzl_shortcut_manager_get_internal_theme (manager);
dzl_shortcut_theme_set_chord_for_command (theme, command_id, chord, phase);
/* Hook things up into the default context */
context = _dzl_shortcut_theme_find_default_context_with_phase (theme, priv->widget, phase);
if (!_dzl_shortcut_context_contains (context, chord))
dzl_shortcut_context_add_command (context, default_accel, command_id);
}
else
g_warning ("\"%s\" is not a valid accelerator chord", default_accel);
}
}
void
dzl_shortcut_controller_add_command_action (DzlShortcutController *self,
const gchar *command_id,
const gchar *default_accel,
DzlShortcutPhase phase,
const gchar *action)
{
DzlShortcutClosureChain *chain;
g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self));
g_return_if_fail (command_id != NULL);
chain = dzl_shortcut_closure_chain_append_action_string (NULL, action);
dzl_shortcut_controller_add_command (self, command_id, default_accel, phase, chain);
}
void
dzl_shortcut_controller_add_command_callback (DzlShortcutController *self,
const gchar *command_id,
const gchar *default_accel,
DzlShortcutPhase phase,
GtkCallback callback,
gpointer callback_data,
GDestroyNotify callback_data_destroy)
{
DzlShortcutClosureChain *chain;
g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self));
g_return_if_fail (command_id != NULL);
chain = dzl_shortcut_closure_chain_append_callback (NULL,
callback,
callback_data,
callback_data_destroy);
dzl_shortcut_controller_add_command (self, command_id, default_accel, phase, chain);
}
void
dzl_shortcut_controller_add_command_signal (DzlShortcutController *self,
const gchar *command_id,
const gchar *default_accel,
DzlShortcutPhase phase,
const gchar *signal_name,
guint n_args,
...)
{
DzlShortcutClosureChain *chain;
va_list args;
g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self));
g_return_if_fail (command_id != NULL);
va_start (args, n_args);
chain = dzl_shortcut_closure_chain_append_signal (NULL, signal_name, n_args, args);
va_end (args);
dzl_shortcut_controller_add_command (self, command_id, default_accel, phase, chain);
}
DzlShortcutChord *
_dzl_shortcut_controller_push (DzlShortcutController *self,
const GdkEventKey *event)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_return_val_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self), NULL);
g_return_val_if_fail (event != NULL, NULL);
/*
* Only the toplevel controller handling the event needs to determine
* the current "chord" as that state lives in the root controller only.
*
* So our first step is to determine the current chord, or if this input
* breaks further chord processing.
*
* We will use these chords during capture/dispatch/bubble later on.
*/
if (priv->current_chord == NULL)
{
/* Try to create a new chord starting with this key.
* current_chord may still be NULL after this.
*/
priv->current_chord = dzl_shortcut_chord_new_from_event (event);
}
else
{
if (!dzl_shortcut_chord_append_event (priv->current_chord, event))
{
/* Failed to add the key to the chord, cancel */
_dzl_shortcut_controller_clear (self);
return NULL;
}
}
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_CHORD]);
return dzl_shortcut_chord_copy (priv->current_chord);
}
void
_dzl_shortcut_controller_clear (DzlShortcutController *self)
{
DzlShortcutControllerPrivate *priv = dzl_shortcut_controller_get_instance_private (self);
g_return_if_fail (DZL_IS_SHORTCUT_CONTROLLER (self));
g_clear_pointer (&priv->current_chord, dzl_shortcut_chord_free);
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_CHORD]);
}