[gnome-builder/wip/gtk4-port: 860/1774] libide/gui: create GtkShortcut from keybindings bundles




commit c82d9874665172ebf52c8264845c416c4299773a
Author: Christian Hergert <chergert redhat com>
Date:   Tue May 3 14:00:32 2022 -0700

    libide/gui: create GtkShortcut from keybindings bundles
    
    This still needs testing as it has only been compile tested. But the goal
    is that we will parse JSON files of keybinding descriptions into a bundle
    (which is a GListModel of GtkShortcut). We use the template-glib based
    TmplExpr for "when" to determine if we can activate the
    GtkShortcutAction from a GtkCallbackAction.
    
    The GtkCallbackAction's user_data contains the phase, which we might want
    to use at some point to get filtered GListModel by propagation phase for
    the GtkShortcutController.

 meson.build                           |   2 +-
 src/libide/gui/ide-shortcut-bundle.c  | 245 ++++++++++++++++++++++++++++++----
 src/libide/gui/ide-shortcut-private.h |  31 -----
 src/libide/gui/ide-shortcut.c         |  92 -------------
 src/libide/gui/meson.build            |   2 -
 5 files changed, 221 insertions(+), 151 deletions(-)
---
diff --git a/meson.build b/meson.build
index 4ee62a05a..b678b6967 100644
--- a/meson.build
+++ b/meson.build
@@ -294,7 +294,7 @@ libjsonrpc_glib_dep = dependency('jsonrpc-glib-1.0', version: '>= 3.42.0')
 libm_dep = cc.find_library('m', required: false)
 libpeas_dep = dependency('libpeas-1.0', version: '>= 1.32.0')
 libportal_dep = dependency('libportal-gtk4', required: false)
-libtemplate_glib_dep = dependency('template-glib-1.0', version: '>= 3.28.0')
+libtemplate_glib_dep = dependency('template-glib-1.0', version: '>= 3.35.0')
 libvte_dep = dependency('vte-2.91-gtk4', version: '>= 0.69.0')
 libxml2_dep = dependency('libxml-2.0', version: '>= 2.9.0')
 
diff --git a/src/libide/gui/ide-shortcut-bundle.c b/src/libide/gui/ide-shortcut-bundle.c
index 5efd5438a..128f3675e 100644
--- a/src/libide/gui/ide-shortcut-bundle.c
+++ b/src/libide/gui/ide-shortcut-bundle.c
@@ -22,10 +22,14 @@
 
 #include "config.h"
 
+#include <gtk/gtk.h>
 #include <json-glib/json-glib.h>
+#include <tmpl-glib.h>
 
+#include "ide-gui-global.h"
 #include "ide-shortcut-bundle-private.h"
