[gnome-builder] gui: Add support for session autosaving
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder] gui: Add support for session autosaving
- Date: Sun, 18 Jul 2021 17:28:07 +0000 (UTC)
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]