[gnome-builder] gui: Add support for session autosaving



commit 287ae8af35d5bad1a6acfdb457f139069ad44816
Author: vanadiae <vanadiae35 gmail com>
Date:   Mon Jul 12 22:16:56 2021 +0200

    gui: Add support for session autosaving
    
    Currently if Builder crashes then all the pages opening/closing/moving
    and any state a page had modified will be lost entirely, because saving
    is only done when closing the primary workspace, which obviously
    doesn't happen cleanly when it crashes.
    
    So to avoid this big issue, this commit adds support for automatically
    saving the grid's session state whenever a page is added/closed/moved,
    or one of the worthy page properties changes, as defined by its session
    addin.
    
    So now Builder should be more reliable to such crashes, or at least
    at least the last five minutes of work on the primary workspace will
    be saved. It makes crashes less disruptive when working on a project.

 doc/help/plugins/session.rst       |   3 +
 src/libide/gui/ide-session-addin.c |  35 ++++++
 src/libide/gui/ide-session-addin.h |  39 +++----
 src/libide/gui/ide-session.c       | 211 ++++++++++++++++++++++++++++++++-----
 4 files changed, 242 insertions(+), 46 deletions(-)
---
diff --git a/doc/help/plugins/session.rst b/doc/help/plugins/session.rst
index 43072801d..fc340aefa 100644
--- a/doc/help/plugins/session.rst
+++ b/doc/help/plugins/session.rst
@@ -17,6 +17,9 @@ The `Ide.SessionAddin` allows for saving and restoring state of an `Ide.Page` wh
 
    class MySessionAddin(Ide.Object, Ide.SessionAddin):
 
+       def get_autosave_properties(self):
+           return ['uri', 'current-directory']
+
        def can_save_page(self, page):
            return issubclass(page, My.CustomPage)
 
diff --git a/src/libide/gui/ide-session-addin.c b/src/libide/gui/ide-session-addin.c
index 9c97ae002..b64f4f6aa 100644
--- a/src/libide/gui/ide-session-addin.c
+++ b/src/libide/gui/ide-session-addin.c
@@ -77,6 +77,12 @@ ide_session_addin_real_can_save_page (IdeSessionAddin *self,
   return FALSE;
 }
 
+static char **
+ide_session_addin_real_get_autosave_properties (IdeSessionAddin *self)
+{
+  return NULL;
+}
+
 static void
 ide_session_addin_default_init (IdeSessionAddinInterface *iface)
 {
@@ -85,6 +91,7 @@ ide_session_addin_default_init (IdeSessionAddinInterface *iface)
   iface->restore_page_async = ide_session_addin_real_restore_page_async;
   iface->restore_page_finish = ide_session_addin_real_restore_page_finish;
   iface->can_save_page = ide_session_addin_real_can_save_page;
+  iface->get_autosave_properties = ide_session_addin_real_get_autosave_properties;
 }
 
 /**
@@ -205,6 +212,8 @@ ide_session_addin_restore_page_finish (IdeSessionAddin  *self,
  *
  * Checks whether @self supports saving @page. This is typically done by checking for
  * its GObject type using `FOO_IS_BAR_PAGE ()` for page types defined in the plugin.
+ * In practice it means that this @self addin supports all the different vfuncs for
+ * this @page.
  *
  * Returns: whether @self supports saving @page.
  *
@@ -219,3 +228,29 @@ ide_session_addin_can_save_page (IdeSessionAddin *self,
 
   return IDE_SESSION_ADDIN_GET_IFACE (self)->can_save_page (self, page);
 }
+
+/**
+ * ide_session_addin_get_autosave_properties:
+ * @self: an #IdeSessionAddin
+ *
+ * For the pages supported by its ide_session_addin_can_save_page() function, gets
+ * a list of properties names that should be watched for changes on this page using
+ * the GObject notify mechanism. So given an array with "foo" and "bar", the #IdeSession
+ * will connect to the "notify::foo" and "notify::bar" signals and schedule a saving
+ * operation for some minutes later, so saving operations are grouped together.
+ *
+ * A possible autosave property could be the #IdePage's "title" property, in case
+ * your state is always reflected there. But in general, it's better to use your
+ * own custom page properties as it will be more reliable.
+ *
+ * Returns: (array zero-terminated=1) (element-type utf8) (nullable) (transfer-full): A %NULL terminated 
array of properties names, or %NULL.
+ *
+ * Since: 41.0
+ */
+char **
+ide_session_addin_get_autosave_properties (IdeSessionAddin *self)
+{
+  g_return_val_if_fail (IDE_IS_SESSION_ADDIN (self), NULL);
+
+  return IDE_SESSION_ADDIN_GET_IFACE (self)->get_autosave_properties (self);
+}
diff --git a/src/libide/gui/ide-session-addin.h b/src/libide/gui/ide-session-addin.h
index f475f032b..bf313a821 100644
--- a/src/libide/gui/ide-session-addin.h
+++ b/src/libide/gui/ide-session-addin.h
@@ -56,30 +56,33 @@ struct _IdeSessionAddinInterface
                                     GError              **error);
   gboolean  (*can_save_page)       (IdeSessionAddin      *self,
                                     IdePage              *page);