-#include "ide-shortcut-private.h"
+#include "ide-workbench.h"
+#include "ide-workspace.h"
 
 struct _IdeShortcutBundle
 {
@@ -35,17 +39,111 @@ struct _IdeShortcutBundle
 
 typedef struct
 {
-  const char *key;
-  const char *when;
-  const char *command;
-  const char *action;
-  const char *phase;
-} Shortcut;
+  TmplExpr            *when;
+  GVariant            *args;
+  GtkShortcutAction   *action;
+  GtkPropagationPhase  phase;
+} IdeShortcut;
+
+static IdeShortcut *
+ide_shortcut_new (const char          *action,
+                  GVariant            *args,
+                  TmplExpr            *when,
+                  GtkPropagationPhase  phase)
+{
+  IdeShortcut *ret;
+
+  g_assert (action != NULL);
+  g_assert (phase == GTK_PHASE_CAPTURE || phase == GTK_PHASE_BUBBLE);
+
+  ret = g_slice_new0 (IdeShortcut);
+  ret->action = gtk_named_action_new (action);
+  ret->args = args ? g_variant_ref (args) : NULL;
+  ret->when = when ? tmpl_expr_ref (when) : NULL;
+  ret->phase = phase;
+
+  return ret;
+}
+
+static void
+ide_shortcut_free (IdeShortcut *shortcut)
+{
+  g_clear_pointer (&shortcut->when, tmpl_expr_unref);
+  g_clear_pointer (&shortcut->args, g_variant_unref);
+  g_clear_object (&shortcut->action);
+  shortcut->phase = 0;
+  g_slice_free (IdeShortcut, shortcut);
+}
+
+static void
+set_object (TmplScope  *scope,
+            const char *name,
+            GType       type,
+            gpointer    object)
+{
+  g_auto(GValue) value = G_VALUE_INIT;
+
+  g_value_init (&value, type);
+  g_value_set_object (&value, object);
+  tmpl_scope_set_value (scope, name, &value);
+}
+
+static gboolean
+ide_shortcut_activate (GtkWidget *widget,
+                       GVariant  *args,
+                       gpointer   user_data)
+{
+  IdeShortcut *shortcut = user_data;
+
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (shortcut != NULL);
+
+  if (shortcut->when != NULL)
+    {
+      g_auto(GValue) enabled = G_VALUE_INIT;
+      g_autoptr(TmplScope) scope = tmpl_scope_new ();
+      IdeWorkspace *workspace = ide_widget_get_workspace (widget);
+      IdeWorkbench *workbench = ide_widget_get_workbench (widget);
+      IdePage *page = workspace ? ide_workspace_get_most_recent_page (workspace) : NULL;
+
+      set_object (scope, "focus", GTK_TYPE_WIDGET, widget);
+      set_object (scope, "workbench", IDE_TYPE_WORKBENCH, workbench);
+      set_object (scope, "workspace", IDE_TYPE_WORKSPACE, workspace);
+      set_object (scope, "page", IDE_TYPE_PAGE, page);
+
+      if (!tmpl_expr_eval (shortcut->when, scope, &enabled, NULL))
+        return FALSE;
+
+      if (!G_VALUE_HOLDS_BOOLEAN (&enabled))
+        {
+          GValue as_bool = G_VALUE_INIT;
+
+          g_value_init (&as_bool, G_TYPE_BOOLEAN);
+          if (!g_value_transform (&enabled, &as_bool))
+            return FALSE;
+
+          g_value_unset (&enabled);
+          enabled = as_bool;
+        }
+
+      g_assert (G_VALUE_HOLDS_BOOLEAN (&enabled));
+
+      if (!g_value_get_boolean (&enabled))
+        return FALSE;
+    }
+
+  return gtk_shortcut_action_activate (shortcut->action,
+                                       GTK_SHORTCUT_ACTION_EXCLUSIVE,
+                                       widget,
+                                       shortcut->args);
+}
 
 static guint
 ide_shortcut_bundle_get_n_items (GListModel *model)
 {
-  return IDE_SHORTCUT_BUNDLE (model)->items->len;
+  IdeShortcutBundle *self = IDE_SHORTCUT_BUNDLE (model);
+
+  return self->items ? self->items->len : 0;
 }
 
 static gpointer
