From 429234ff213ba04b6d0b02a28ed68aaa8af7c02c Mon Sep 17 00:00:00 2001 From: Matthew Barnes Date: Thu, 2 Sep 2010 13:21:08 -0400 Subject: Convert composer autosave to an EExtension. Given the way the autosave feature was awkwardly bolted on to the composer, an EExtension seemed like a natural fit. And it helped clean up some object lifecycle hacks (and bugs). What we have now is a new module consisting of two EExtensions: EComposerAutosave extends EMsgComposer and determines when to kick off an asynchronous autosave operation. EComposerRegistry extends EShell and offers to restore orphaned autosave files on startup (which is also asynchronous now). e-autosave-utils.c holds the actual asynchronous functions and a few other miscellaneous utility functions. Source code for the new module lives in /modules/composer-autosave. --- composer/Makefile.am | 2 - composer/e-composer-autosave.c | 511 --------------------- composer/e-composer-autosave.h | 45 -- composer/e-composer-private.h | 1 - composer/e-msg-composer.c | 142 +----- composer/e-msg-composer.h | 5 - configure.ac | 1 + mail/em-composer-utils.c | 7 +- modules/composer-autosave/Makefile.am | 28 ++ modules/composer-autosave/e-autosave-utils.c | 499 ++++++++++++++++++++ modules/composer-autosave/e-autosave-utils.h | 48 ++ modules/composer-autosave/e-composer-autosave.c | 227 +++++++++ modules/composer-autosave/e-composer-registry.c | 236 ++++++++++ .../evolution-composer-autosave.c | 40 ++ modules/mail/e-mail-shell-backend.c | 8 - shell/e-shell.c | 17 +- 16 files changed, 1113 insertions(+), 704 deletions(-) delete mode 100644 composer/e-composer-autosave.c delete mode 100644 composer/e-composer-autosave.h create mode 100644 modules/composer-autosave/Makefile.am create mode 100644 modules/composer-autosave/e-autosave-utils.c create mode 100644 modules/composer-autosave/e-autosave-utils.h create mode 100644 modules/composer-autosave/e-composer-autosave.c create mode 100644 modules/composer-autosave/e-composer-registry.c create mode 100644 modules/composer-autosave/evolution-composer-autosave.c diff --git a/composer/Makefile.am b/composer/Makefile.am index 68a7ec142c..5caea41ee7 100644 --- a/composer/Makefile.am +++ b/composer/Makefile.am @@ -18,7 +18,6 @@ libcomposerinclude_HEADERS = \ e-composer-text-header.h \ e-composer-common.h \ e-composer-actions.h \ - e-composer-autosave.h \ e-msg-composer.h libcomposer_la_CPPFLAGS = \ @@ -43,7 +42,6 @@ libcomposer_la_CPPFLAGS = \ libcomposer_la_SOURCES = \ $(libcomposerinclude_HEADERS) \ e-composer-actions.c \ - e-composer-autosave.c \ e-composer-header.c \ e-composer-header-table.c \ e-composer-from-header.c \ diff --git a/composer/e-composer-autosave.c b/composer/e-composer-autosave.c deleted file mode 100644 index 82a44d570a..0000000000 --- a/composer/e-composer-autosave.c +++ /dev/null @@ -1,511 +0,0 @@ -/* - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) version 3. - * - * 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with the program; if not, see - * - * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) - */ - -#include "e-composer-autosave.h" - -#include -#include -#include -#include - -#include -#include - -#define AUTOSAVE_PREFIX ".evolution-composer.autosave" -#define AUTOSAVE_SEED AUTOSAVE_PREFIX "-XXXXXX" -#define AUTOSAVE_INTERVAL 60 /* seconds */ - -typedef struct _AutosaveState AutosaveState; - -struct _AutosaveState { - EMsgComposer *composer; - GFile *file; - gboolean changed; - guint source_id; /* timeout source ID */ - gboolean enabled; - gboolean error_shown; -}; - -static GList *autosave_registry; - -static void composer_changed_cb (EMsgComposer *composer); - -static EMsgComposer * -composer_autosave_registry_lookup (const gchar *basename) -{ - GList *iter; - - /* Find the composer with the given autosave filename. */ - for (iter = autosave_registry; iter != NULL; iter = iter->next) { - EMsgComposer *composer = iter->data; - AutosaveState *state; - gchar *_basename; - - state = g_object_get_data (G_OBJECT (composer), "autosave"); - if (state == NULL || state->file == NULL) - continue; - - _basename = g_file_get_basename (state->file); - if (strcmp (_basename, basename) == 0) { - g_free (_basename); - return composer; - } - g_free (_basename); - } - - return NULL; -} - -static AutosaveState * -composer_autosave_state_new (EMsgComposer *composer) -{ - AutosaveState *state; - - state = g_slice_new0 (AutosaveState); - state->enabled = TRUE; - state->changed = FALSE; - state->source_id = 0; - state->composer = composer; - - g_signal_connect ( - composer, "notify::changed", - G_CALLBACK (composer_changed_cb), NULL); - - return state; -} - -static void -composer_autosave_state_free (AutosaveState *state) -{ - if (state->source_id) - g_source_remove (state->source_id); - if (state->file) - g_object_unref (state->file); - g_slice_free (AutosaveState, state); -} - -static gboolean -composer_autosave_state_open (AutosaveState *state, - GError **error) -{ - const gchar *user_data_dir; - gchar *path; - gint fd; - - if (state->file != NULL) - return TRUE; - - user_data_dir = e_get_user_data_dir (); - path = g_build_filename (user_data_dir, AUTOSAVE_SEED, NULL); - - /* Since GIO doesn't have support for creating temporary files - * from a template (and in a given directory), we have to use - * g_mkstemp(), which brings a small risk of overwriting another - * autosave file. The risk is, however, miniscule. */ - errno = 0; - fd = g_mkstemp (path); - if (fd == -1) { - g_set_error ( - error, G_FILE_ERROR, - g_file_error_from_errno (errno), - "%s", g_strerror (errno)); - g_free (path); - return FALSE; - } - - close (fd); - - /* Create the GFile */ - state->file = g_file_new_for_path (path); - g_free (path); - - return TRUE; -} - -static void -composer_autosave_finish_cb (EMsgComposer *composer, - GAsyncResult *result) -{ - AutosaveState *state; - GError *error = NULL; - - state = g_object_get_data (G_OBJECT (composer), "autosave"); - g_return_if_fail (state != NULL); - - e_composer_autosave_snapshot_finish (composer, result, &error); - - if (error != NULL) { - gchar *basename; - - if (G_IS_FILE (state->file)) - basename = g_file_get_basename (state->file); - else - basename = g_strdup (" "); - - /* Only show one error dialog at a - * time to avoid cascading dialogs. */ - if (!state->error_shown) { - state->error_shown = TRUE; - e_alert_run_dialog_for_args ( - GTK_WINDOW (composer), - "mail-composer:no-autosave", - basename, error->message, NULL); - state->error_shown = FALSE; - } else - g_warning ("%s: %s", basename, error->message); - - g_free (basename); - g_error_free (error); - } -} - -static gboolean -composer_autosave_timeout (EMsgComposer *composer) -{ - AutosaveState *state; - - g_return_val_if_fail (composer != NULL, FALSE); - g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), FALSE); - - state = g_object_get_data (G_OBJECT (composer), "autosave"); - g_return_val_if_fail (state != NULL, FALSE); - g_return_val_if_fail (state->composer == composer, FALSE); - - if (!state->changed) { - state->source_id = 0; - return FALSE; - } - - composer = state->composer; - - if (e_composer_autosave_get_enabled (composer)) { - state->changed = FALSE; - e_composer_autosave_snapshot_async ( - composer, (GAsyncReadyCallback) - composer_autosave_finish_cb, NULL); - } - - return TRUE; -} - -static void -composer_changed_cb (EMsgComposer *composer) -{ - AutosaveState *state; - - g_return_if_fail (composer != NULL); - g_return_if_fail (E_IS_MSG_COMPOSER (composer)); - - state = g_object_get_data (G_OBJECT (composer), "autosave"); - g_return_if_fail (state != NULL); - g_return_if_fail (state->composer == composer); - - g_object_get (G_OBJECT (composer), "changed", &state->changed, NULL); - - if (state->changed && state->source_id == 0) { - state->source_id = g_timeout_add_seconds ( - AUTOSAVE_INTERVAL, (GSourceFunc) - composer_autosave_timeout, state->composer); - } -} - -static void -composer_autosave_notify (gpointer unused, - GObject *where_the_object_was) -{ - /* Remove the dead composer from the registry. */ - autosave_registry = g_list_remove ( - autosave_registry, where_the_object_was); -} - -GList * -e_composer_autosave_find_orphans (GError **error) -{ - GDir *dir; - const gchar *dirname; - const gchar *basename; - GList *orphans = NULL; - - dirname = e_get_user_data_dir (); - dir = g_dir_open (dirname, 0, error); - if (dir == NULL) - return NULL; - - /* Scan the user directory for autosave files. */ - while ((basename = g_dir_read_name (dir)) != NULL) { - const gchar *errmsg; - gchar *filename; - struct stat st; - - /* Is this an autosave file? */ - if (!g_str_has_prefix (basename, AUTOSAVE_PREFIX)) - continue; - - /* Is this an orphaned autosave file? */ - if (composer_autosave_registry_lookup (basename) != NULL) - continue; - - filename = g_build_filename (dirname, basename, NULL); - - /* Try to examine the autosave file. Failure here - * is non-fatal; just emit a warning and move on. */ - errno = 0; - if (g_stat (filename, &st) < 0) { - errmsg = g_strerror (errno); - g_warning ("%s: %s", filename, errmsg); - g_free (filename); - continue; - } - - /* If the file is empty, delete it. Failure here - * is non-fatal; just emit a warning and move on. */ - if (st.st_size == 0) { - errno = 0; - if (g_unlink (filename) < 0) { - errmsg = g_strerror (errno); - g_warning ("%s: %s", filename, errmsg); - } - g_free (filename); - continue; - } - - orphans = g_list_prepend (orphans, filename); - } - - g_dir_close (dir); - - return g_list_reverse (orphans); -} - -void -e_composer_autosave_register (EMsgComposer *composer) -{ - g_return_if_fail (E_IS_MSG_COMPOSER (composer)); - - g_object_set_data_full ( - G_OBJECT (composer), "autosave", - composer_autosave_state_new (composer), - (GDestroyNotify) composer_autosave_state_free); - - autosave_registry = g_list_prepend (autosave_registry, composer); - - g_object_weak_ref ( - G_OBJECT (composer), (GWeakNotify) - composer_autosave_notify, NULL); -} - -void -e_composer_autosave_unregister (EMsgComposer *composer) -{ - AutosaveState *state; - - g_return_if_fail (E_IS_MSG_COMPOSER (composer)); - - state = g_object_get_data (G_OBJECT (composer), "autosave"); - if (state == NULL || state->file == NULL) - return; - - g_file_delete (state->file, NULL, NULL); - - g_object_set_data (G_OBJECT (composer), "autosave", NULL); -} - -static void -autosave_snapshot_splice_cb (GOutputStream *output_stream, - GAsyncResult *result, - GSimpleAsyncResult *simple) -{ - GError *error = NULL; - - g_output_stream_splice_finish (output_stream, result, &error); - - if (error != NULL) { - g_simple_async_result_set_from_error (simple, error); - g_error_free (error); - } - - g_simple_async_result_complete (simple); - g_object_unref (simple); -} - -static void -autosave_snapshot_cb (GFile *file, - GAsyncResult *result, - GSimpleAsyncResult *simple) -{ - GObject *object; - EMsgComposer *composer; - CamelMimeMessage *message; - GFileOutputStream *output_stream; - GInputStream *input_stream; - CamelStream *camel_stream; - GByteArray *buffer; - GError *error = NULL; - - object = g_async_result_get_source_object (G_ASYNC_RESULT (simple)); - - output_stream = g_file_replace_finish (file, result, &error); - - if (error != NULL) { - g_simple_async_result_set_from_error (simple, error); - g_simple_async_result_complete (simple); - g_object_unref (simple); - g_error_free (error); - return; - } - - /* Extract a MIME message from the composer. */ - composer = E_MSG_COMPOSER (object); - message = e_msg_composer_get_message_draft (composer, &error); - if (error != NULL) { - g_simple_async_result_set_from_error (simple, error); - g_simple_async_result_complete (simple); - g_object_unref (output_stream); - g_object_unref (simple); - return; - } - - /* Decode the MIME part to an in-memory buffer. We have to do - * this because CamelStream is synchronous-only, and using threads - * is dangerous because CamelDataWrapper is not reentrant. */ - buffer = g_byte_array_new (); - camel_stream = camel_stream_mem_new (); - camel_stream_mem_set_byte_array ( - CAMEL_STREAM_MEM (camel_stream), buffer); - camel_data_wrapper_decode_to_stream ( - CAMEL_DATA_WRAPPER (message), camel_stream, NULL); - g_object_unref (message); - g_object_unref (camel_stream); - - /* Load the buffer into a GMemoryInputStream. - * But watch out for zero length MIME parts. */ - input_stream = g_memory_input_stream_new (); - if (buffer->len > 0) - g_memory_input_stream_add_data ( - G_MEMORY_INPUT_STREAM (input_stream), - buffer->data, (gssize) buffer->len, - (GDestroyNotify) g_free); - g_byte_array_free (buffer, FALSE); - - /* Splice the input and output streams */ - g_output_stream_splice_async ( - G_OUTPUT_STREAM (output_stream), input_stream, - G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | - G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, - G_PRIORITY_DEFAULT, NULL, (GAsyncReadyCallback) - autosave_snapshot_splice_cb, simple); - - g_object_unref (output_stream); - g_object_unref (input_stream); -} - -void -e_composer_autosave_snapshot_async (EMsgComposer *composer, - GAsyncReadyCallback callback, - gpointer user_data) -{ - AutosaveState *state; - GSimpleAsyncResult *simple; - GError *error = NULL; - - g_return_if_fail (E_IS_MSG_COMPOSER (composer)); - - state = g_object_get_data (G_OBJECT (composer), "autosave"); - g_return_if_fail (state != NULL); - - simple = g_simple_async_result_new ( - G_OBJECT (composer), callback, user_data, - e_composer_autosave_snapshot_async); - - /* If the contents are unchanged, exit early. */ - if (!gtkhtml_editor_get_changed (GTKHTML_EDITOR (composer))) { - g_simple_async_result_complete (simple); - g_object_unref (simple); - return; - } - - /* Open the autosave file on-demand. */ - if (!composer_autosave_state_open (state, &error)) { - g_simple_async_result_set_from_error (simple, error); - g_simple_async_result_complete (simple); - g_object_unref (simple); - return; - } - - /* Overwrite the file. */ - g_file_replace_async ( - state->file, NULL, FALSE, G_FILE_CREATE_PRIVATE, - G_PRIORITY_DEFAULT, NULL, (GAsyncReadyCallback) - autosave_snapshot_cb, simple); -} - -gboolean -e_composer_autosave_snapshot_finish (EMsgComposer *composer, - GAsyncResult *result, - GError **error) -{ - GSimpleAsyncResult *simple; - - g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), FALSE); - g_return_val_if_fail (G_IS_SIMPLE_ASYNC_RESULT (result), FALSE); - g_return_val_if_fail (error == NULL || *error == NULL, FALSE); - - simple = G_SIMPLE_ASYNC_RESULT (result); - - /* Success is assumed in the absense of a GError. */ - return !g_simple_async_result_propagate_error (simple, error); -} - -gchar * -e_composer_autosave_get_filename (EMsgComposer *composer) -{ - AutosaveState *state; - - g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL); - - state = g_object_get_data (G_OBJECT (composer), "autosave"); - g_return_val_if_fail (state != NULL, NULL); - - return g_file_get_path (state->file); -} - -gboolean -e_composer_autosave_get_enabled (EMsgComposer *composer) -{ - AutosaveState *state; - - g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), FALSE); - - state = g_object_get_data (G_OBJECT (composer), "autosave"); - g_return_val_if_fail (state != NULL, FALSE); - - return state->enabled; -} - -void -e_composer_autosave_set_enabled (EMsgComposer *composer, - gboolean enabled) -{ - AutosaveState *state; - - g_return_if_fail (E_IS_MSG_COMPOSER (composer)); - - state = g_object_get_data (G_OBJECT (composer), "autosave"); - g_return_if_fail (state != NULL); - - state->enabled = enabled; -} diff --git a/composer/e-composer-autosave.h b/composer/e-composer-autosave.h deleted file mode 100644 index 6f55f10b1a..0000000000 --- a/composer/e-composer-autosave.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) version 3. - * - * 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with the program; if not, see - * - * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) - */ - -#ifndef E_COMPOSER_AUTOSAVE_H -#define E_COMPOSER_AUTOSAVE_H - -#include "e-composer-common.h" -#include "e-msg-composer.h" - -G_BEGIN_DECLS - -GList * e_composer_autosave_find_orphans (GError **error); - -void e_composer_autosave_register (EMsgComposer *composer); -void e_composer_autosave_unregister (EMsgComposer *composer); -void e_composer_autosave_snapshot_async - (EMsgComposer *composer, - GAsyncReadyCallback callback, - gpointer user_data); -gboolean e_composer_autosave_snapshot_finish - (EMsgComposer *composer, - GAsyncResult *result, - GError **error); -gchar * e_composer_autosave_get_filename (EMsgComposer *composer); -gboolean e_composer_autosave_get_enabled (EMsgComposer *composer); -void e_composer_autosave_set_enabled (EMsgComposer *composer, - gboolean enabled); - -G_END_DECLS - -#endif /* E_COMPOSER_AUTOSAVE_H */ diff --git a/composer/e-composer-private.h b/composer/e-composer-private.h index 8d1408956d..026ed96954 100644 --- a/composer/e-composer-private.h +++ b/composer/e-composer-private.h @@ -28,7 +28,6 @@ #include #include "e-composer-actions.h" -#include "e-composer-autosave.h" #include "e-composer-header-table.h" #include "e-util/e-binding.h" #include "e-util/e-charset.h" diff --git a/composer/e-msg-composer.c b/composer/e-msg-composer.c index 65523677e9..480bfbe3bd 100644 --- a/composer/e-msg-composer.c +++ b/composer/e-msg-composer.c @@ -45,12 +45,13 @@ #include #include -#include "e-util/e-dialog-utils.h" +#include "e-util/e-account-utils.h" #include "e-util/e-alert-dialog.h" +#include "e-util/e-dialog-utils.h" +#include "e-util/e-extensible.h" #include "e-util/e-plugin-ui.h" -#include "e-util/e-util-private.h" -#include "e-util/e-account-utils.h" #include "e-util/e-signature-utils.h" +#include "e-util/e-util-private.h" #include "e-signature-combo-box.h" #include "shell/e-shell.h" #include "em-format/em-format.h" @@ -59,7 +60,6 @@ #include "e-msg-composer.h" #include "e-attachment.h" -#include "e-composer-autosave.h" #include "e-composer-private.h" #include "e-composer-header-table.h" @@ -113,10 +113,11 @@ static void handle_multipart_signed (EMsgComposer *composer, CamelMultipart *multipart, gint depth); -G_DEFINE_TYPE ( +G_DEFINE_TYPE_WITH_CODE ( EMsgComposer, e_msg_composer, - GTKHTML_TYPE_EDITOR) + GTKHTML_TYPE_EDITOR, + G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL)) /** * emcu_part_to_html: @@ -1439,71 +1440,6 @@ set_editor_text (EMsgComposer *composer, g_free (body); } -/* Commands. */ - -static void -autosave_load_draft_cb (EMsgComposer *composer, - GAsyncResult *result, - gchar *filename) -{ - GError *error = NULL; - - if (e_composer_autosave_snapshot_finish (composer, result, &error)) - g_unlink (filename); - - else { - e_alert_run_dialog_for_args ( - GTK_WINDOW (composer), - "mail-composer:no-autosave", - (filename != NULL) ? filename : "", - (error != NULL) ? error->message : - _("Unable to reconstruct message from autosave file"), - NULL); - - if (error != NULL) - g_error_free (error); - } - - g_free (filename); -} - -static EMsgComposer * -autosave_load_draft (EShell *shell, - const gchar *filename) -{ - CamelStream *stream; - CamelMimeMessage *message; - EMsgComposer *composer; - - g_return_val_if_fail (E_IS_SHELL (shell), NULL); - g_return_val_if_fail (filename != NULL, NULL); - - stream = camel_stream_fs_new_with_name ( - filename, O_RDONLY, 0, NULL); - if (stream == NULL) - return NULL; - - message = camel_mime_message_new (); - camel_data_wrapper_construct_from_stream ( - CAMEL_DATA_WRAPPER (message), stream, NULL); - g_object_unref (stream); - - composer = e_msg_composer_new_with_message (shell, message); - if (composer) { - /* Mark the message as changed so it gets autosaved again, - * then we can safely remove the old autosave file in the - * callback function. */ - gtkhtml_editor_set_changed (GTKHTML_EDITOR (composer), TRUE); - e_composer_autosave_snapshot_async ( - composer, (GAsyncReadyCallback) - autosave_load_draft_cb, g_strdup (filename)); - - gtk_widget_show (GTK_WIDGET (composer)); - } - - return composer; -} - /* Miscellaneous callbacks. */ static void @@ -1830,7 +1766,6 @@ msg_composer_finalize (GObject *object) { EMsgComposer *composer = E_MSG_COMPOSER (object); - e_composer_autosave_unregister (composer); e_composer_private_finalize (composer); /* Chain up to parent's finalize() method. */ @@ -1980,14 +1915,14 @@ msg_composer_constructed (GObject *object) store, "row-inserted", G_CALLBACK (attachment_store_changed_cb), composer); - e_composer_autosave_register (composer); - /* Initialization may have tripped the "changed" state. */ gtkhtml_editor_set_changed (editor, FALSE); id = "org.gnome.evolution.composer"; e_plugin_ui_register_manager (ui_manager, id, composer); e_plugin_ui_enable_manager (ui_manager, id); + + e_extensible_load_extensions (E_EXTENSIBLE (composer)); } static void @@ -2068,7 +2003,7 @@ msg_composer_key_press_event (GtkWidget *widget, #ifdef HAVE_XFREE if (event->keyval == XF86XK_Send) { - g_signal_emit (G_OBJECT (composer), signals[SEND], 0); + e_msg_composer_send (composer); return TRUE; } #endif /* HAVE_XFREE */ @@ -3250,9 +3185,16 @@ e_msg_composer_get_shell (EMsgComposer *composer) void e_msg_composer_send (EMsgComposer *composer) { + GtkhtmlEditor *editor; + g_return_if_fail (E_IS_MSG_COMPOSER (composer)); + editor = GTKHTML_EDITOR (composer); + g_signal_emit (composer, signals[SEND], 0); + + /* XXX This should be elsewhere. */ + gtkhtml_editor_set_changed (editor, FALSE); } /** @@ -4093,13 +4035,6 @@ e_msg_composer_get_raw_message_text (EMsgComposer *composer) return array; } -void -e_msg_composer_set_enable_autosave (EMsgComposer *composer, - gboolean enabled) -{ - e_composer_autosave_set_enabled (composer, enabled); -} - gboolean e_msg_composer_is_exiting (EMsgComposer *composer) { @@ -4201,49 +4136,6 @@ e_msg_composer_load_from_file (EShell *shell, return composer; } -void -e_msg_composer_check_autosave (EShell *shell) -{ - GtkWindow *parent; - GList *orphans = NULL; - gint response; - GError *error = NULL; - - g_return_if_fail (E_IS_SHELL (shell)); - - parent = e_shell_get_active_window (shell); - - /* Look for orphaned autosave files. */ - orphans = e_composer_autosave_find_orphans (&error); - if (orphans == NULL) { - if (error != NULL) { - g_warning ("%s", error->message); - g_error_free (error); - } - return; - } - - /* Ask if the user wants to recover the orphaned files. */ - response = e_alert_run_dialog_for_args ( - parent, "mail-composer:recover-autosave", NULL); - - /* Based on the user's response, recover or delete them. */ - while (orphans != NULL) { - const gchar *filename = orphans->data; - EMsgComposer *composer; - - if (response == GTK_RESPONSE_YES) { - /* FIXME: composer is never used */ - composer = autosave_load_draft (shell, filename); - } else { - g_unlink (filename); - } - - g_free (orphans->data); - orphans = g_list_delete_link (orphans, orphans); - } -} - void e_msg_composer_set_alternative (EMsgComposer *composer, gboolean alt) diff --git a/composer/e-msg-composer.h b/composer/e-msg-composer.h index 4c51929f06..5cb439512c 100644 --- a/composer/e-msg-composer.h +++ b/composer/e-msg-composer.h @@ -133,10 +133,6 @@ CamelInternetAddress * void e_msg_composer_clear_inlined_table (EMsgComposer *composer); -void e_msg_composer_set_enable_autosave - (EMsgComposer *composer, - gboolean enabled); - void e_msg_composer_add_message_attachments (EMsgComposer *composer, CamelMimeMessage *message, @@ -148,7 +144,6 @@ gboolean e_msg_composer_can_close (EMsgComposer *composer, EMsgComposer * e_msg_composer_load_from_file (EShell *shell, const gchar *filename); -void e_msg_composer_check_autosave (EShell *shell); void e_msg_composer_reply_indent (EMsgComposer *composer); diff --git a/configure.ac b/configure.ac index e38d8c31ee..c7e2c8749a 100644 --- a/configure.ac +++ b/configure.ac @@ -1803,6 +1803,7 @@ modules/calendar/Makefile modules/mail/Makefile modules/mailto-handler/Makefile modules/network-manager/Makefile +modules/composer-autosave/Makefile modules/connman/Makefile modules/plugin-lib/Makefile modules/plugin-mono/Makefile diff --git a/mail/em-composer-utils.c b/mail/em-composer-utils.c index aa2a550197..82ba4bcd5c 100644 --- a/mail/em-composer-utils.c +++ b/mail/em-composer-utils.c @@ -50,7 +50,6 @@ #include "em-composer-utils.h" #include "composer/e-msg-composer.h" #include "composer/e-composer-actions.h" -#include "composer/e-composer-autosave.h" #include "composer/e-composer-post-header.h" #include "em-folder-selector.h" #include "em-folder-tree.h" @@ -255,10 +254,8 @@ composer_send_queued_cb (CamelFolder *folder, CamelMimeMessage *msg, CamelMessag /* queue a message send */ mail_send (); } - } else { - e_msg_composer_set_enable_autosave (send->composer, TRUE); + } else gtk_widget_show (GTK_WIDGET (send->composer)); - } camel_message_info_free (info); @@ -555,8 +552,6 @@ em_utils_composer_send_cb (EMsgComposer *composer) send->composer = g_object_ref (composer); gtk_widget_hide (GTK_WIDGET (composer)); - e_msg_composer_set_enable_autosave (composer, FALSE); - mail_append_mail ( folder, message, info, composer_send_queued_cb, send); diff --git a/modules/composer-autosave/Makefile.am b/modules/composer-autosave/Makefile.am new file mode 100644 index 0000000000..0b802e11bf --- /dev/null +++ b/modules/composer-autosave/Makefile.am @@ -0,0 +1,28 @@ +module_LTLIBRARIES = libevolution-module-composer-autosave.la + +libevolution_module_composer_autosave_la_CPPFLAGS = \ + $(AM_CPPFLAGS) \ + -I$(top_srcdir) \ + -I$(top_srcdir)/widgets \ + -DG_LOG_DOMAIN=\"evolution-composer-autosave\" \ + $(EVOLUTION_MAIL_CFLAGS) \ + $(GNOME_PLATFORM_CFLAGS) + +libevolution_module_composer_autosave_la_SOURCES = \ + evolution-composer-autosave.c \ + e-autosave-utils.c \ + e-autosave-utils.h \ + e-composer-autosave.c \ + e-composer-registry.c + +libevolution_module_composer_autosave_la_LIBADD = \ + $(top_builddir)/shell/libeshell.la \ + $(top_builddir)/composer/libcomposer.la \ + $(top_builddir)/widgets/misc/libemiscwidgets.la \ + $(EVOLUTION_MAIL_LIBS) \ + $(GNOME_PLATFORM_LIBS) + +libevolution_module_composer_autosave_la_LDFLAGS = \ + -module -avoid-version $(NO_UNDEFINED) + +-include $(top_srcdir)/git.mk diff --git a/modules/composer-autosave/e-autosave-utils.c b/modules/composer-autosave/e-autosave-utils.c new file mode 100644 index 0000000000..ec0e322322 --- /dev/null +++ b/modules/composer-autosave/e-autosave-utils.c @@ -0,0 +1,499 @@ +/* + * e-autosave-utils.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + */ + +#include "e-autosave-utils.h" + +#include +#include +#include + +#include + +#define SNAPSHOT_FILE_KEY "e-composer-snapshot-file" +#define SNAPSHOT_FILE_PREFIX ".evolution-composer.autosave" +#define SNAPSHOT_FILE_SEED SNAPSHOT_FILE_PREFIX "-XXXXXX" + +typedef struct _LoadContext LoadContext; +typedef struct _SaveContext SaveContext; + +struct _LoadContext { + EMsgComposer *composer; +}; + +struct _SaveContext { + GCancellable *cancellable; +}; + +static void +load_context_free (LoadContext *context) +{ + if (context->composer != NULL) + g_object_unref (context->composer); + + g_slice_free (LoadContext, context); +} + +static void +save_context_free (SaveContext *context) +{ + if (context->cancellable != NULL) + g_object_unref (context->cancellable); + + g_slice_free (SaveContext, context); +} + +static void +delete_snapshot_file (GFile *snapshot_file) +{ + g_file_delete (snapshot_file, NULL, NULL); + g_object_unref (snapshot_file); +} + +static GFile * +create_snapshot_file (EMsgComposer *composer, + GError **error) +{ + GFile *snapshot_file; + const gchar *user_data_dir; + gchar *path; + gint fd; + + snapshot_file = e_composer_get_snapshot_file (composer); + + if (G_IS_FILE (snapshot_file)) + return snapshot_file; + + user_data_dir = e_get_user_data_dir (); + path = g_build_filename (user_data_dir, SNAPSHOT_FILE_SEED, NULL); + + /* g_mkstemp() modifies the XXXXXX part of the + * template string to form the actual filename. */ + errno = 0; + fd = g_mkstemp (path); + if (fd == -1) { + g_set_error ( + error, G_FILE_ERROR, + g_file_error_from_errno (errno), + "%s", g_strerror (errno)); + g_free (path); + return FALSE; + } + + close (fd); + + snapshot_file = g_file_new_for_path (path); + + /* Save the GFile for subsequent snapshots. */ + g_object_set_data_full ( + G_OBJECT (composer), + SNAPSHOT_FILE_KEY, snapshot_file, + (GDestroyNotify) delete_snapshot_file); + + return snapshot_file; +} + +static void +load_snapshot_loaded_cb (GFile *snapshot_file, + GAsyncResult *result, + GSimpleAsyncResult *simple) +{ + EShell *shell; + GObject *object; + LoadContext *context; + EMsgComposer *composer; + CamelMimeMessage *message; + CamelStream *camel_stream; + gchar *contents = NULL; + gsize length; + GError *error = NULL; + + context = g_simple_async_result_get_op_res_gpointer (simple); + + g_file_load_contents_finish ( + snapshot_file, result, &contents, &length, NULL, &error); + + if (error != NULL) { + g_warn_if_fail (contents == NULL); + g_simple_async_result_set_from_error (simple, error); + g_simple_async_result_complete (simple); + g_error_free (error); + return; + } + + /* Create an in-memory buffer for the MIME parser to read from. + * We have to do this because CamelStreams are syncrhonous-only, + * and feeding the parser a direct file stream would block. */ + message = camel_mime_message_new (); + camel_stream = camel_stream_mem_new_with_buffer (contents, length); + camel_data_wrapper_construct_from_stream ( + CAMEL_DATA_WRAPPER (message), camel_stream, &error); + g_object_unref (camel_stream); + g_free (contents); + + if (error != NULL) { + g_simple_async_result_set_from_error (simple, error); + g_simple_async_result_complete (simple); + g_object_unref (message); + g_error_free (error); + return; + } + + /* g_async_result_get_source_object() returns a new reference. */ + object = g_async_result_get_source_object (G_ASYNC_RESULT (simple)); + + /* Create a new composer window from the loaded message and + * restore its snapshot file so it continues auto-saving to + * the same file. */ + shell = E_SHELL (object); + g_object_ref (snapshot_file); + composer = e_msg_composer_new_with_message (shell, message); + g_object_set_data_full ( + G_OBJECT (composer), + SNAPSHOT_FILE_KEY, snapshot_file, + (GDestroyNotify) delete_snapshot_file); + context->composer = g_object_ref_sink (composer); + g_object_unref (message); + + g_object_unref (object); + + g_simple_async_result_complete (simple); + g_object_unref (simple); +} + +static void +save_snapshot_splice_cb (GOutputStream *output_stream, + GAsyncResult *result, + GSimpleAsyncResult *simple) +{ + GError *error = NULL; + + g_output_stream_splice_finish (output_stream, result, &error); + + if (error != NULL) { + g_simple_async_result_set_from_error (simple, error); + g_error_free (error); + } + + g_simple_async_result_complete (simple); + g_object_unref (simple); +} + +static void +save_snapshot_replace_cb (GFile *snapshot_file, + GAsyncResult *result, + GSimpleAsyncResult *simple) +{ + GObject *object; + EMsgComposer *composer; + SaveContext *context; + CamelMimeMessage *message; + GFileOutputStream *output_stream; + GInputStream *input_stream; + CamelStream *camel_stream; + GByteArray *buffer; + GError *error = NULL; + + context = g_simple_async_result_get_op_res_gpointer (simple); + + output_stream = g_file_replace_finish (snapshot_file, result, &error); + + if (error != NULL) { + g_warn_if_fail (output_stream == NULL); + g_simple_async_result_set_from_error (simple, error); + g_simple_async_result_complete (simple); + g_object_unref (simple); + g_error_free (error); + return; + } + + g_return_if_fail (G_IS_OUTPUT_STREAM (output_stream)); + + /* g_async_result_get_source_object() returns a new reference. */ + object = g_async_result_get_source_object (G_ASYNC_RESULT (simple)); + + /* Extract a MIME message from the composer. */ + composer = E_MSG_COMPOSER (object); + message = e_msg_composer_get_message_draft (composer, &error); + + g_object_unref (object); + + if (error != NULL) { + g_warn_if_fail (message == NULL); + g_simple_async_result_set_from_error (simple, error); + g_simple_async_result_complete (simple); + g_object_unref (output_stream); + g_object_unref (simple); + g_error_free (error); + return; + } + + g_return_if_fail (CAMEL_IS_MIME_MESSAGE (message)); + + /* Decode the message to an in-memory buffer. We have to do this + * because CamelStreams are synchronous-only, and using threads is + * dangerous because CamelDataWrapper is not reentrant. */ + buffer = g_byte_array_new (); + camel_stream = camel_stream_mem_new (); + camel_stream_mem_set_byte_array ( + CAMEL_STREAM_MEM (camel_stream), buffer); + camel_data_wrapper_decode_to_stream ( + CAMEL_DATA_WRAPPER (message), camel_stream, NULL); + g_object_unref (camel_stream); + g_object_unref (message); + + /* Load the buffer into a GMemoryInputStream. */ + input_stream = g_memory_input_stream_new (); + if (buffer->len > 0) + g_memory_input_stream_add_data ( + G_MEMORY_INPUT_STREAM (input_stream), + buffer->data, (gssize) buffer->len, + (GDestroyNotify) g_free); + g_byte_array_free (buffer, FALSE); + + /* Splice the input and output streams. */ + g_output_stream_splice_async ( + G_OUTPUT_STREAM (output_stream), input_stream, + G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | + G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, + G_PRIORITY_DEFAULT, context->cancellable, + (GAsyncReadyCallback) save_snapshot_splice_cb, + simple); + + g_object_unref (input_stream); + g_object_unref (output_stream); +} + +static EMsgComposer * +composer_registry_lookup (GQueue *registry, + const gchar *basename) +{ + GList *iter; + + /* Find the composer with the given snapshot filename. */ + for (iter = registry->head; iter != NULL; iter = iter->next) { + EMsgComposer *composer; + GFile *snapshot_file; + gchar *snapshot_name; + + composer = E_MSG_COMPOSER (iter->data); + snapshot_file = e_composer_get_snapshot_file (composer); + + if (!G_IS_FILE (snapshot_file)) + continue; + + snapshot_name = g_file_get_basename (snapshot_file); + if (g_strcmp0 (basename, snapshot_name) == 0) { + g_free (snapshot_name); + return composer; + } + + g_free (snapshot_name); + } + + return NULL; +} + +GList * +e_composer_find_orphans (GQueue *registry, + GError **error) +{ + GDir *dir; + const gchar *dirname; + const gchar *basename; + GList *orphans = NULL; + + g_return_val_if_fail (registry != NULL, NULL); + + dirname = e_get_user_data_dir (); + dir = g_dir_open (dirname, 0, error); + if (dir == NULL) + return NULL; + + /* Scan the user data directory for snapshot files. */ + while ((basename = g_dir_read_name (dir)) != NULL) { + const gchar *errmsg; + gchar *filename; + struct stat st; + + /* Is this a snapshot file? */ + if (!g_str_has_prefix (basename, SNAPSHOT_FILE_PREFIX)) + continue; + + /* Is this an orphaned snapshot file? */ + if (composer_registry_lookup (registry, basename) != NULL) + continue; + + filename = g_build_filename (dirname, basename, NULL); + + /* Try to examine the snapshot file. Failure here + * is non-fatal; just emit a warning and move on. */ + errno = 0; + if (g_stat (filename, &st) < 0) { + errmsg = g_strerror (errno); + g_warning ("%s: %s", filename, errmsg); + g_free (filename); + continue; + } + + /* If the file is empty, delete it. Failure here + * is non-fatal; just emit a warning and move on. */ + if (st.st_size == 0) { + errno = 0; + if (g_unlink (filename) < 0) { + errmsg = g_strerror (errno); + g_warning ("%s: %s", filename, errmsg); + } + g_free (filename); + continue; + } + + orphans = g_list_prepend ( + orphans, g_file_new_for_path (filename)); + + g_free (filename); + } + + g_dir_close (dir); + + return g_list_reverse (orphans); +} + +void +e_composer_load_snapshot (EShell *shell, + GFile *snapshot_file, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + LoadContext *context; + + g_return_if_fail (E_IS_SHELL (shell)); + g_return_if_fail (G_IS_FILE (snapshot_file)); + + context = g_slice_new0 (LoadContext); + + simple = g_simple_async_result_new ( + G_OBJECT (shell), callback, user_data, + e_composer_load_snapshot); + + g_simple_async_result_set_op_res_gpointer ( + simple, context, (GDestroyNotify) load_context_free); + + g_file_load_contents_async ( + snapshot_file, cancellable, (GAsyncReadyCallback) + load_snapshot_loaded_cb, simple); +} + +EMsgComposer * +e_composer_load_snapshot_finish (EShell *shell, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + LoadContext *context; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (shell), + e_composer_load_snapshot), NULL); + + simple = G_SIMPLE_ASYNC_RESULT (result); + context = g_simple_async_result_get_op_res_gpointer (simple); + + if (g_simple_async_result_propagate_error (simple, error)) + return NULL; + + g_return_val_if_fail (E_IS_MSG_COMPOSER (context->composer), NULL); + + return g_object_ref (context->composer); +} + +void +e_composer_save_snapshot (EMsgComposer *composer, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + SaveContext *context; + GFile *snapshot_file; + GError *error = NULL; + + g_return_if_fail (E_IS_MSG_COMPOSER (composer)); + + context = g_slice_new0 (SaveContext); + + if (G_IS_CANCELLABLE (cancellable)) + context->cancellable = g_object_ref (cancellable); + + simple = g_simple_async_result_new ( + G_OBJECT (composer), callback, user_data, + e_composer_save_snapshot); + + g_simple_async_result_set_op_res_gpointer ( + simple, context, (GDestroyNotify) save_context_free); + + snapshot_file = e_composer_get_snapshot_file (composer); + + if (!G_IS_FILE (snapshot_file)) + snapshot_file = create_snapshot_file (composer, &error); + + if (error != NULL) { + g_warn_if_fail (snapshot_file == NULL); + g_simple_async_result_set_from_error (simple, error); + g_simple_async_result_complete (simple); + g_object_unref (simple); + g_error_free (error); + return; + } + + g_return_if_fail (G_IS_FILE (snapshot_file)); + + g_file_replace_async ( + snapshot_file, NULL, FALSE, + G_FILE_CREATE_PRIVATE, G_PRIORITY_DEFAULT, + context->cancellable, (GAsyncReadyCallback) + save_snapshot_replace_cb, simple); +} + +gboolean +e_composer_save_snapshot_finish (EMsgComposer *composer, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (composer), + e_composer_save_snapshot), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + + /* Success is assumed unless a GError is set. */ + return !g_simple_async_result_propagate_error (simple, error); +} + +GFile * +e_composer_get_snapshot_file (EMsgComposer *composer) +{ + g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL); + + return g_object_get_data (G_OBJECT (composer), SNAPSHOT_FILE_KEY); +} diff --git a/modules/composer-autosave/e-autosave-utils.h b/modules/composer-autosave/e-autosave-utils.h new file mode 100644 index 0000000000..3a33d6b90a --- /dev/null +++ b/modules/composer-autosave/e-autosave-utils.h @@ -0,0 +1,48 @@ +/* + * e-autosave-utils.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + */ + +#ifndef E_AUTOSAVE_UTILS_H +#define E_AUTOSAVE_UTILS_H + +#include +#include + +G_BEGIN_DECLS + +GList * e_composer_find_orphans (GQueue *registry, + GError **error); +void e_composer_load_snapshot (EShell *shell, + GFile *snapshot_file, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +EMsgComposer * e_composer_load_snapshot_finish (EShell *shell, + GAsyncResult *result, + GError **error); +void e_composer_save_snapshot (EMsgComposer *composer, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_composer_save_snapshot_finish (EMsgComposer *composer, + GAsyncResult *result, + GError **error); +GFile * e_composer_get_snapshot_file (EMsgComposer *composer); + +G_END_DECLS + +#endif /* E_AUTOSAVE_UTILS_H */ diff --git a/modules/composer-autosave/e-composer-autosave.c b/modules/composer-autosave/e-composer-autosave.c new file mode 100644 index 0000000000..b0a22ad4cf --- /dev/null +++ b/modules/composer-autosave/e-composer-autosave.c @@ -0,0 +1,227 @@ +/* + * e-composer-autosave.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + */ + +#include +#include +#include + +#include "e-autosave-utils.h" + +/* Standard GObject macros */ +#define E_TYPE_COMPOSER_AUTOSAVE \ + (e_composer_autosave_get_type ()) +#define E_COMPOSER_AUTOSAVE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_COMPOSER_AUTOSAVE, EComposerAutosave)) + +#define AUTOSAVE_INTERVAL 60 /* seconds */ + +typedef struct _EComposerAutosave EComposerAutosave; +typedef struct _EComposerAutosaveClass EComposerAutosaveClass; + +struct _EComposerAutosave { + EExtension parent; + + GCancellable *cancellable; + guint timeout_id; + + /* Composer contents have changed since + * the last auto-save or explicit save. */ + gboolean changed; + + /* Prevent error dialogs from piling up. */ + gboolean error_shown; +}; + +struct _EComposerAutosaveClass { + EExtensionClass parent_class; +}; + +/* Forward Declarations */ +GType e_composer_autosave_get_type (void); +void e_composer_autosave_type_register (GTypeModule *type_module); + +G_DEFINE_DYNAMIC_TYPE ( + EComposerAutosave, + e_composer_autosave, + E_TYPE_EXTENSION) + +static void +composer_autosave_finished_cb (EMsgComposer *composer, + GAsyncResult *result, + EComposerAutosave *autosave) +{ + GFile *snapshot_file; + GError *error = NULL; + + snapshot_file = e_composer_get_snapshot_file (composer); + e_composer_save_snapshot_finish (composer, result, &error); + + /* Return silently if we were cancelled. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_error_free (error); + + else if (error != NULL) { + gchar *basename; + + if (G_IS_FILE (snapshot_file)) + basename = g_file_get_basename (snapshot_file); + else + basename = g_strdup (" "); + + /* Only show one error dialog at a time. */ + if (!autosave->error_shown) { + autosave->error_shown = TRUE; + e_alert_run_dialog_for_args ( + GTK_WINDOW (composer), + "mail-composer:no-autosave", + basename, error->message, NULL); + autosave->error_shown = FALSE; + } else + g_warning ("%s: %s", basename, error->message); + + g_free (basename); + g_error_free (error); + } + + g_object_unref (autosave); +} + +static gboolean +composer_autosave_timeout_cb (EComposerAutosave *autosave) +{ + EExtensible *extensible; + + extensible = e_extension_get_extensible (E_EXTENSION (autosave)); + + /* User may have reverted or explicitly saved + * the changes since the timeout was scheduled. */ + if (autosave->changed) { + + /* Cancel the previous snapshot if it's still in + * progress and start a new snapshot operation. */ + g_cancellable_cancel (autosave->cancellable); + g_object_unref (autosave->cancellable); + autosave->cancellable = g_cancellable_new (); + + e_composer_save_snapshot ( + E_MSG_COMPOSER (extensible), + autosave->cancellable, + (GAsyncReadyCallback) + composer_autosave_finished_cb, + g_object_ref (autosave)); + } + + autosave->timeout_id = 0; + autosave->changed = FALSE; + + return FALSE; +} + +static void +composer_autosave_changed_cb (EComposerAutosave *autosave) +{ + GtkhtmlEditor *editor; + EExtensible *extensible; + + extensible = e_extension_get_extensible (E_EXTENSION (autosave)); + + editor = GTKHTML_EDITOR (extensible); + autosave->changed = gtkhtml_editor_get_changed (editor); + + if (autosave->changed && autosave->timeout_id == 0) + autosave->timeout_id = g_timeout_add_seconds ( + AUTOSAVE_INTERVAL, (GSourceFunc) + composer_autosave_timeout_cb, autosave); +} + +static void +composer_autosave_dispose (GObject *object) +{ + EComposerAutosave *autosave; + GObjectClass *parent_class; + + autosave = E_COMPOSER_AUTOSAVE (object); + + /* Cancel any snapshots in progress. */ + if (autosave->cancellable != NULL) { + g_cancellable_cancel (autosave->cancellable); + g_object_unref (autosave->cancellable); + autosave->cancellable = NULL; + } + + if (autosave->timeout_id > 0) { + g_source_remove (autosave->timeout_id); + autosave->timeout_id = 0; + } + + /* Chain up to parent's dispose() method. */ + parent_class = G_OBJECT_CLASS (e_composer_autosave_parent_class); + parent_class->dispose (object); +} + +static void +composer_autosave_constructed (GObject *object) +{ + EExtensible *extensible; + GObjectClass *parent_class; + + /* Chain up to parent's constructed() method. */ + parent_class = G_OBJECT_CLASS (e_composer_autosave_parent_class); + parent_class->constructed (object); + + extensible = e_extension_get_extensible (E_EXTENSION (object)); + + g_signal_connect_swapped ( + extensible, "notify::changed", + G_CALLBACK (composer_autosave_changed_cb), object); +} + +static void +e_composer_autosave_class_init (EComposerAutosaveClass *class) +{ + GObjectClass *object_class; + EExtensionClass *extension_class; + + object_class = G_OBJECT_CLASS (class); + object_class->dispose = composer_autosave_dispose; + object_class->constructed = composer_autosave_constructed; + + extension_class = E_EXTENSION_CLASS (class); + extension_class->extensible_type = E_TYPE_MSG_COMPOSER; +} + +static void +e_composer_autosave_class_finalize (EComposerAutosaveClass *class) +{ +} + +static void +e_composer_autosave_init (EComposerAutosave *autosave) +{ + autosave->cancellable = g_cancellable_new (); +} + +void +e_composer_autosave_type_register (GTypeModule *type_module) +{ + /* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration + * function, so we have to wrap it with a public function in + * order to register types from a separate compilation unit. */ + e_composer_autosave_register_type (type_module); +} diff --git a/modules/composer-autosave/e-composer-registry.c b/modules/composer-autosave/e-composer-registry.c new file mode 100644 index 0000000000..a48464addd --- /dev/null +++ b/modules/composer-autosave/e-composer-registry.c @@ -0,0 +1,236 @@ +/* + * e-composer-registry.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + */ + +#include +#include +#include +#include +#include +#include + +#include "e-autosave-utils.h" + +/* Standard GObject macros */ +#define E_TYPE_COMPOSER_REGISTRY \ + (e_composer_registry_get_type ()) +#define E_COMPOSER_REGISTRY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_COMPOSER_REGISTRY, EComposerRegistry)) + +typedef struct _EComposerRegistry EComposerRegistry; +typedef struct _EComposerRegistryClass EComposerRegistryClass; + +struct _EComposerRegistry { + EExtension parent; + GQueue composers; + gboolean orphans_restored; +}; + +struct _EComposerRegistryClass { + EExtensionClass parent_class; +}; + +/* Forward Declarations */ +GType e_composer_registry_get_type (void); +void e_composer_registry_type_register (GTypeModule *type_module); + +G_DEFINE_DYNAMIC_TYPE ( + EComposerRegistry, + e_composer_registry, + E_TYPE_EXTENSION) + +static void +composer_registry_recovered_cb (EShell *shell, + GAsyncResult *result, + EComposerRegistry *registry) +{ + EMsgComposer *composer; + GError *error = NULL; + + composer = e_composer_load_snapshot_finish (shell, result, &error); + + if (error != NULL) { + /* FIXME Show an alert dialog here explaining + * why we could not recover the message. + * Will need a new error XML entry. */ + g_warn_if_fail (composer == NULL); + g_warning ("%s", error->message); + g_error_free (error); + goto exit; + } + + gtk_widget_show (GTK_WIDGET (composer)); + + g_object_unref (composer); + +exit: + g_object_unref (registry); +} + +static gboolean +composer_registry_map_event_cb (GtkWindow *parent, + GdkEvent *event, + EComposerRegistry *registry) +{ + EExtensible *extensible; + GList *orphans; + gint response; + GError *error = NULL; + + extensible = e_extension_get_extensible (E_EXTENSION (registry)); + + /* Look for orphaned auto-save files. */ + orphans = e_composer_find_orphans ( + ®istry->composers, &error); + if (orphans == NULL) { + if (error != NULL) { + g_warning ("%s", error->message); + g_error_free (error); + } + goto exit; + } + + /* Ask if the user wants to recover the orphaned files. */ + response = e_alert_run_dialog_for_args ( + parent, "mail-composer:recover-autosave", NULL); + + /* Based on the user's reponse, recover or delete them. */ + while (orphans != NULL) { + GFile *file = orphans->data; + + if (response == GTK_RESPONSE_YES) + e_composer_load_snapshot ( + E_SHELL (extensible), + file, NULL, (GAsyncReadyCallback) + composer_registry_recovered_cb, + g_object_ref (registry)); + else + g_file_delete (file, NULL, NULL); + + g_object_unref (file); + + orphans = g_list_delete_link (orphans, orphans); + } + +exit: + registry->orphans_restored = TRUE; + + return FALSE; +} + +static void +composer_registry_notify_cb (EComposerRegistry *registry, + GObject *where_the_object_was) +{ + /* Remove the finalized composer from the registry. */ + g_queue_remove (®istry->composers, where_the_object_was); + + g_object_unref (registry); +} + +static void +composer_registry_window_created_cb (EShell *shell, + GtkWindow *window, + EComposerRegistry *registry) +{ + /* Offer to restore any orphaned auto-save files from the + * previous session once the first EShellWindow is mapped. */ + if (E_IS_SHELL_WINDOW (window) && !registry->orphans_restored) + g_signal_connect ( + window, "map-event", + G_CALLBACK (composer_registry_map_event_cb), + registry); + + /* Track the new composer window. */ + else if (E_IS_MSG_COMPOSER (window)) { + g_queue_push_tail (®istry->composers, window); + g_object_weak_ref ( + G_OBJECT (window), (GWeakNotify) + composer_registry_notify_cb, + g_object_ref (registry)); + } +} + +static void +composer_registry_finalize (GObject *object) +{ + GObjectClass *parent_class; + EComposerRegistry *registry; + + registry = E_COMPOSER_REGISTRY (object); + + /* All composers should have been finalized by now. */ + g_warn_if_fail (g_queue_is_empty (®istry->composers)); + + /* Chain up to parent's finalize() method. */ + parent_class = G_OBJECT_CLASS (e_composer_registry_parent_class); + parent_class->finalize (object); +} + +static void +composer_registry_constructed (GObject *object) +{ + EExtensible *extensible; + GObjectClass *parent_class; + + /* Chain up to parent's constructed() method. */ + parent_class = G_OBJECT_CLASS (e_composer_registry_parent_class); + parent_class->constructed (object); + + extensible = e_extension_get_extensible (E_EXTENSION (object)); + + /* Listen for new watched windows. */ + g_signal_connect ( + extensible, "window-created", + G_CALLBACK (composer_registry_window_created_cb), + object); +} + +static void +e_composer_registry_class_init (EComposerRegistryClass *class) +{ + GObjectClass *object_class; + EExtensionClass *extension_class; + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = composer_registry_finalize; + object_class->constructed = composer_registry_constructed; + + extension_class = E_EXTENSION_CLASS (class); + extension_class->extensible_type = E_TYPE_SHELL; +} + +static void +e_composer_registry_class_finalize (EComposerRegistryClass *class) +{ +} + +static void +e_composer_registry_init (EComposerRegistry *registry) +{ + g_queue_init (®istry->composers); +} + +void +e_composer_registry_type_register (GTypeModule *type_module) +{ + /* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration + * function, so we have to wrap it with a public function in + * order to register types from a separate compilation unit. */ + e_composer_registry_register_type (type_module); +} diff --git a/modules/composer-autosave/evolution-composer-autosave.c b/modules/composer-autosave/evolution-composer-autosave.c new file mode 100644 index 0000000000..d7e32f2f69 --- /dev/null +++ b/modules/composer-autosave/evolution-composer-autosave.c @@ -0,0 +1,40 @@ +/* + * evolution-module-composer-autosave.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + */ + +#include +#include + +/* Module Entry Points */ +void e_module_load (GTypeModule *type_module); +void e_module_unload (GTypeModule *type_module); + +/* Forward Declarations */ +void e_composer_autosave_type_register (GTypeModule *type_module); +void e_composer_registry_type_register (GTypeModule *type_module); + +G_MODULE_EXPORT void +e_module_load (GTypeModule *type_module) +{ + e_composer_autosave_type_register (type_module); + e_composer_registry_type_register (type_module); +} + +G_MODULE_EXPORT void +e_module_unload (GTypeModule *type_module) +{ +} diff --git a/modules/mail/e-mail-shell-backend.c b/modules/mail/e-mail-shell-backend.c index 870437aeb6..f1696692dd 100644 --- a/modules/mail/e-mail-shell-backend.c +++ b/modules/mail/e-mail-shell-backend.c @@ -375,7 +375,6 @@ mail_shell_backend_window_created_cb (EShell *shell, GtkWindow *window, EShellBackend *shell_backend) { - static gboolean first_time = TRUE; const gchar *backend_name; /* This applies to both the composer and signature editor. */ @@ -425,13 +424,6 @@ mail_shell_backend_window_created_cb (EShell *shell, g_object_weak_ref ( G_OBJECT (window), (GWeakNotify) mail_shell_backend_window_weak_notify_cb, shell); - - if (first_time) { - g_signal_connect_swapped ( - window, "map-event", - G_CALLBACK (e_msg_composer_check_autosave), shell); - first_time = FALSE; - } } static void diff --git a/shell/e-shell.c b/shell/e-shell.c index fcb3d9abf3..db9efc9331 100644 --- a/shell/e-shell.c +++ b/shell/e-shell.c @@ -180,6 +180,16 @@ shell_window_focus_in_event_cb (EShell *shell, return FALSE; } +static gboolean +shell_emit_window_destroyed_cb (EShell *shell) +{ + g_signal_emit (shell, signals[WINDOW_DESTROYED], 0); + + g_object_unref (shell); + + return FALSE; +} + static void shell_window_weak_notify_cb (EShell *shell, GObject *where_the_object_was) @@ -190,7 +200,12 @@ shell_window_weak_notify_cb (EShell *shell, list = g_list_remove (list, where_the_object_was); shell->priv->watched_windows = list; - g_signal_emit (shell, signals[WINDOW_DESTROYED], 0); + /* Let the watched window finish finalizing itself before we + * emit the "window-destroyed" signal, which may trigger the + * application to initiate shutdown. */ + g_idle_add ( + (GSourceFunc) shell_emit_window_destroyed_cb, + g_object_ref (shell)); } static void -- cgit