diff --git a/src/client/application/application-controller.vala b/src/client/application/application-controller.vala index 147c5efe..473f18f7 100644 --- a/src/client/application/application-controller.vala +++ b/src/client/application/application-controller.vala @@ -339,7 +339,7 @@ internal class Application.Controller : composer = new Composer.Widget( this, this.application.config, - context, + send_context, null ); register_composer(composer); @@ -418,7 +418,7 @@ internal class Application.Controller : composer = new Composer.Widget( this, this.application.config, - account, + send_context, null ); register_composer(composer); diff --git a/src/client/application/application-plugin-manager.vala b/src/client/application/application-plugin-manager.vala index 811775a3..0f0c9622 100644 --- a/src/client/application/application-plugin-manager.vala +++ b/src/client/application/application-plugin-manager.vala @@ -124,6 +124,8 @@ public class Application.PluginManager : GLib.Object { internal weak PluginGlobals globals; private GLib.SimpleActionGroup? action_group = null; + private Gee.Map composer_impls = + new Gee.HashMap(); public ApplicationImpl(Client backing, @@ -134,14 +136,82 @@ public class Application.PluginManager : GLib.Object { this.globals = globals; } - public Plugin.Composer new_composer(Plugin.Account source) + public async Plugin.Composer compose_blank(Plugin.Account source) throws Plugin.Error { var impl = source as AccountImpl; if (impl == null) { throw new Plugin.Error.NOT_SUPPORTED("Not a valid account"); } - return new ComposerImpl( - this.backing, impl.backing, this.folders, this.email + return to_plugin_composer( + yield this.backing.controller.compose_blank(impl.backing) + ); + } + + public async Plugin.Composer? compose_with_context( + Plugin.Account send_from, + Plugin.Composer.ContextType plugin_type, + Plugin.EmailIdentifier to_load, + string? quote = null + ) throws Plugin.Error { + var source_impl = send_from as AccountImpl; + if (source_impl == null) { + throw new Plugin.Error.NOT_SUPPORTED("Not a valid account"); + } + var id = this.globals.email.to_engine_id(to_load); + if (id == null) { + throw new Plugin.Error.NOT_FOUND("Email id not found"); + } + Gee.Collection? email = null; + try { + email = yield source_impl.backing.emails.list_email_by_sparse_id_async( + Geary.Collection.single(id), + Composer.Widget.REQUIRED_FIELDS, + NONE, + source_impl.backing.cancellable + ); + } catch (GLib.Error err) { + throw new Plugin.Error.NOT_FOUND( + "Error looking up email: %s", err.message + ); + } + if (email == null || email.is_empty) { + throw new Plugin.Error.NOT_FOUND("Email not found for id"); + } + var context = Geary.Collection.first(email); + + var type = Composer.Widget.ContextType.NONE; + switch (plugin_type) { + case NONE: + type = Composer.Widget.ContextType.NONE; + break; + + case EDIT: + type = Composer.Widget.ContextType.EDIT; + // Use the same folder that the email exists since it + // could be getting edited somewhere outside of drafts + // (e.g. templates) + break; + + case REPLY_SENDER: + type = Composer.Widget.ContextType.REPLY_SENDER; + break; + + case REPLY_ALL: + type = Composer.Widget.ContextType.REPLY_ALL; + break; + + case FORWARD: + type = Composer.Widget.ContextType.FORWARD; + break; + } + + return to_plugin_composer( + yield this.backing.controller.compose_with_context( + source_impl.backing, + type, + context, + quote + ) ); } @@ -212,6 +282,33 @@ public class Application.PluginManager : GLib.Object { this.backing.controller.report_problem(problem); } + internal void engine_composer_registered(Composer.Widget registered) { + var impl = to_plugin_composer(registered); + if (impl != null) { + composer_registered(impl); + } + } + + internal void engine_composer_deregistered(Composer.Widget deregistered) { + var impl = this.composer_impls.get(deregistered); + if (impl != null) { + composer_deregistered(impl); + this.composer_impls.unset(deregistered); + } + } + + private ComposerImpl? to_plugin_composer(Composer.Widget? widget) { + ComposerImpl impl = null; + if (widget != null) { + impl = this.composer_impls.get(widget); + if (impl == null) { + impl = new ComposerImpl(widget, this); + this.composer_impls.set(widget, impl); + } + } + return impl; + } + private void on_window_added(Gtk.Window window) { if (this.action_group != null) { var main = window as MainWindow; @@ -246,75 +343,72 @@ public class Application.PluginManager : GLib.Object { } - private class ComposerImpl : Geary.BaseObject, Plugin.Composer { + /** An implementation of the plugin Composer interface. */ + internal class ComposerImpl : Geary.BaseObject, Plugin.Composer { - public override bool can_send { get; set; default = true; } - - private Client application; - private AccountContext account; - private FolderStoreFactory folders; - private EmailStoreFactory email; - private Geary.Email? to_load = null; - private Geary.Folder? save_location = null; - - - public ComposerImpl(Client application, - AccountContext account, - FolderStoreFactory folders, - EmailStoreFactory email) { - this.application = application; - this.account = account; - this.folders = folders; - this.email = email; + public bool can_send { + get { return this.backing.can_send; } + set { this.backing.can_send = value; } } - public void show() { - this.show_impl.begin(); - } - - public async void edit_email(Plugin.EmailIdentifier to_load) - throws Error { - Geary.EmailIdentifier? id = this.email.to_engine_id(to_load); - if (id == null) { - throw new Plugin.Error.NOT_FOUND("Email id not found"); - } - Gee.Collection? email = - yield this.account.emails.list_email_by_sparse_id_async( - Geary.Collection.single(id), - Composer.Widget.REQUIRED_FIELDS, - NONE, - this.account.cancellable + public Plugin.Account? sender_context { + get { + // ugh + this._sender_context = this.application.globals.accounts.get( + this.backing.sender_context ); - if (email != null && !email.is_empty) { - this.to_load = Geary.Collection.first(email); + return this._sender_context; } } + private Plugin.Account? _sender_context = null; + + public Plugin.Folder? save_to { + get { + // Ugh + this._save_to = ( + (backing.save_to != null) + ? this.application.globals.folders.get_plugin_folder( + this.backing.save_to + ) + : null + ); + return this._save_to; + } + } + private Plugin.Folder? _save_to = null; + + private Composer.Widget backing; + private weak ApplicationImpl application; + + + public ComposerImpl(Composer.Widget backing, + ApplicationImpl application) { + this.backing = backing; + this.application = application; + } public void save_to_folder(Plugin.Folder? location) { - var folder = this.folders.get_engine_folder(location); - if (folder != null && folder.account == this.account.account) { - this.save_location = folder; + var engine = this.application.globals.folders.get_engine_folder(location); + if (engine != null && engine.account == this.backing.sender_context.account) { + this.backing.set_save_to_override.begin( + engine, + (obj, res) => { + try { + this.backing.set_save_to_override.end(res); + } catch (GLib.Error err) { + debug( + "Error setting folder for saving: %s", + err.message + ); + } + } + ); } } - private async void show_impl() { - var controller = this.application.controller; - Composer.Widget? composer = null; - if (this.to_load == null) { - composer = yield controller.compose_new_email( - null, - this.save_location - ); - } else { - composer = yield controller.compose_with_context_email( - EDIT, - this.to_load, - null, - this.save_location - ); - } - composer.can_send = this.can_send; + public void present() { + this.application.backing.controller.present_composer(this.backing); } } @@ -629,4 +723,16 @@ public class Application.PluginManager : GLib.Object { this.plugin_set.unset(context.info); } + private void on_composer_registered(Composer.Widget registered) { + foreach (var context in this.plugin_set.values) { + context.application.engine_composer_registered(registered); + } + } + + private void on_composer_deregistered(Composer.Widget deregistered) { + foreach (var context in this.plugin_set.values) { + context.application.engine_composer_deregistered(deregistered); + } + } + } diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala index f217a4ec..0b256316 100644 --- a/src/client/composer/composer-widget.vala +++ b/src/client/composer/composer-widget.vala @@ -342,6 +342,9 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { get; private set; default = new Geary.RFC822.MessageIDList(); } + /** Overrides for the draft folder as save destination, if any. */ + internal Geary.Folder? save_to { get; private set; default = null; } + internal WebView editor { get; private set; } internal Headerbar header { get; private set; } @@ -490,7 +493,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { private Gee.Map inline_files = new Gee.HashMap(); private Gee.Map cid_files = new Gee.HashMap(); - private Geary.Folder? save_to; private Geary.App.DraftManager? draft_manager = null; private GLib.Cancellable? draft_manager_opening = null; private Geary.TimeoutManager draft_timer; @@ -1102,6 +1104,13 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } } + /** Overrides the draft folder as a destination for saving. */ + internal async void set_save_to_override(Geary.Folder? save_to) + throws GLib.Error { + this.save_to = save_to; + yield reopen_draft_manager(); + } + /** * Loads and sets contact auto-complete data for the current account. */ diff --git a/src/client/plugin/email-templates/email-templates.vala b/src/client/plugin/email-templates/email-templates.vala index 51b55efb..981898a6 100644 --- a/src/client/plugin/email-templates/email-templates.vala +++ b/src/client/plugin/email-templates/email-templates.vala @@ -138,7 +138,16 @@ public class Plugin.EmailTemplates : private async void edit_email(Folder? target, EmailIdentifier? id, bool send) { var account = (target != null) ? target.account : id.account; try { - var composer = this.plugin_application.new_composer(account); + Plugin.Composer? composer = null; + if (id != null) { + composer = yield this.plugin_application.compose_with_context( + id.account, + Composer.ContextType.EDIT, + id + ); + } else { + composer = yield this.plugin_application.compose_blank(account); + } if (!send) { var folder = target; if (folder == null && id != null) { @@ -153,10 +162,7 @@ public class Plugin.EmailTemplates : composer.can_send = false; } - if (id != null) { - yield composer.edit_email(id); - } - composer.show(); + composer.present(); } catch (GLib.Error err) { warning("Unable to construct composer: %s", err.message); } diff --git a/src/client/plugin/mail-merge/mail-merge.vala b/src/client/plugin/mail-merge/mail-merge.vala index 806f3e3c..2885d655 100644 --- a/src/client/plugin/mail-merge/mail-merge.vala +++ b/src/client/plugin/mail-merge/mail-merge.vala @@ -138,7 +138,11 @@ public class Plugin.MailMerge : private async void edit_email(EmailIdentifier id) { try { - var composer = this.plugin_application.new_composer(id.account); + var composer = yield this.plugin_application.compose_with_context( + id.account, + Composer.ContextType.EDIT, + id + ); var containing = yield this.folder_store.list_containing_folders( id, this.cancellable ); @@ -148,8 +152,7 @@ public class Plugin.MailMerge : composer.save_to_folder(folder); composer.can_send = false; - yield composer.edit_email(id); - composer.show(); + composer.present(); } catch (GLib.Error err) { warning("Unable to construct composer: %s", err.message); } diff --git a/src/client/plugin/plugin-application.vala b/src/client/plugin/plugin-application.vala index 97b30da6..25a28ddf 100644 --- a/src/client/plugin/plugin-application.vala +++ b/src/client/plugin/plugin-application.vala @@ -15,13 +15,62 @@ public interface Plugin.Application : Geary.BaseObject { /** - * Constructs a new, blank composer for the given account. + * Emitted when a new composer is registered with the application. + * + * A composer is registered when it is first constructed. + * + * @see Composer.present + */ + public signal void composer_registered(Composer composer); + + /** + * Emitted when an existing composer is de-registered. + * + * A composer is deregistered when it is destroyed, either after + * being sent, closed, or discarded. + */ + public signal void composer_deregistered(Composer composer); + + + /** + * Obtains a new, blank composer for the given account. * * The composer will be initialised to send an email from the - * given account. This may be changed by people before they send - * the email, however. + * given account. This may be changed via the UI before the email + * is sent, however. + * + * Existing composer instances are re-used where possible, thus if + * a blank composer is already open, the same instance may be + * returned if this method is called multiple times. */ - public abstract Composer new_composer(Account source) throws Error; + public abstract async Composer compose_blank(Account send_from) + throws Error; + + /** + * Obtains a new composer with the given message as a context + * + * The composer will be initialised to send an email from the + * given account, with the given email loaded as either an email + * to edit, a reply, or a forwarded message, depending on the + * given context. + * + * If a quote is given, this added as a quote in the composer's + * body. + * + * Existing composer instances are re-used where possible, thus if + * a composer with a given context and email is already open, the + * same instance may be returned if this method is called multiple + * times with the same arguments. + * + * Returns null if there is an existing composer open and the + * prompt to close it was declined. + */ + public abstract async Composer? compose_with_context( + Account send_from, + Composer.ContextType type, + EmailIdentifier context, + string? quote = null + ) throws Error; /** * Registers a plugin action with the application. diff --git a/src/client/plugin/plugin-composer.vala b/src/client/plugin/plugin-composer.vala index 146721dd..9765ef13 100644 --- a/src/client/plugin/plugin-composer.vala +++ b/src/client/plugin/plugin-composer.vala @@ -7,43 +7,76 @@ /** * An object representing a composer for use by plugins. + * + * Instances of this interface can be obtained by calling {@link + * Application.compose_blank} or {@link + * Application.compose_with_context}. A composer instance may not be + * visible until {@link present} is called, allowing it to be + * configured via calls to this interface first, if required. */ public interface Plugin.Composer : Geary.BaseObject { + /** + * Determines the type of the context email passed to the composer + * + * @see Application.compose_with_context + */ + public enum ContextType { + /** No context mail was provided. */ + NONE, + + /** Context is an email to edited, for example a draft or template. */ + EDIT, + + /** Context is an email being replied to the sender only. */ + REPLY_SENDER, + + /** Context is an email being replied to all recipients. */ + REPLY_ALL, + + /** Context is an email being forwarded. */ + FORWARD + } + + /** + * Denotes the account the composed email will be sent from. + */ + public abstract Plugin.Account? sender_context { get; } + /** * Determines if the email in the composer can be sent. */ public abstract bool can_send { get; set; } /** - * Causes the composer to be made visible. + * Denotes the folder that the email will be saved to. * - * The composer will be shown as either full-pane and in-window if - * not a reply to a currently displayed conversation, inline and - * in-window if a reply to an existing conversation being - * displayed, or detached if there is already an in-window - * composer being displayed. + * If non-null, fixes the folder used by the composer for saving + * the email. If null, the current account's Draft folder will be + * used. + * + * @see save_to_folder */ - public abstract void show(); + public abstract Plugin.Folder? save_to { get; } + /** - * Loads an email into the composer to be edited. + * Presents the composer on screen. * - * Loads the given email, and sets it as the email to be edited in - * this composer. This must be called before calling {@link show}, - * and has no effect if called afterwards. + * The composer is made visible if this has not yet been done so, + * and the application attempts to ensure that it is presented on + * the active display. */ - public async abstract void edit_email(EmailIdentifier to_load) - throws GLib.Error; + public abstract void present(); /** * Sets the folder used to save the message being composed. * * Ensures email for both automatic and manual saving of the email - * in the composer is saved to the given folder. This must be - * called before calling {@link show}, and has no effect if called - * afterwards. + * in the composer is saved to the given folder. This disables + * changing accounts in the composer's UI since email cannot be + * saved across accounts. */ public abstract void save_to_folder(Plugin.Folder? location); diff --git a/src/client/plugin/special-folders/special-folders.vala b/src/client/plugin/special-folders/special-folders.vala index 6b654887..0f49a012 100644 --- a/src/client/plugin/special-folders/special-folders.vala +++ b/src/client/plugin/special-folders/special-folders.vala @@ -146,9 +146,12 @@ public class Plugin.SpecialFolders : private async void edit_draft(EmailIdentifier id) { try { - var composer = this.plugin_application.new_composer(id.account); - yield composer.edit_email(id); - composer.show(); + var composer = yield this.plugin_application.compose_with_context( + id.account, + Composer.ContextType.EDIT, + id + ); + composer.present(); } catch (GLib.Error err) { warning("Unable to construct composer: %s", err.message); }