@@ -54,16 +152,16 @@ ide_shortcut_bundle_get_item (GListModel *model,
 {
   IdeShortcutBundle *self = IDE_SHORTCUT_BUNDLE (model);
 
-  if (position >= self->items->len)
+  if (self->items == NULL || position >= self->items->len)
     return NULL;
 
-  return NULL;
+  return g_object_ref (g_ptr_array_index (self->items, position));
 }
 
 static GType
 ide_shortcut_bundle_get_item_type (GListModel *model)
 {
-  return IDE_TYPE_SHORTCUT;
+  return GTK_TYPE_SHORTCUT;
 }
 
 static void
@@ -77,15 +175,13 @@ list_model_iface_init (GListModelInterface *iface)
 G_DEFINE_FINAL_TYPE_WITH_CODE (IdeShortcutBundle, ide_shortcut_bundle, G_TYPE_OBJECT,
                                G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
 
-static void
-shortcut_free (gpointer data)
-{
-  g_slice_free (Shortcut, data);
-}
-
 static void
 ide_shortcut_bundle_dispose (GObject *object)
 {
+  IdeShortcutBundle *self = (IdeShortcutBundle *)object;
+
+  g_clear_pointer (&self->items, g_ptr_array_unref);
+
   G_OBJECT_CLASS (ide_shortcut_bundle_parent_class)->dispose (object);
 }
 
@@ -100,7 +196,7 @@ ide_shortcut_bundle_class_init (IdeShortcutBundleClass *klass)
 static void
 ide_shortcut_bundle_init (IdeShortcutBundle *self)
 {
-  self->items = g_ptr_array_new_with_free_func (shortcut_free);
+  self->items = g_ptr_array_new_with_free_func (g_object_unref);
 }
 
 IdeShortcutBundle *
@@ -152,13 +248,42 @@ get_string_member (JsonObject  *obj,
   return TRUE;
 }
 
+static gboolean
+parse_phase (const char          *str,
+             GtkPropagationPhase *phase)
+{
+  if (str == NULL || str[0] == 0 || strcasecmp ("capture", str) == 0)
+    {
+      *phase = GTK_PHASE_CAPTURE;
+      return TRUE;
+    }
+  else if (strcasecmp ("bubble", str) == 0)
+    {
+      *phase = GTK_PHASE_BUBBLE;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
 static gboolean
 populate_from_object (IdeShortcutBundle  *self,
                       JsonNode           *node,
                       GError            **error)
 {
+  g_autoptr(GtkShortcutTrigger) trigger = NULL;
+  g_autoptr(GtkShortcutAction) callback = NULL;
+  g_autoptr(GtkShortcut) shortcut = NULL;
+  g_autoptr(TmplExpr) when = NULL;
+  g_autoptr(GVariant) args = NULL;
+  const char *trigger_str = NULL;
+  const char *when_str = NULL;
+  const char *args_str = NULL;
+  const char *phase_str = NULL;
+  const char *command = NULL;
+  const char *action = NULL;
+  GtkPropagationPhase phase = 0;
   JsonObject *obj;
-  Shortcut shortcut = {0};
 
   g_assert (IDE_IS_SHORTCUT_BUNDLE (self));
   g_assert (node != NULL);
@@ -166,14 +291,84 @@ populate_from_object (IdeShortcutBundle  *self,
 
   obj = json_node_get_object (node);
 
-  if (!get_string_member (obj, "key", &shortcut.key, error) ||
-      !get_string_member (obj, "when", &shortcut.when, error) ||
-      !get_string_member (obj, "command", &shortcut.command, error) ||
-      !get_string_member (obj, "action", &shortcut.action, error) ||
-      !get_string_member (obj, "phase", &shortcut.phase, error))
+  /* TODO: We might want to add title/description so that our internal
+   *       keybindings can be displayed to the user from global search
+   *       with more than just a command name and/or arguments.
+   */
+
+  if (!get_string_member (obj, "trigger", &trigger_str, error) ||
+      !get_string_member (obj, "when", &when_str, error) ||
+      !get_string_member (obj, "args", &args_str, error) ||
+      !get_string_member (obj, "command", &command, error) ||
+      !get_string_member (obj, "action", &action, error) ||
+      !get_string_member (obj, "phase", &phase_str, error))
     return FALSE;
 
-  g_ptr_array_add (self->items, g_slice_dup (Shortcut, &shortcut));
+  if (!(trigger = gtk_shortcut_trigger_parse_string (trigger_str)))
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_DATA,
+                   "Failed to parse shortcut trigger: \"%s\"",
+                   trigger_str);
+      return FALSE;
+    }
+
+  if (!ide_str_empty0 (command) && !ide_str_empty0 (action))
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_DATA,
+                   "Cannot specify both \"command\" and \"action\" (\"%s\" and \"%s\")",
+                   command, action);
+      return FALSE;
+    }
+
+  if (!ide_str_empty0 (args_str))
+    {
+      /* Parse args from string into GVariant if any */
+      if (!(args = g_variant_parse (NULL, args_str, NULL, NULL, error)))
+        return FALSE;
+    }
+
+  if (!ide_str_empty0 (command))
+    {
+      GVariantBuilder builder;
+
+      g_variant_builder_init (&builder, G_VARIANT_TYPE ("(smv)"));
+      g_variant_builder_add (&builder, "s", command);
+      g_variant_builder_open (&builder, G_VARIANT_TYPE ("mv"));
+      if (args != NULL)
+        g_variant_builder_add_value (&builder, args);
+      g_variant_builder_close (&builder);
+
+      g_clear_pointer (&args, g_variant_unref);
+      args = g_variant_builder_end (&builder);
+      action = "workbench.command";
+    }
+
+  if (!ide_str_empty0 (when_str))
+    {
+      if (!(when = tmpl_expr_from_string (when_str, error)))
+        return FALSE;
+    }
+
+  if (!parse_phase (phase_str, &phase))
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_DATA,
+                   "Unknown phase \"%s\"",
+                   phase_str);
+      return FALSE;
+    }
+
+  callback = gtk_callback_action_new (ide_shortcut_activate,
+                                      ide_shortcut_new (action, args, when, phase),
+                                      (GDestroyNotify) ide_shortcut_free);
+  shortcut = gtk_shortcut_new (g_steal_pointer (&trigger),
+                               g_steal_pointer (&callback));
+  g_ptr_array_add (self->items, g_steal_pointer (&shortcut));
 
   return TRUE;
 }
diff --git a/src/libide/gui/meson.build b/src/libide/gui/meson.build
index 7736bbee2..f96003d95 100644
--- a/src/libide/gui/meson.build
+++ b/src/libide/gui/meson.build
@@ -54,7 +54,6 @@ libide_gui_private_headers = [
   'ide-recoloring-private.h',
   'ide-search-popover-private.h',
   'ide-session-private.h',
-  'ide-shortcut-private.h',
   'ide-shortcut-bundle-private.h',
   'ide-shortcut-model-private.h',
   'ide-shortcut-controller-private.h',
@@ -75,7 +74,6 @@ libide_gui_private_sources = [
   'ide-recoloring.c',
   'ide-search-popover.c',
   'ide-session.c',
-  'ide-shortcut.c',
   'ide-shortcut-bundle.c',
   'ide-shortcut-model.c',
   'ide-shortcut-controller.c',


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