How to realize overlapping keybindings for different modes of an application?

I am developing an application with several tabs which have different operation modes. I use GTK3.

What makes things compicated is

  • There can be several tabs of one type
  • Some widgets inside tabs (they are subclassed from GtkDrawingArea) manually handle mouse and keyboard events. However, this is not much of a problem.
  • Wigets in these tabs can also have different modes.

For example, one of the tabs is map view. It allows measurements and drawing. I want to use keybindings both to switch between modes and within modes. Like this:

MapTab1
├ <Ctrl>M - measurement mode
└ <Ctrl>D - drawing mode
     ├  <Esc> -   leave this mode
     ├  <Ctrl>M - draw a square
     └  <Ctrl>O - draw a circle 
MapTab2
├ <Ctrl>M - measurement mode
└ <Ctrl>D - drawing mode
     ├  <Esc> -   leave this mode
     ├  <Ctrl>M - draw a square
     └  <Ctrl>O - draw a circle 

Another Tab
└ <Ctrl>D - destroying all humans mode

As you can see, some keybinding may overlap. To resolve these conflicts I thought of several approaches:

1. Dynamically add/remove actions and keybindings when a tab switches and mode entered.
Keybindings would never conflict, because they are automatically added/removed.

static GActionEntry tab_entries[] = {
	{ "draw_mode", enter_draw, NULL, NULL, NULL },
	{ "measure_mode", enter_measure, NULL, NULL, NULL },
	{ "zzz", action_z, NULL, NULL, NULL },
};

static tab_keybinding[] = {
	{"draw_mode", {"<Ctrl>D", NULL}},
	{"measure_mode", {"<Ctrl>M", NULL}},
  }

static GActionEntry draw_mode_entries[] = {
	{ "circle", action_circle, NULL, NULL, NULL },
	{ "square", action_square, NULL, NULL, NULL },
};

static draw_mode_keybinding[] = {
	{"circle", {"<Ctrl>M", NULL}},
	{"square", {"<Ctrl>O", NULL}},
}

void tab_leave (MyTabWidget *self, GtkApplicationWindow *app_window)
{
	GApplication * app = g_application_get_default ();
	for (i = 0; i < G_N_ELEMENTS (entries); ++i)
		g_action_map_remove_action (G_ACTION_MAP (app_window), entries[i].name)

	for (i = 0; i < G_N_ELEMENTS (keybinding); ++i)
		gtk_application_set_accels_for_action (app, keybinding[i].name, NULL);
}

void tab_enter (MyTabWidget *self, GtkApplicationWindow *app_window)
{
	g_action_map_add_action_entries (G_ACTION_MAP (app_window),
	                                 entries, G_N_ELEMENTS (entries),
	                                 self);
	for (i = 0; i < G_N_ELEMENTS (keybinding); ++i)
		gtk_application_set_accels_for_action (app, keybinding[i].name, keybinding[i].accels);
}

// Plus mode_leave and mode_enter methods called at entering modes

2. Make every tab prefix it’s actions with some id. Dynamically enable/disable actions.
Action names never conflict. Keybindings never conflict, as actions get automatically enabled/disabled.

void tab_created (MyTabWidget *self, GtkApplicationWindow *app_window)
{
	gchar *prefix = my_tab_get_prefix (self);
	gchar *name;
	GActionEntry *prefixed_entries;

			
	// For the purpose of simplicity I omit dynamic renaming of entries.
	self->priv->prefixed_entries = my_tab_get_prefixed_entries (self, entries, prefix);
	g_action_map_add_action_entries (G_ACTION_MAP (app_window),
	                                 self->priv->prefixed_entries,
	                                 G_N_ELEMENTS (entries), self);

	for (i = 0; i < G_N_ELEMENTS (keybinding); ++i)
		{
			name = g_strdup_printf ("%s%s", prefix, keybinding[i].name);
			gtk_application_set_accels_for_action (app, name, keybinding[i].accels);
		}

	// Disable all actions by default.
	tab_leave(self, app_window);
}

void tab_leave (MyTabWidget *self, GtkApplicationWindow *app_window)
{
	for (i = 0; i < G_N_ELEMENTS (entries); ++i)
		g_simple_action_set_enabled (self->priv->prefixed_entries[i].name, FALSE);
}

void tab_enter (MyTabWidget *self, GtkApplicationWindow *app_window)
{
	for (i = 0; i < G_N_ELEMENTS (entries); ++i)
		g_simple_action_set_enabled (self->priv->prefixed_entries[i].name, TRUE);
}

// Plus mode_leave and mode_enter methods called at entering modes

3. Have keybinding only for entering modes and continue handling key/mouse events manually.
I don’t see any pros. Possible keybinding conflicts must be resolved at compile-time and changing keybings is very complicated. Looks like it’s a bad approach.

