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. --- 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 ++ 6 files changed, 1078 insertions(+) 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 (limited to 'modules/composer-autosave') 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) +{ +} -- cgit v1.2.3