+  char    **(*get_autosave_properties) (IdeSessionAddin *self);
 };
 
 IDE_AVAILABLE_IN_41
-void      ide_session_addin_save_page_async      (IdeSessionAddin      *self,
-                                                  IdePage              *page,
-                                                  GCancellable         *cancellable,
-                                                  GAsyncReadyCallback   callback,
-                                                  gpointer              user_data);
+void       ide_session_addin_save_page_async         (IdeSessionAddin      *self,
+                                                      IdePage              *page,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
 IDE_AVAILABLE_IN_41
-GVariant *ide_session_addin_save_page_finish     (IdeSessionAddin      *self,
-                                                  GAsyncResult         *result,
-                                                  GError              **error);
+GVariant  *ide_session_addin_save_page_finish        (IdeSessionAddin      *self,
+                                                      GAsyncResult         *result,
+                                                      GError              **error);
 IDE_AVAILABLE_IN_41
-void      ide_session_addin_restore_page_async   (IdeSessionAddin      *self,
-                                                  GVariant             *state,
-                                                  GCancellable         *cancellable,
-                                                  GAsyncReadyCallback   callback,
-                                                  gpointer              user_data);
+void       ide_session_addin_restore_page_async      (IdeSessionAddin      *self,
+                                                      GVariant             *state,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
 IDE_AVAILABLE_IN_41
-IdePage  *ide_session_addin_restore_page_finish  (IdeSessionAddin      *self,
-                                                  GAsyncResult         *result,
-                                                  GError              **error);
+IdePage   *ide_session_addin_restore_page_finish     (IdeSessionAddin      *self,
+                                                      GAsyncResult         *result,
+                                                      GError              **error);
 IDE_AVAILABLE_IN_41
-gboolean  ide_session_addin_can_save_page (IdeSessionAddin *self,
-                                           IdePage         *page);
+gboolean   ide_session_addin_can_save_page           (IdeSessionAddin      *self,
+                                                      IdePage              *page);
+IDE_AVAILABLE_IN_41
+char     **ide_session_addin_get_autosave_properties (IdeSessionAddin      *self);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-session.c b/src/libide/gui/ide-session.c
index 398ecbd6b..f2532ebe3 100644
--- a/src/libide/gui/ide-session.c
+++ b/src/libide/gui/ide-session.c
@@ -35,7 +35,7 @@
 struct _IdeSession
 {
   IdeObject               parent_instance;
-  IdeExtensionSetAdapter *addins;
+  GPtrArray              *addins;
 };
 
 typedef struct
@@ -43,6 +43,7 @@ typedef struct
   GPtrArray      *addins;
   GVariantBuilder pages_state;
   guint           active;
+  IdeGrid        *grid;
 } Save;
 
 typedef struct
@@ -72,7 +73,6 @@ restore_free (Restore *r)
   g_assert (r != NULL);
   g_assert (r->active == 0);
 
-  g_clear_pointer (&r->addins, g_ptr_array_unref);
   g_clear_pointer (&r->state, g_variant_unref);
   g_clear_pointer (&r->pages, g_array_unref);
 
@@ -85,8 +85,6 @@ save_free (Save *s)
   g_assert (s != NULL);
   g_assert (s->active == 0);
 
-  g_clear_pointer (&s->addins, g_ptr_array_unref);
-
   g_slice_free (Save, s);
 }
 
@@ -132,6 +130,152 @@ collect_addins_cb (IdeExtensionSetAdapter *set,
   g_ptr_array_add (ar, g_object_ref (exten));
 }
 