The final questions are:

  1. Are there any pros/cons of the first two approaches which I don’t see?
  2. Are there simpler ways to achieve what I need?

I’ve decided to use 2nd approach and it works just fine. Sample code below.

#include "my-tab.h"

enum
{
  PROP_0,
  PROP_AW
};

struct _MyTabPrivate
{
  GtkApplicationWindow *app_window;

  gchar        *prefix;
  GActionEntry *prefixed_actions;
  GHashTable   *mode_action; /* {static gchar * mode_name : gchar** action_list} */

};

static void    my_tab_set_property             (GObject       *object,
                                                guint          prop_id,
                                                const GValue  *value,
                                                GParamSpec    *pspec);
static void    my_tab_object_constructed       (GObject       *object);

static void    my_tab_mode1_foo                (GSimpleAction *action,
                                                GVariant      *parameter,
                                                gpointer       _self);
static void    my_tab_mode1_bar                (GSimpleAction *action,
                                                GVariant      *parameter,
                                                gpointer       _self);
static void    my_tab_mode2_baz                (GSimpleAction *action,
                                                GVariant      *parameter,
                                                gpointer       _self);
static void    my_tab_action_switcher          (MyTab     *self,
                                                const gchar   *mode,
                                                gboolean       _enabled);
static void    my_tab_mode_enter               (GSimpleAction *action,
                                                GVariant      *parameter,
                                                gpointer       _self);

G_DEFINE_TYPE_WITH_PRIVATE (MyTab, my_tab, GTK_TYPE_BIN);

/* All actions of this tab. 
 * "main" is main mode, 
 * "exit" is a special action to exit to main from other modes. */
static GActionEntry actions[] =
{
  {"main",      my_tab_mode_enter, "s", "''", NULL},
  {"exit",      my_tab_mode_enter, "s", "''", NULL},
  {"mode1_foo", my_tab_mode1_foo, "s", "''", NULL},
  {"mode1_bar", my_tab_mode1_bar, NULL, NULL, NULL},
  {"mode2_baz", my_tab_mode2_baz, "s", "''", NULL},
};

/* All accelerators of this tab. 
 *
 * There are actually 3 modes: "main", "mode1" and "mode2". */
static gchar *keys[] =
{
  "main::mode1",     "<Ctrl>a", NULL,
  "main::mode2",     "<Ctrl>s", NULL,
  "exit::main",      "<Ctrl>w",  NULL,

  "mode1_foo::1",    "<Ctrl>a", NULL,
  "mode1_foo::2",    "<Ctrl>s", NULL,
  "mode1_bar",       "<Ctrl>d", NULL,

  "mode2_baz::3",    "<Ctrl>a", NULL,
  "mode2_baz::4",    "<Ctrl>s", NULL,
  NULL,
};

/* Modes and their actions. */
static gchar * mode_actions[] =
{
  "main", "main", NULL,
  "mode1", "exit", "mode1_foo", "mode1_bar", NULL,
  "mode2", "exit", "mode2_baz", NULL,
  NULL,
};

static void
my_tab_class_init (MyTabClass *klass)
{
  GObjectClass *oclass = G_OBJECT_CLASS (klass);

  oclass->set_property = my_tab_set_property;
  oclass->constructed = my_tab_object_constructed;

  g_object_class_install_property (oclass, PROP_AW,
    g_param_spec_object ("win", "win", "win", GTK_TYPE_APPLICATION_WINDOW,
                         G_PARAM_WRITABLE | G_PARAM_CONSTRUCT));
}

static inline gchar *
my_tab_prefix_string (MyTab   *self,
                          const gchar *string)
{
  return string == NULL ? NULL : g_strdup_printf ("%s%s", self->priv->prefix, string);
}

static uint tab_count = 0;

static void
my_tab_init (MyTab *self)
{
  guint i;
  MyTabPrivate *priv;
  const gchar **it;

  priv = my_tab_get_instance_private (self);
  self->priv = priv;

  priv->prefix = g_strdup_printf("tab%u_", tab_count++);

  /* All tab's actions. Generate prefixed actions. */
  priv->prefixed_actions = g_malloc0 (sizeof (GActionEntry) * G_N_ELEMENTS (actions));
  for (i = 0; i < G_N_ELEMENTS (actions); ++i)
    {
      priv->prefixed_actions[i] = actions[i];
      priv->prefixed_actions[i].name = my_tab_prefix_string (self, actions[i].name);
    }

  /* Dict of modes : their actions. Needed to dynamically enable and disable actions.  */
  priv->mode_action = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)g_strfreev);

  for (it = mode_actions; it[0] != NULL;)
    {
      gchar *mode = it[0];
      gchar **acts;
      guint i, n_actions = g_strv_length ((gchar**)it)+1;

      acts = g_malloc0 (sizeof (gchar*) * n_actions);
      for (i = 0; i < n_actions; ++i)
        acts[i] = my_tab_prefix_string (self, it[i + 1]);

      g_hash_table_insert (priv->mode_action, mode, acts);

      it += n_actions;
    }
}

