/* ide-session.c * * Copyright 2018-2019 Christian Hergert * * 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 3 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 . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "ide-session" #include "config.h" #include #include #include #include #include "ide-session-addin.h" #include "ide-session-private.h" #include "ide-gui-private.h" struct _IdeSession { IdeObject parent_instance; GPtrArray *addins; }; typedef struct { GPtrArray *addins; GVariantBuilder pages_state; guint active; IdeGrid *grid; } Save; typedef struct { GPtrArray *addins; GVariant *state; IdeGrid *grid; GArray *pages; guint active; } Restore; typedef struct { guint column; guint row; guint depth; IdeSessionAddin *addin; GVariant *state; IdePage *restored_page; } RestoreItem; G_DEFINE_FINAL_TYPE (IdeSession, ide_session, IDE_TYPE_OBJECT) static void restore_free (Restore *r) { g_assert (r != NULL); g_assert (r->active == 0); g_clear_pointer (&r->state, g_variant_unref); g_clear_pointer (&r->pages, g_array_unref); g_slice_free (Restore, r); } static void save_free (Save *s) { g_assert (s != NULL); g_assert (s->active == 0); g_slice_free (Save, s); } static void restore_item_clear (RestoreItem *item) { g_assert (item != NULL); g_clear_pointer (&item->state, g_variant_unref); } static gint compare_restore_items (gconstpointer a, gconstpointer b) { const RestoreItem *item_a = a; const RestoreItem *item_b = b; gint ret; if (!(ret = item_a->column - item_b->column)) { if (!(ret = item_a->row - item_b->row)) ret = item_a->depth - item_b->depth; } return ret; } static void collect_addins_cb (IdeExtensionSetAdapter *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { GPtrArray *ar = user_data; g_assert (IDE_IS_MAIN_THREAD ()); g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_SESSION_ADDIN (exten)); g_assert (ar != NULL); 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) { IdeSession *self = (IdeSession *)object; IDE_ENTRY; g_assert (IDE_IS_MAIN_THREAD ()); g_assert (IDE_IS_SESSION (self)); g_clear_pointer (&self->addins, g_ptr_array_unref); IDE_OBJECT_CLASS (ide_session_parent_class)->destroy (object); IDE_EXIT; } static void 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)); g_assert (!parent || IDE_IS_OBJECT (parent)); if (parent == NULL) return; 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 ide_session_class_init (IdeSessionClass *klass) { IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass); i_object_class->destroy = ide_session_destroy; i_object_class->parent_set = ide_session_parent_set; } static void ide_session_init (IdeSession *self) { } static void restore_pages_to_grid (GArray *r_items, IdeGrid *grid) { IDE_ENTRY; for (guint i = 0; i < r_items->len; i++) { RestoreItem *item = &g_array_index (r_items, RestoreItem, i); IdeGridColumn *column; IdeFrame *stack; /* Ignore pages that couldn't be restored. */ if (item->restored_page == NULL) continue; /* This relies on the fact that the items are sorted. */ column = ide_grid_get_nth_column (grid, item->column); stack = _ide_grid_get_nth_stack_for_column (grid, column, item->row); gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (item->restored_page)); } IDE_EXIT; } typedef struct { IdeTask *task; RestoreItem *item; } RestorePage; static void on_session_addin_page_restored_cb (GObject *object, GAsyncResult *result, gpointer user_data) { IdeSessionAddin *addin = (IdeSessionAddin *)object; RestorePage *r_page = user_data; g_autoptr(IdeTask) task = r_page->task; g_autoptr(GError) error = NULL; RestoreItem *item = r_page->item; Restore *r; IDE_ENTRY; g_assert (IDE_IS_SESSION_ADDIN (addin)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_TASK (task)); r = ide_task_get_task_data (task); g_assert (r != NULL); g_assert (r->addins != NULL); g_assert (r->active > 0); g_assert (r->state != NULL); if (!(item->restored_page = ide_session_addin_restore_page_finish (addin, result, &error))) g_warning ("Couldn't restore page with addin %s: %s", G_OBJECT_TYPE_NAME (addin), error->message); r->active--; if (r->active == 0) { restore_pages_to_grid (r->pages, r->grid); ide_task_return_boolean (task, TRUE); } IDE_EXIT; } static IdeSessionAddin * get_addin_for_name (GPtrArray *addins, const char *addin_name) { GType addin_type = g_type_from_name (addin_name); for (guint i = 0; i < addins->len; i++) { if (G_OBJECT_TYPE (addins->pdata[i]) == addin_type) return addins->pdata[i]; } return NULL; } static void load_restore_items (Restore *r, GArray *items) { GVariantIter iter; RestoreItem item; GVariant *page_state = NULL; g_assert (r != NULL); g_assert (r->state != NULL); g_assert (r->addins != NULL); g_assert (items != NULL); g_variant_iter_init (&iter, r->state); while ((page_state = g_variant_iter_next_value (&iter))) { const char *addin_name = NULL; g_variant_lookup (page_state, "column", "u", &item.column); g_variant_lookup (page_state, "row", "u", &item.row); g_variant_lookup (page_state, "depth", "u", &item.depth); g_variant_lookup (page_state, "addin_name", "&s", &addin_name); g_variant_lookup (page_state, "addin_page_state", "v", &item.state); item.addin = get_addin_for_name (r->addins, addin_name); g_array_append_val (items, item); g_variant_unref (page_state); } } static GVariant * migrate_pre_api_rework (GVariant *pages_variant) { GVariantIter iter; const char *uri = NULL; int column, row, depth; /* Freed in the loop. */ GVariant *search_variant; GVariantDict version_wrapper_dict; GVariantBuilder addins_states; g_variant_dict_init (&version_wrapper_dict, NULL); /* Migrate old format to first version of the new format. */ g_variant_dict_insert (&version_wrapper_dict, "version", "u", (guint32) 1); g_variant_builder_init (&addins_states, G_VARIANT_TYPE ("aa{sv}")); g_debug ("Handling migration of the project's session.gvariant, from prior to the Session API rework…"); g_variant_iter_init (&iter, pages_variant); while (g_variant_iter_next (&iter, "(&siiiv)", &uri, &column, &row, &depth, &search_variant)) { GVariantDict addin_state; GVariantDict editor_session_state; g_variant_dict_init (&addin_state, NULL); g_variant_dict_insert (&addin_state, "column", "u", (guint32) column); g_variant_dict_insert (&addin_state, "row", "u", (guint32) row); g_variant_dict_insert (&addin_state, "depth", "u", (guint32) depth); g_variant_dict_insert (&addin_state, "addin_name", "s", "GbpEditorSessionAddin"); /* Since we need to migrate the data for the new API, let's also migrate to a dictionary * instead of a tuple, for greater flexibility and extensibility in the future. */ g_variant_dict_init (&editor_session_state, NULL); g_variant_dict_insert (&editor_session_state, "uri", "s", uri); /* Unbox the search_variant since we don't want to bother with multiple levels of variants, * just have an a{sv} */ g_variant_dict_insert_value (&editor_session_state, "search", g_variant_get_variant (search_variant)); g_variant_dict_insert (&addin_state, "addin_page_state", "v", g_variant_dict_end (&editor_session_state)); g_variant_builder_add_value (&addins_states, g_variant_dict_end (&addin_state)); g_variant_unref (search_variant); } g_variant_dict_insert_value (&version_wrapper_dict, "data", g_variant_builder_end (&addins_states)); g_debug ("Successfully migrated old session.gvariant to new format."); return g_variant_take_ref (g_variant_dict_end (&version_wrapper_dict)); } static GVariant * load_state_with_migrations (GBytes *bytes) { g_autoptr(GVariant) variant = NULL; /* This is the value of the "data" key in the final @variant. */ g_autoptr(GVariant) migrated_state = NULL; GVariantDict state; gboolean fully_migrated = FALSE; GVariant *old_api_state = NULL; g_assert (bytes != NULL); variant = g_variant_take_ref (g_variant_new_from_bytes (G_VARIANT_TYPE_VARDICT, bytes, FALSE)); if (!variant) { g_warning ("Couldn't load the array of pages' states from session.gvariant!"); return NULL; } g_variant_dict_init (&state, variant); /* Handle migrations from prior to the Session API rework, where there was only GbpEditorSessionAddin that used it */ old_api_state = g_variant_dict_lookup_value (&state, "GbpEditorSessionAddin", G_VARIANT_TYPE ("a(siiiv)")); if (old_api_state) migrated_state = migrate_pre_api_rework (old_api_state); else migrated_state = g_steal_pointer (&variant); while (!fully_migrated) { guint32 version; g_autoptr(GVariant) versioned_data = NULL; if (!g_variant_lookup (migrated_state, "version", "u", &version)) { g_warning ("session.gvariant isn't using the old format but doesn't have a version field, so cannot load it!"); fully_migrated = TRUE; migrated_state = NULL; break; } if (!(versioned_data = g_variant_lookup_value (migrated_state, "data", G_VARIANT_TYPE ("aa{sv}")))) { g_warning ("session.gvariant had a version field but the actual versioned data wasn't found, so cannot load it!"); fully_migrated = TRUE; migrated_state = NULL; break; } switch (version) { /* It's the current format so the rest of the code understands it natively. */ case 1: migrated_state = g_steal_pointer (&migrated_state); fully_migrated = TRUE; break; default: g_warning ("Version %d of session.gvariant data is not known to Builder!", version); migrated_state = NULL; fully_migrated = TRUE; } } if (migrated_state) /* The current format (version 1) is an `aa{sv}` (array of dictionaries) with the dict's keys being: * guint32 column, row, depth; * char *addin_name; * GVariant *addin_page_state; */ return g_variant_lookup_value (migrated_state, "data", NULL); else return NULL; } static void on_session_cache_loaded_cb (GObject *object, GAsyncResult *result, gpointer user_data) { GFile *file = (GFile *)object; g_autoptr(IdeTask) task = user_data; g_autoptr(GError) error = NULL; g_autoptr(GBytes) bytes = NULL; GArray *items = NULL; GCancellable *cancellable; Restore *r; IDE_ENTRY; g_assert (G_IS_FILE (file)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_TASK (task)); r = ide_task_get_task_data (task); cancellable = ide_task_get_cancellable (task); g_assert (r != NULL); g_assert (r->addins != NULL); g_assert (r->addins->len > 0); g_assert (r->state == NULL); g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); if (!(bytes = g_file_load_bytes_finish (file, result, NULL, &error))) { if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) ide_task_return_boolean (task, TRUE); else ide_task_return_error (task, g_steal_pointer (&error)); IDE_EXIT; } if (g_bytes_get_size (bytes) == 0) { ide_task_return_boolean (task, TRUE); IDE_EXIT; } r->state = load_state_with_migrations (bytes); if (r->state == NULL) { ide_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "Failed to decode session state"); IDE_EXIT; } items = g_array_new (FALSE, FALSE, sizeof (RestoreItem)); g_array_set_clear_func (items, (GDestroyNotify)restore_item_clear); load_restore_items (r, items); r->pages = items; r->active = items->len; g_array_sort (items, compare_restore_items); for (guint i = 0; i < items->len; i++) { RestoreItem *item = &g_array_index (items, RestoreItem, i); RestorePage *r_page = g_slice_new0 (RestorePage); r_page->task = g_object_ref (task); r_page->item = item; ide_session_addin_restore_page_async (item->addin, item->state, cancellable, on_session_addin_page_restored_cb, r_page); } if (r->active == 0) { ide_task_return_boolean (task, TRUE); IDE_EXIT; } IDE_EXIT; } /** * ide_session_restore_async: * @self: an #IdeSession * @grid: an #IdeGrid * @cancellable: (nullable): a #GCancellable or %NULL * @callback: the callback to execute upon completion * @user_data: user data for callback * * This function will asynchronously restore the state of the project to * the point it was last saved (typically upon shutdown). This includes * open documents and editor splits to the degree possible. Adding support * for a new page type requires implementing an #IdeSessionAddin. * * Since: 41 */ void ide_session_restore_async (IdeSession *self, IdeGrid *grid, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(IdeTask) task = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GSettings) settings = NULL; IdeContext *context; Restore *r; IDE_ENTRY; g_return_if_fail (IDE_IS_SESSION (self)); g_return_if_fail (IDE_IS_GRID (grid)); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); task = ide_task_new (self, cancellable, callback, user_data); ide_task_set_source_tag (task, ide_session_restore_async); r = g_slice_new0 (Restore); r->addins = self->addins; r->grid = grid; ide_task_set_task_data (task, r, restore_free); settings = g_settings_new ("org.gnome.builder"); if (!g_settings_get_boolean (settings, "restore-previous-files")) { ide_task_return_boolean (task, TRUE); return; } context = ide_object_get_context (IDE_OBJECT (self)); file = ide_context_cache_file (context, "session.gvariant", NULL); g_file_load_bytes_async (file, cancellable, on_session_cache_loaded_cb, g_steal_pointer (&task)); IDE_EXIT; } gboolean ide_session_restore_finish (IdeSession *self, GAsyncResult *result, 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); } static void on_state_saved_to_cache_file_cb (GObject *object, GAsyncResult *result, gpointer user_data) { GFile *file = (GFile *)object; g_autoptr(GError) error = NULL; g_autoptr(IdeTask) task = user_data; IDE_ENTRY; g_assert (G_IS_FILE (file)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_TASK (task)); if (!g_file_replace_contents_finish (file, result, NULL, &error)) ide_task_return_error (task, g_steal_pointer (&error)); else ide_task_return_boolean (task, TRUE); IDE_EXIT; } static void get_page_position (IdePage *page, guint *out_column, guint *out_row, guint *out_depth) { GtkWidget *frame_pages_stack; GtkWidget *frame; GtkWidget *grid_column; GtkWidget *grid; g_assert (IDE_IS_PAGE (page)); g_assert (out_column != NULL); g_assert (out_row != NULL); g_assert (out_depth != NULL); frame_pages_stack = gtk_widget_get_ancestor (GTK_WIDGET (page), GTK_TYPE_STACK); frame = gtk_widget_get_ancestor (GTK_WIDGET (frame_pages_stack), IDE_TYPE_FRAME); grid_column = gtk_widget_get_ancestor (GTK_WIDGET (frame), IDE_TYPE_GRID_COLUMN); grid = gtk_widget_get_ancestor (GTK_WIDGET (grid_column), IDE_TYPE_GRID); /* When this page is the currently visible one for this frame, we want to keep it on top when * restoring so that there's no need to switch back to the pages we were working on. We need to * do this because the stack's "position" child property only refers to the order in which the * pages were initially opened, not the most-recently-used order. */ if (ide_frame_get_visible_child (IDE_FRAME (frame)) == page) { *out_depth = g_list_model_get_n_items (G_LIST_MODEL (frame)); } else { gtk_container_child_get (GTK_CONTAINER (frame_pages_stack), GTK_WIDGET (page), "position", out_depth, NULL); *out_depth = MAX (*out_depth, 0); } gtk_container_child_get (GTK_CONTAINER (grid_column), GTK_WIDGET (frame), "index", out_row, NULL); *out_row = MAX (*out_row, 0); gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (grid_column), "index", out_column, NULL); *out_column = MAX (*out_column, 0); } typedef struct { IdeTask *task; IdePage *page; } SavePage; static void save_state_to_disk (IdeSession *self, IdeTask *task, GVariantBuilder *pages_state) { g_autoptr(GVariant) state = NULL; g_autoptr(GBytes) bytes = NULL; g_autoptr(GFile) file = NULL; GCancellable *cancellable; IdeContext *context; GVariantDict final_dict; IDE_ENTRY; g_assert (IDE_IS_SESSION (self)); g_assert (IDE_IS_TASK (task)); g_assert (pages_state != NULL); cancellable = ide_task_get_cancellable (task); g_variant_dict_init (&final_dict, NULL); g_variant_dict_insert (&final_dict, "version", "u", (guint32) 1); g_variant_dict_insert_value (&final_dict, "data", g_variant_builder_end (pages_state)); state = g_variant_ref_sink (g_variant_dict_end (&final_dict)); bytes = g_variant_get_data_as_bytes (state); #ifdef IDE_ENABLE_TRACE { g_autofree char *str = g_variant_print (state, TRUE); IDE_TRACE_MSG ("Saving session state to %s", str); } #endif context = ide_object_get_context (IDE_OBJECT (self)); file = ide_context_cache_file (context, "session.gvariant", NULL); if (!ide_task_return_error_if_cancelled (task)) g_file_replace_contents_bytes_async (file, bytes, NULL, FALSE, G_FILE_CREATE_NONE, cancellable, on_state_saved_to_cache_file_cb, g_object_ref (task)); IDE_EXIT; } static void on_session_addin_page_saved_cb (GObject *object, GAsyncResult *result, gpointer user_data) { IdeSessionAddin *addin = (IdeSessionAddin *)object; g_autoptr(GVariant) page_state = NULL; SavePage *save_page = user_data; g_autoptr(IdeTask) task = save_page->task; IdePage *page = save_page->page; g_autoptr(GError) error = NULL; IdeSession *self; Save *s; IDE_ENTRY; g_assert (IDE_IS_SESSION_ADDIN (addin)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_TASK (task)); self = ide_task_get_source_object (task); s = ide_task_get_task_data (task); g_assert (IDE_IS_SESSION (self)); g_assert (s != NULL); g_assert (s->active > 0); page_state = ide_session_addin_save_page_finish (addin, result, &error); if (error != NULL) g_warning ("Could not save page with addin %s: %s", G_OBJECT_TYPE_NAME (addin), error->message); if (page_state != NULL) { guint frame_column, frame_row, frame_depth; GVariantDict state_dict; g_assert (!g_variant_is_floating (page_state)); get_page_position (page, &frame_column, &frame_row, &frame_depth); g_variant_dict_init (&state_dict, NULL); g_variant_dict_insert (&state_dict, "column", "u", frame_column); g_variant_dict_insert (&state_dict, "row", "u", frame_row); g_variant_dict_insert (&state_dict, "depth", "u", frame_depth); g_variant_dict_insert (&state_dict, "addin_name", "s", G_OBJECT_TYPE_NAME (addin)); g_variant_dict_insert (&state_dict, "addin_page_state", "v", page_state); g_variant_builder_add_value (&s->pages_state, g_variant_dict_end (&state_dict)); } g_slice_free (SavePage, save_page); s->active--; if (s->active == 0) save_state_to_disk (self, task, &s->pages_state); IDE_EXIT; } static void foreach_page_in_grid_save_cb (GtkWidget *widget, gpointer user_data) { IdePage *page = IDE_PAGE (widget); IdeTask *task = user_data; IdeSessionAddin *addin; SavePage *save_page = NULL; Save *s; g_assert (IDE_IS_PAGE (page)); g_assert (IDE_IS_TASK (task)); s = ide_task_get_task_data (task); g_assert (s != NULL); g_assert (s->addins != NULL); if (!(addin = find_suitable_addin_for_page (page, s->addins))) { /* It's not a saveable page. */ s->active--; return; } save_page = g_slice_new0 (SavePage); save_page->task = g_object_ref (task); save_page->page = page; ide_session_addin_save_page_async (addin, page, ide_task_get_cancellable (task), on_session_addin_page_saved_cb, g_steal_pointer (&save_page)); } /** * ide_session_save_async: * @self: an #IdeSession * @grid: an #IdeGrid * @cancellable: (nullable): a #GCancellable, or %NULL * @callback: a callback to execute upon completion * @user_data: user data for @callback * * This function will save the position and content of the pages in the @grid, * which can then be restored with ide_session_restore_async(), asking the * content of the pages to the appropriate #IdeSessionAddin. * * Since: 41 */ void ide_session_save_async (IdeSession *self, IdeGrid *grid, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(IdeTask) task = NULL; Save *s; IDE_ENTRY; g_return_if_fail (IDE_IS_SESSION (self)); g_return_if_fail (IDE_IS_GRID (grid)); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); task = ide_task_new (self, cancellable, callback, user_data); ide_task_set_source_tag (task, ide_session_save_async); s = g_slice_new0 (Save); 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 (s->grid, foreach_page_in_grid_save_cb, task); g_assert (s != NULL); /* Save the empty pages state there too because it wouldn't have * been done in foreach_page_in_grid_save_cb() since there's no * pages to save. */ if (s->active == 0) save_state_to_disk (self, task, &s->pages_state); IDE_EXIT; } gboolean ide_session_save_finish (IdeSession *self, GAsyncResult *result, GError **error) { gboolean ret; IDE_ENTRY; g_return_val_if_fail (IDE_IS_SESSION (self), FALSE); g_return_val_if_fail (IDE_IS_TASK (result), FALSE); ret = ide_task_propagate_boolean (IDE_TASK (result), error); IDE_RETURN (ret); } IdeSession * ide_session_new (void) { return g_object_new (IDE_TYPE_SESSION, NULL); }