+static IdeSessionAddin *
+find_suitable_addin_for_page (IdePage   *page,
+                              GPtrArray *addins)
+{
+  for (guint i = 0; i < addins->len; i++)
+    {
+      IdeSessionAddin *addin = g_ptr_array_index (addins, i);
+      if (ide_session_addin_can_save_page (addin, page))
+        return addin;
+    }
+  return NULL;
+}
+
+static void
+on_session_autosaved_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  IdeSession *session = (IdeSession *)object;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SESSION (session));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  if (!ide_session_save_finish (session, result, &error))
+    g_warning ("Couldn't autosave session: %s", error->message);
+}
+
+typedef struct {
+  IdeSession *session;
+  IdeGrid    *grid;
+  guint       session_autosave_source;
+} AutosaveGrid;
+
+static void
+autosave_grid_free (gpointer data,
+                    GClosure *closure)
+{
+  AutosaveGrid *self = (AutosaveGrid *)data;
+
+  if (self->session_autosave_source)
+    {
+      g_source_remove (self->session_autosave_source);
+      self->session_autosave_source = 0;
+    }
+  g_slice_free (AutosaveGrid, self);
+}
+
+static gboolean
+on_session_autosave_timeout_cb (gpointer user_data)
+{
+  AutosaveGrid *autosave_grid = (AutosaveGrid *)user_data;
+
+  g_assert (IDE_IS_SESSION (autosave_grid->session));
+  g_assert (IDE_IS_GRID (autosave_grid->grid));
+
+  ide_session_save_async (autosave_grid->session,
+                          autosave_grid->grid,
+                          NULL,
+                          on_session_autosaved_cb,
+                          NULL);
+
+  autosave_grid->session_autosave_source = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+schedule_session_autosave_timeout (AutosaveGrid *autosave_grid)
+{
+  if (!autosave_grid->session_autosave_source)
+    {
+      /* We don't want to be saving the state on each (small) change, so introduce a small
+       * timeout so changes are grouped when saving.
+       */
+      autosave_grid->session_autosave_source =
+        g_timeout_add_seconds (30,
+                               on_session_autosave_timeout_cb,
+                               autosave_grid);
+    }
+}
+
+static void
+on_autosave_property_changed_cb (GObject    *gobject,
+                                 GParamSpec *pspec,
+                                 gpointer    user_data)
+{
+  schedule_session_autosave_timeout ((AutosaveGrid *)user_data);
+}
+
+static void
+watch_pages_session_autosave (AutosaveGrid *autosave_grid,
+                              guint         start_pos,
+                              guint         end_pos)
+{
+  GListModel *list = (GListModel *)autosave_grid->grid;
+  IdeSession *session = (IdeSession *)autosave_grid->session;
+
+  g_assert (IDE_IS_SESSION (session));
+  g_assert (G_IS_LIST_MODEL (list));
+  g_assert (g_type_is_a (g_list_model_get_item_type (list), IDE_TYPE_PAGE));
+  g_assert (start_pos <= end_pos);
+
+  for (guint i = start_pos; i < end_pos; i++)
+    {
+      IdePage *page = IDE_PAGE (g_list_model_get_object (list, i));
+      IdeSessionAddin *addin;
+      g_auto(GStrv) props = NULL;
+
+      if ((addin = find_suitable_addin_for_page (page, session->addins)) &&
+          (props = ide_session_addin_get_autosave_properties (addin)))
+        {
+          for (guint j = 0; props[j] != NULL; j++)
+            {
+              char detailed_signal[256];
+              g_snprintf (detailed_signal, sizeof detailed_signal, "notify::%s", props[j]);
+
+              g_signal_connect (page, detailed_signal, G_CALLBACK (on_autosave_property_changed_cb), 
autosave_grid);
+            }
+        }
+    }
+}
+
+static void
+on_grid_items_changed_cb (GListModel *list,
+                          guint       position,
+                          guint       removed,
+                          guint       added,
+                          gpointer    user_data)
+{
+  AutosaveGrid *autosave_grid = (AutosaveGrid *)user_data;
+
+  g_assert (G_IS_LIST_MODEL (list));
+  g_assert (g_type_is_a (g_list_model_get_item_type (list), IDE_TYPE_PAGE));
+
+  /* We've nothing to do when no page were added here as signals are
+   * automatically disconnected, so avoid extra work by stopping here early.
+   */
+  if (added > 0)
+    watch_pages_session_autosave (autosave_grid, position, position + added);
+
+  /* Handles autosaving both when closing/opening a page and when moving a page in the grid. */
+  schedule_session_autosave_timeout (autosave_grid);
+}
+
 static void
 ide_session_destroy (IdeObject *object)
 {
@@ -142,7 +286,7 @@ ide_session_destroy (IdeObject *object)
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_SESSION (self));
 
-  ide_clear_and_destroy_object (&self->addins);
+  g_clear_pointer (&self->addins, g_ptr_array_unref);
 
   IDE_OBJECT_CLASS (ide_session_parent_class)->destroy (object);
 
@@ -154,6 +298,7 @@ ide_session_parent_set (IdeObject *object,
                         IdeObject *parent)
 {
   IdeSession *self = (IdeSession *)object;
+  g_autoptr(IdeExtensionSetAdapter) extension_set = NULL;
 
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_SESSION (self));
@@ -162,10 +307,13 @@ ide_session_parent_set (IdeObject *object,
   if (parent == NULL)
     return;
 
-  self->addins = ide_extension_set_adapter_new (IDE_OBJECT (self),
-                                                peas_engine_get_default (),
-                                                IDE_TYPE_SESSION_ADDIN,
-                                                NULL, NULL);
+  extension_set = ide_extension_set_adapter_new (IDE_OBJECT (self),
+                                                 peas_engine_get_default (),
+                                                 IDE_TYPE_SESSION_ADDIN,
+                                                 NULL, NULL);
+
+  self->addins = g_ptr_array_new_with_free_func (g_object_unref);
+  ide_extension_set_adapter_foreach (extension_set, collect_addins_cb, self->addins);
 }
 
 static void
@@ -549,8 +697,7 @@ ide_session_restore_async (IdeSession          *self,
   ide_task_set_source_tag (task, ide_session_restore_async);
 
   r = g_slice_new0 (Restore);
-  r->addins = g_ptr_array_new_with_free_func (g_object_unref);
-  ide_extension_set_adapter_foreach (self->addins, collect_addins_cb, r->addins);
+  r->addins = self->addins;
   r->grid = grid;
   ide_task_set_task_data (task, r, restore_free);
 
@@ -578,12 +725,33 @@ ide_session_restore_finish (IdeSession    *self,
                             GError       **error)
 {
   gboolean ret;
+  Restore *r;
+  GListModel *list;
+  AutosaveGrid *autosave_grid;
 
   IDE_ENTRY;
 
   g_return_val_if_fail (IDE_IS_SESSION (self), FALSE);
   g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
 
+  r = ide_task_get_task_data (IDE_TASK (result));
+  g_assert (r != NULL);
+  list = G_LIST_MODEL (r->grid);
+
+  autosave_grid = g_slice_new0 (AutosaveGrid);
+  autosave_grid->grid = r->grid;
+  autosave_grid->session = self;
+  autosave_grid->session_autosave_source = 0;
+
+  watch_pages_session_autosave (autosave_grid,
+                                0, g_list_model_get_n_items (list));
+  g_signal_connect_data (list,
+                         "items-changed",
+                         G_CALLBACK (on_grid_items_changed_cb),
+                         autosave_grid,
+                         autosave_grid_free,
+                         0);
+
   ret = ide_task_propagate_boolean (IDE_TASK (result), error);
 
   IDE_RETURN (ret);
@@ -776,19 +944,6 @@ on_session_addin_page_saved_cb (GObject      *object,
   IDE_EXIT;
 }
 
-static IdeSessionAddin *
-find_suitable_addin_for_page (IdePage   *page,
-                              GPtrArray *addins)
-{
-  for (guint i = 0; i < addins->len; i++)
-    {
-      IdeSessionAddin *addin = g_ptr_array_index (addins, i);
-      if (ide_session_addin_can_save_page (addin, page))
-        return addin;
-    }
-  return NULL;
-}
-
 static void
 foreach_page_in_grid_save_cb (GtkWidget *widget,
                               gpointer   user_data)
@@ -859,14 +1014,14 @@ ide_session_save_async (IdeSession          *self,
   ide_task_set_source_tag (task, ide_session_save_async);
 
   s = g_slice_new0 (Save);
-  s->addins = g_ptr_array_new_with_free_func (g_object_unref);
-  ide_extension_set_adapter_foreach (self->addins, collect_addins_cb, s->addins);
-  s->active = ide_grid_count_pages (grid);
+  s->addins = self->addins;
+  s->grid = grid;
+  s->active = ide_grid_count_pages (s->grid);
 
   g_variant_builder_init (&s->pages_state, G_VARIANT_TYPE ("aa{sv}"));
   ide_task_set_task_data (task, s, save_free);
 
-  ide_grid_foreach_page (grid,
+  ide_grid_foreach_page (s->grid,
                          foreach_page_in_grid_save_cb,
                          task);
 


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