static void
my_tab_set_property (GObject      *object,
                     guint         prop_id,
                     const GValue *value,
                     GParamSpec   *pspec)
{
  MyTab *self = HYSCAN_TAB (object);
  MyTabPrivate *priv = self->priv;

  if (prop_id == PROP_AW):
    priv->app_window = g_value_dup_object (value);
  else
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
 
}

static void
my_tab_object_constructed (GObject *object)
{
  MyTab *self = HYSCAN_TAB (object);
  MyTabPrivate *priv = self->priv;
  GtkApplication *app = GTK_APPLICATION (g_application_get_default ());
  gchar *name;
  gchar **it;

  G_OBJECT_CLASS (my_tab_parent_class)->constructed (object);

  /* Add all accels to GtkApplicationWindow. Note:
   * 1. window's actions are prefixed with "win." 
   * 2. our actions are prefixed with, well, unique prefix. */
  g_action_map_add_action_entries (G_ACTION_MAP (priv->app_window),
                                   priv->prefixed_actions,
                                   G_N_ELEMENTS (actions), self);

  for (it = keys; it[0]; it += g_strv_length ((gchar **)it) + 1)
   {
      name = g_strdup_printf ("win.%s%s", priv->prefix, it[0]);
      gtk_application_set_accels_for_action (app, name, &it[1]);
      g_free (name);
   }

  /* Disable all actions by default. They will be enabled when tab is entered. */
  my_tab_action_switcher (self, "main", TRUE);
}


static void
my_tab_mode1_foo (GSimpleAction *action,
                      GVariant      *parameter,
                      gpointer       _self)
{
  MyTab *self = HYSCAN_TAB (_self);
  const gchar *param = g_variant_get_string (parameter, NULL);

  g_message ("%s mode1_foo: %s", self->priv->prefix, param);
}

static void
my_tab_mode1_bar (GSimpleAction *action,
                      GVariant      *parameter,
                      gpointer       _self)
{
  MyTab *self = HYSCAN_TAB (_self);

  g_message ("%s mode1_bar", self->priv->prefix);
}

static void
my_tab_mode2_baz (GSimpleAction *action,
                      GVariant      *parameter,
                      gpointer       _self)
{
  MyTab *self = HYSCAN_TAB (_self);
  const gchar *param = g_variant_get_string (parameter, NULL);

  g_message ("%s mode2_baz: %s", self->priv->prefix, param);
}

/* Helper to disable all actions and optionally enable current mode's actions. */
static void
my_tab_action_switcher (MyTab   *self,
                            const gchar *mode,
                            gboolean     enabled)
{
  GActionMap *map = G_ACTION_MAP (self->priv->app_window);
  GHashTableIter iter;
  gchar *k;
  gchar **v;

  g_hash_table_iter_init (&iter, self->priv->mode_action);
  while (g_hash_table_iter_next (&iter, (gpointer)&k, (gpointer)&v))
    {
      for (; *v != NULL; ++v)
        {
          GSimpleAction *act = g_action_map_lookup_action (map, *v);
          g_simple_action_set_enabled (act, FALSE);
        }
    }

  if (mode == NULL || !enabled)
    return;

  for (v = g_hash_table_lookup (self->priv->mode_action, mode); *v != NULL; ++v)
    {
      GSimpleAction *act = g_action_map_lookup_action (map, *v);
      g_simple_action_set_enabled (act, TRUE);
      g_message ("Enabled: %s", *v);
    }
};


void
my_tab_mode_enter (GSimpleAction *action,
                       GVariant      *parameter,
                       gpointer       _self)
{
  MyTab *self = HYSCAN_TAB (_self);
  const gchar *mode = g_variant_get_string (parameter, NULL);

  g_message ("Enter: <%s>", mode);

  if (mode == NULL)
    mode = "main";

  my_tab_action_switcher (self, mode, TRUE);
}

MyTab *
my_tab_new (GtkApplicationWindow *win)
{
  return g_object_new (HYSCAN_TYPE_TAB,
                       "win", win,
                       NULL);
}

/* To be called by parent container when a tab appears on the screen. */
void
my_tab_enter (MyTab *self)
{
  my_tab_action_switcher (self, "main", TRUE);
}

/* To be called by parent container when a tab is no longer visible. */
void
my_tab_leave (MyTab *self)
{
  my_tab_action_switcher (self, NULL, FALSE);
}

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.