diff --git a/src/client/application/application-controller.vala b/src/client/application/application-controller.vala index baeba205..5d20a421 100644 --- a/src/client/application/application-controller.vala +++ b/src/client/application/application-controller.vala @@ -407,7 +407,9 @@ public class Application.Controller : Geary.BaseObject { // Schedule the send for after we have an account open. this.pending_mailtos.add(mailto); } else { - create_compose_widget(selected, NEW_MESSAGE, null, null, mailto); + create_compose_widget( + selected, NEW_MESSAGE, mailto, null, null, false + ); } } @@ -417,8 +419,9 @@ public class Application.Controller : Geary.BaseObject { public void compose_with_context_email(Geary.Account account, Composer.Widget.ComposeType type, Geary.Email context, - string? quote) { - create_compose_widget(account, type, context, quote); + string? quote, + bool is_draft) { + create_compose_widget(account, type, null, context, quote, is_draft); } /** Adds a new composer to be kept track of. */ @@ -1493,14 +1496,14 @@ public class Application.Controller : Geary.BaseObject { // result in their removal from composer_windows, // which could crash this loop. composers_to_destroy.add(cw); - ((Composer.Container) cw.parent).vanish(); + cw.set_enabled(false); } } } // Safely destroy windows. - foreach(Composer.Widget cw in composers_to_destroy) { - ((Composer.Container) cw.parent).close_container(); + foreach (Composer.Widget cw in composers_to_destroy) { + cw.close.begin(); } // If we cancelled the quit we can bail here. @@ -1538,10 +1541,10 @@ public class Application.Controller : Geary.BaseObject { */ private void create_compose_widget(Geary.Account account, Composer.Widget.ComposeType compose_type, - Geary.Email? referred = null, - string? quote = null, - string? mailto = null, - bool is_draft = false) { + string? mailto, + Geary.Email? referred, + string? quote, + bool is_draft) { // There's a few situations where we can re-use an existing // composer, check for these first. @@ -1554,18 +1557,25 @@ public class Application.Controller : Geary.BaseObject { existing.state == PANED && existing.is_blank) { existing.present(); - existing.set_focus(); return; } - } else if (compose_type != NEW_MESSAGE) { - // We're replying, see whether we already have a reply for - // that message and if so, insert a quote into that. + } else if (compose_type != NEW_MESSAGE && referred != null) { + // A reply/forward was requested, see whether there is + // already an inline message that is either a + // reply/forward for that message, or there is a quote + // to insert into it. foreach (Composer.Widget existing in this.composer_widgets) { - if (existing.state != DETACHED && - ((referred != null && existing.referred_ids.contains(referred.id)) || + if ((existing.state == INLINE || + existing.state == INLINE_COMPACT) && + (referred.id in existing.get_referred_ids() || quote != null)) { - existing.change_compose_type(compose_type, referred, quote); - return; + try { + existing.append_to_email(referred, quote, compose_type); + existing.present(); + return; + } catch (Geary.EngineError error) { + report_problem(new Geary.ProblemReport(error)); + } } } @@ -1585,10 +1595,7 @@ public class Application.Controller : Geary.BaseObject { ); } else { widget = new Composer.Widget( - this.application, - account, - is_draft ? referred.id : null, - compose_type + this.application, account, compose_type ); } @@ -1607,6 +1614,7 @@ public class Application.Controller : Geary.BaseObject { account, widget, referred, + is_draft, quote ); } @@ -1614,6 +1622,7 @@ public class Application.Controller : Geary.BaseObject { private async void load_composer(Geary.Account account, Composer.Widget widget, Geary.Email? referred = null, + bool is_draft, string? quote = null) { Geary.Email? full = null; GLib.Cancellable? cancellable = null; @@ -1635,7 +1644,7 @@ public class Application.Controller : Geary.BaseObject { } } try { - yield widget.load(full, quote, cancellable); + yield widget.load(full, is_draft, quote, cancellable); } catch (GLib.Error err) { report_problem(new Geary.ProblemReport(err)); } diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala index b37de490..3690d0a6 100644 --- a/src/client/components/main-window.vala +++ b/src/client/components/main-window.vala @@ -566,21 +566,18 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { /** Displays a composer addressed to a specific email address. */ public void open_composer_for_mailbox(Geary.RFC822.MailboxAddress to) { - Application.Controller controller = this.application.controller; - Composer.Widget composer = new Composer.Widget( - this.application, this.selected_folder.account, null, NEW_MESSAGE + var composer = new Composer.Widget.from_mailbox( + this.application, this.selected_folder.account, to ); - composer.to = to.to_full_display(); - controller.add_composer(composer); + this.application.controller.add_composer(composer); show_composer(composer); - composer.load.begin(null, null, null); + composer.load.begin(null, false, null, null); } /** Displays a composer in the window if possible, else in a new window. */ public void show_composer(Composer.Widget composer) { if (this.has_composer) { - composer.state = Composer.Widget.ComposerState.DETACHED; - new Composer.Window(composer, this.application); + composer.detach(); } else { this.conversation_viewer.do_compose(composer); get_window_action(ACTION_FIND_IN_CONVERSATION).set_enabled(false); @@ -599,7 +596,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { if (composer != null) { switch (composer.should_close()) { case DO_CLOSE: - composer.close(); + composer.close.begin(); break; case CANCEL_CLOSE: @@ -1256,7 +1253,8 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { account, compose_type, email_view.email, - quote + quote, + false ); }); } @@ -1717,8 +1715,8 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { bool already_open = false; foreach (Composer.Widget composer in this.application.controller.get_composers()) { - if (composer.draft_id != null && - composer.draft_id.equal_to(draft.id)) { + if (composer.current_draft_id != null && + composer.current_draft_id.equal_to(draft.id)) { already_open = true; composer.present(); composer.set_focus(); @@ -1731,7 +1729,8 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { activated.base_folder.account, NEW_MESSAGE, draft, - null + null, + true ); } } @@ -2117,7 +2116,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { Geary.Account? account = this.selected_account; if (account != null) { this.application.controller.compose_with_context_email( - account, REPLY, target, quote + account, REPLY, target, quote, false ); } } @@ -2126,7 +2125,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { Geary.Account? account = this.selected_account; if (account != null) { this.application.controller.compose_with_context_email( - account, REPLY_ALL, target, quote + account, REPLY_ALL, target, quote, false ); } } @@ -2135,7 +2134,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { Geary.Account? account = this.selected_account; if (account != null) { this.application.controller.compose_with_context_email( - account, FORWARD, target, quote + account, FORWARD, target, quote, false ); } } @@ -2144,7 +2143,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { Geary.Account? account = this.selected_account; if (account != null) { this.application.controller.compose_with_context_email( - account, NEW_MESSAGE, target, null + account, NEW_MESSAGE, target, null, true ); } } diff --git a/src/client/composer/composer-box.vala b/src/client/composer/composer-box.vala index 6f02dc16..e6700afb 100644 --- a/src/client/composer/composer-box.vala +++ b/src/client/composer/composer-box.vala @@ -1,26 +1,28 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. +/* + * Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2019 Michael Gratton * * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. + * (version 2.1 or later). See the COPYING file in this distribution. */ /** - * A ComposerBox is a ComposerContainer that is used to compose mails in the main-window - * (i.e. not-detached), yet separate from a conversation. + * A container for full-height paned composers in the main window. */ public class Composer.Box : Gtk.Frame, Container { - public Gtk.ApplicationWindow top_window { - get { return (Gtk.ApplicationWindow) get_toplevel(); } + /** {@inheritDoc} */ + public Gtk.ApplicationWindow? top_window { + get { return get_toplevel() as Gtk.ApplicationWindow; } } + /** {@inheritDoc} */ internal Widget composer { get; set; } - protected Gee.MultiMap? old_accelerators { get; set; } - private MainToolbar main_toolbar { get; private set; } + /** Emitted when the container is closed. */ public signal void vanished(); @@ -40,21 +42,12 @@ public class Composer.Box : Gtk.Frame, Container { show(); } - public void remove_composer() { - remove(this.composer); - close_container(); - } - - public void vanish() { - hide(); + /** {@inheritDoc} */ + public void close() { this.main_toolbar.remove_conversation_header(composer.header); - this.composer.state = Widget.ComposerState.DETACHED; vanished(); - } - public void close_container() { - if (this.visible) - vanish(); + remove(this.composer); destroy(); } diff --git a/src/client/composer/composer-container.vala b/src/client/composer/composer-container.vala index 88e12366..8756544d 100644 --- a/src/client/composer/composer-container.vala +++ b/src/client/composer/composer-container.vala @@ -5,38 +5,37 @@ */ /** - * A generic interface for widgets that have a single ComposerWidget-child. + * A generic interface for widgets that have a single composer child. */ public interface Composer.Container { - // The ComposerWidget-child. + /** The top-level window for the container, if any. */ + public abstract Gtk.ApplicationWindow? top_window { get; } + + /** The container's current composer, if any. */ internal abstract Widget composer { get; set; } - // We use old_accelerators to keep track of the accelerators we temporarily disabled. - protected abstract Gee.MultiMap? old_accelerators { get; set; } - - // The toplevel window for the container. Note that it needs to be a GtkApplicationWindow. - public abstract Gtk.ApplicationWindow top_window { get; } - + /** Causes the composer's top-level window to be presented. */ public virtual void present() { - this.top_window.present(); + Gtk.ApplicationWindow top = top_window; + if (top != null) { + top.present(); + } } - public virtual unowned Gtk.Widget get_focus() { - return this.top_window.get_focus(); + /** Returns the top-level window's current focus widget, if any. */ + public virtual Gtk.Widget? get_focus() { + Gtk.Widget? focus = null; + Gtk.ApplicationWindow top = top_window; + if (top != null) { + focus = top.get_focus(); + } + return focus; } - public abstract void close_container(); - /** - * Hides the widget (and possibly its parent). Usecase is when you don't want to close just yet - * but the composer should not be visible any longer (e.g. when you're still saving a draft). + * Removes the composer and destroys the container. */ - public abstract void vanish(); - - /** - * Removes the composer from this ComposerContainer (e.g. when detaching) - */ - public abstract void remove_composer(); + public abstract void close(); } diff --git a/src/client/composer/composer-embed.vala b/src/client/composer/composer-embed.vala index 19ba05cc..ce70ee81 100644 --- a/src/client/composer/composer-embed.vala +++ b/src/client/composer/composer-embed.vala @@ -1,30 +1,33 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. +/* + * Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2019 Michael Gratton * * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. + * (version 2.1 or later). See the COPYING file in this distribution. */ /** - * A ComposerEmbed is a widget that is used to compose emails that are inlined into a - * conversation view, e.g. for reply or forward mails. + * A container for full-height paned composers in the main window. */ public class Composer.Embed : Gtk.EventBox, Container { private const int MIN_EDITOR_HEIGHT = 200; - public Geary.Email referred { get; private set; } - - public Gtk.ApplicationWindow top_window { - get { return (Gtk.ApplicationWindow) get_toplevel(); } + /** {@inheritDoc} */ + public Gtk.ApplicationWindow? top_window { + get { return get_toplevel() as Gtk.ApplicationWindow; } } - internal Widget composer { get; set; } + /** The email this composer was originally a reply to. */ + public Geary.Email referred { get; private set; } - protected Gee.MultiMap? old_accelerators { get; set; } + /** {@inheritDoc} */ + internal Widget composer { get; set; } private Gtk.ScrolledWindow outer_scroller; + /** Emitted when the container is closed. */ public signal void vanished(); @@ -45,6 +48,15 @@ public class Composer.Embed : Gtk.EventBox, Container { show(); } + /** {@inheritDoc} */ + public void close() { + disable_scroll_reroute(this); + vanished(); + + remove(this.composer); + destroy(); + } + private void on_realize() { reroute_scroll_handling(this); } @@ -68,12 +80,6 @@ public class Composer.Embed : Gtk.EventBox, Container { } } - public void remove_composer() { - disable_scroll_reroute(this); - remove(this.composer); - close_container(); - } - // This method intercepts scroll events destined for the embedded // composer and diverts them them to the conversation listbox's // outer scrolled window or the composer's editor as appropriate. @@ -177,15 +183,4 @@ public class Composer.Embed : Gtk.EventBox, Container { return ret; } - public void vanish() { - hide(); - this.composer.state = Widget.ComposerState.DETACHED; - vanished(); - } - - public void close_container() { - if (this.visible) - vanish(); - destroy(); - } } diff --git a/src/client/composer/composer-web-view.vala b/src/client/composer/composer-web-view.vala index ffe7c23a..cb3410d9 100644 --- a/src/client/composer/composer-web-view.vala +++ b/src/client/composer/composer-web-view.vala @@ -191,13 +191,6 @@ public class Composer.WebView : ClientWebView { ); } - /** - * Makes the view uneditable and stops signals from being sent. - */ - public void disable() { - set_sensitive(false); - } - /** * Sets whether the editor is in rich text or plain text mode. */ diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala index 6c879ac5..005ea8ef 100644 --- a/src/client/composer/composer-widget.vala +++ b/src/client/composer/composer-widget.vala @@ -1,6 +1,6 @@ /* * Copyright 2016 Software Freedom Conservancy Inc. - * Copyright 2017 Michael Gratton + * Copyright 2017-2019 Michael Gratton * * This software is licensed under the GNU Lesser General Public License * (version 2.1 or later). See the COPYING file in this distribution. @@ -11,10 +11,11 @@ private errordomain AttachmentError { DUPLICATE } + /** * A widget for editing an email message. * - * Composers must always be placed in an instance of {@link Container}. + * Composers must always be placed in an instance of {@link ComposerContainer}. */ [GtkTemplate (ui = "/org/gnome/Geary/composer-widget.ui")] public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { @@ -82,7 +83,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { private const string ACTION_INSERT_IMAGE = "insert-image"; private const string ACTION_INSERT_LINK = "insert-link"; private const string ACTION_COMPOSE_AS_HTML = "compose-as-html"; - private const string ACTION_SHOW_EXTENDED = "show-extended"; + private const string ACTION_SHOW_EXTENDED_HEADERS = "show-extended-headers"; private const string ACTION_CLOSE_AND_SAVE = "close-and-save"; private const string ACTION_CLOSE_AND_DISCARD = "close-and-discard"; private const string ACTION_DETACH = "detach"; @@ -139,7 +140,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { { ACTION_OPEN_INSPECTOR, on_open_inspector }, { ACTION_SELECT_DICTIONARY, on_select_dictionary }, { ACTION_SEND, on_send }, - { ACTION_SHOW_EXTENDED, on_toggle_action, null, "false", on_show_extended_toggled }, + { ACTION_SHOW_EXTENDED_HEADERS, on_toggle_action, null, "false", on_show_extended_headers_toggled }, }; public static void add_accelerators(GearyApplication application) { @@ -182,51 +183,25 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { private const string ATTACHMENT_KEYWORDS_LOCALISED = _("attach|attaching|attaches|attachment|attachments|attached|enclose|enclosed|enclosing|encloses|enclosure|enclosures"); - private const string PASTED_IMAGE_FILENAME_TEMPLATE = "geary-pasted-image-%u.png"; + + /** The account the email is being sent from. */ public Geary.Account account { get; private set; } - private Gee.Map accounts; /** The identifier of the draft this composer holds, if any. */ - public Geary.EmailIdentifier? draft_id { get; private set; default = null; } - - public Geary.RFC822.MailboxAddresses from { get; private set; } - - public string to { - get { return this.to_entry.get_text(); } - set { this.to_entry.set_text(value); } + public Geary.EmailIdentifier? current_draft_id { + get { + return this.draft_manager != null + ? this.draft_manager.current_draft_id : null; + } } - public string cc { - get { return this.cc_entry.get_text(); } - set { this.cc_entry.set_text(value); } - } - - public string bcc { - get { return this.bcc_entry.get_text(); } - set { this.bcc_entry.set_text(value); } - } - - public string reply_to { - get { return this.reply_to_entry.get_text(); } - set { this.reply_to_entry.set_text(value); } - } - - public Gee.Set in_reply_to = new Gee.HashSet(); - public string references { get; set; } - - public string subject { - get { return this.subject_entry.get_text(); } - set { this.subject_entry.set_text(value); } - } - - public ComposerState state { get; internal set; } + public ComposerState state { get; private set; } + /** Determines the type of email being composed. */ public ComposeType compose_type { get; private set; default = ComposeType.NEW_MESSAGE; } - public Gee.Set referred_ids = new Gee.HashSet(); - /** Determines if the composer is completely empty. */ public bool is_blank { get { @@ -240,33 +215,46 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } } - /** Determines if the composer can currently save a draft. */ - private bool can_save { - get { return this.draft_manager != null; } - } - - /** Determines if current message should be saved as draft. */ - private bool should_save { - get { - return this.can_save - && !this.is_draft_saved - && !this.is_blank; - } - } - - public Headerbar header { get; private set; } - public WebView editor { get; private set; } - public string window_title { get; set; } + internal Headerbar header { get; private set; } - private string body_html = ""; + internal string subject { + get { return this.subject_entry.get_text(); } + private set { this.subject_entry.set_text(value); } + } + + private Geary.RFC822.MailboxAddresses from { get; private set; } + + private string to { + get { return this.to_entry.get_text(); } + set { this.to_entry.set_text(value); } + } + + private string cc { + get { return this.cc_entry.get_text(); } + set { this.cc_entry.set_text(value); } + } + + private string bcc { + get { return this.bcc_entry.get_text(); } + set { this.bcc_entry.set_text(value); } + } + + private string reply_to { + get { return this.reply_to_entry.get_text(); } + set { this.reply_to_entry.set_text(value); } + } + + private Gee.Set in_reply_to = new Gee.HashSet(); + + private string references { get; private set; } [GtkChild] - internal Gtk.Grid editor_container; + private Gtk.Grid editor_container; [GtkChild] - internal Gtk.Grid body_container; + private Gtk.Grid body_container; [GtkChild] private Gtk.Label from_label; @@ -358,6 +346,24 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { private Menu context_menu_webkit_text_entry; private Menu context_menu_inspector; + /** Determines if the composer can currently save a draft. */ + private bool can_save { + get { return this.draft_manager != null; } + } + + /** Determines if current message should be saved as draft. */ + private bool should_save { + get { + return this.can_save + && !this.is_draft_saved + && !this.is_blank; + } + } + + private Gee.Map accounts; + + private string body_html = ""; + private SpellCheckPopover? spell_check_popover = null; private string? pointer_url = null; private string? cursor_url = null; @@ -369,6 +375,10 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { private bool top_posting = true; private string? last_quote = null; + // The message(s) this email is in reply to/forwarded from + private Gee.Set referred_ids = + new Gee.HashSet(); + private Gee.List? pending_attachments = null; private AttachPending pending_include = AttachPending.INLINE_ONLY; private Gee.Set attached_files = new Gee.HashSet(Geary.Files.nullable_hash, @@ -402,31 +412,19 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { // Is the composer closing (e.g. saving a draft or sending)? private bool is_closing = false; - private Container container { - get { return (Container) parent; } + private Container? container { + get { return this.parent as Container; } } private GearyApplication application; - /** Fired when the current saved draft's id has changed. */ - public signal void draft_id_changed(Geary.EmailIdentifier? id); - - /** Fired when the user opens a link in the composer. */ - public signal void link_activated(string url); - - /** Fired when the user has changed the composer's subject. */ - public signal void subject_changed(string new_subject); - - public Widget(GearyApplication application, Geary.Account initial_account, - Geary.EmailIdentifier? draft_id, ComposeType compose_type) { base_ref(); this.application = application; this.account = initial_account; - this.draft_id = draft_id; try { this.accounts = this.application.engine.get_accounts(); @@ -569,33 +567,17 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { load_entry_completions(); } - ~Widget() { - base_unref(); - } - - public override void destroy() { - this.draft_timer.reset(); - if (this.draft_manager_opening != null) { - this.draft_manager_opening.cancel(); - this.draft_manager_opening = null; - } - if (this.draft_manager != null) - close_draft_manager_async.begin(null); - - this.application.engine.account_available.disconnect( - on_account_available - ); - this.application.engine.account_unavailable.disconnect( - on_account_unavailable - ); - - base.destroy(); + public Widget.from_mailbox(GearyApplication application, + Geary.Account initial_account, + Geary.RFC822.MailboxAddress to) { + this(application, initial_account, ComposeType.NEW_MESSAGE); + this.to = to.to_full_display(); } public Widget.from_mailto(GearyApplication application, Geary.Account initial_account, string mailto) { - this(application, initial_account, null, ComposeType.NEW_MESSAGE); + this(application, initial_account, ComposeType.NEW_MESSAGE); Gee.HashMultiMap headers = new Gee.HashMultiMap(); if (mailto.has_prefix(MAILTO_URI_PREFIX)) { @@ -645,15 +627,26 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } } - /** Closes the composer unconditionally. */ - public void close() { - this.container.close_container(); + ~Widget() { + base_unref(); + } + + /** + * Returns the emails referred to by the composed email. + * + * A referred email is the email this composer is a reply to, or + * forwarded from. There may be multiple if a composer was already + * open and another email was replied to. + */ + public Gee.Set get_referred_ids() { + return this.referred_ids.read_only_view; } /** * Loads the message into the composer editor. */ public async void load(Geary.Email? referred = null, + bool is_draft, string? quote = null, GLib.Cancellable? cancellable) throws GLib.Error { @@ -663,21 +656,16 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { "Required fields not met: %s", referred.fields.to_string() ); } - bool is_referred_draft = ( - referred != null && - this.draft_id != null && - referred.id.equal_to(this.draft_id) - ); string referred_quote = ""; this.last_quote = quote; if (referred != null) { referred_quote = fill_in_from_referred(referred, quote); - if (is_referred_draft || + if (is_draft || compose_type == ComposeType.NEW_MESSAGE || compose_type == ComposeType.FORWARD) { this.pending_include = AttachPending.ALL; } - if (is_referred_draft) { + if (is_draft) { yield restore_reply_to_state(); } } @@ -693,12 +681,12 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.body_html, referred_quote, this.top_posting, - is_referred_draft + is_draft ); try { yield open_draft_manager_async( - is_referred_draft ? referred.id : null, + is_draft ? referred.id : null, cancellable ); } catch (Error e) { @@ -706,6 +694,103 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } } + /** Detaches the composer and opens it in a new window. */ + public void detach() { + if (this.state != ComposerState.DETACHED) { + Gtk.Widget? focused_widget = this.container.top_window.get_focus(); + if (this.container != null) { + this.container.close(); + } + Window new_window = new Window(this, this.application); + + // Workaround a GTK+ crasher, Bug 771812. When the + // composer is re-parented, its menu_button's popover + // keeps a reference to the conversation window's + // viewport, so when that is removed it has a null parent + // and we crash. To reproduce: Reply inline, detach the + // composer, then choose a different conversation back in + // the main window. The workaround here sets a new menu + // model and hence the menu_button constructs a new + // popover. + this.composer_actions.change_action_state( + ACTION_COMPOSE_AS_HTML, + this.application.config.compose_as_html + ); + + this.state = DETACHED; + update_composer_view(); + + // If the previously focused widget is in the new composer + // window then focus that, else focus something useful. + bool refocus = true; + if (focused_widget != null) { + Window? focused_window = focused_widget.get_toplevel() as Window; + if (new_window == focused_window) { + focused_widget.grab_focus(); + refocus = false; + } + } + if (refocus) { + set_focus(); + } + } + } + + /** Closes the composer unconditionally. */ + public async void close() { + set_enabled(false); + + if (this.draft_manager_opening != null) { + this.draft_manager_opening.cancel(); + this.draft_manager_opening = null; + } + + if (this.draft_manager != null) { + try { + yield close_draft_manager_async(null); + } catch (Error err) { + debug("Error closing draft manager on composer close"); + } + } + + destroy(); + } + + public override void destroy() { + if (this.draft_manager != null) { + warning("Draft manager still open on composer destroy"); + } + + this.application.engine.account_available.disconnect( + on_account_available + ); + this.application.engine.account_unavailable.disconnect( + on_account_unavailable + ); + + base.destroy(); + } + + /** + * Sets whether the composer is able to be used. + * + * If disabled, the composer hidden, detached from its container + * and will stop periodically saving drafts. + */ + public void set_enabled(bool enabled) { + this.is_closing = !enabled; + this.set_sensitive(enabled); + + if (enabled) { + this.open_draft_manager_async.begin(null, null); + } else { + if (this.container != null) { + this.container.close(); + } + this.draft_timer.reset(); + } + } + /** * Loads and sets contact auto-complete data for the current account. */ @@ -833,10 +918,10 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } if (show_extended) { this.editor_actions.change_action_state( - ACTION_SHOW_EXTENDED, true + ACTION_SHOW_EXTENDED_HEADERS, true ); this.composer_actions.change_action_state( - ACTION_SHOW_EXTENDED, true + ACTION_SHOW_EXTENDED_HEADERS, true ); } break; @@ -866,6 +951,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { public void present() { this.container.present(); + set_focus(); } public void set_focus() { @@ -905,7 +991,9 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.editor_actions, this.composer_actions }; foreach (var entries_users in composer_action_entries_users) { - entries_users.change_action_state(ACTION_SHOW_EXTENDED, false); + entries_users.change_action_state( + ACTION_SHOW_EXTENDED_HEADERS, false + ); entries_users.change_action_state( ACTION_COMPOSE_AS_HTML, this.application.config.compose_as_html ); @@ -1031,6 +1119,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { return true; } + /** Returns a representation of the current message. */ public async Geary.ComposedEmail get_composed_email(GLib.DateTime? date_override = null, bool for_draft = false) { Geary.ComposedEmail email = new Geary.ComposedEmail( @@ -1085,69 +1174,36 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { return email; } - public void change_compose_type(ComposeType new_type, Geary.Email? referred = null, - string? quote = null) { - if (referred != null && quote != null && quote != this.last_quote) { - this.last_quote = quote; - // Always use reply styling, since forward styling doesn't work for inline quotes + /** Appends an email or fragment quoted into the composer. */ + public void append_to_email(Geary.Email referred, + string? to_quote, + ComposeType type) + throws Geary.EngineError { + if (!referred.fields.is_all_set(REQUIRED_FIELDS)) { + throw new Geary.EngineError.INCOMPLETE_MESSAGE( + "Required fields not met: %s", referred.fields.to_string() + ); + } + + if (!this.referred_ids.contains(referred.id)) { + add_recipients_and_ids(type, referred); + } + + if (this.last_quote != to_quote) { + this.last_quote = to_quote; + // Always use reply styling, since forward styling doesn't + // work for inline quotes this.editor.insert_html( Util.Email.quote_email_for_reply( referred, - quote, + to_quote, this.application.config.clock_format, Geary.RFC822.TextFormat.HTML ) ); - - if (!referred_ids.contains(referred.id)) { - add_recipients_and_ids(new_type, referred); - - if (this.state != ComposerState.PANED && - this.state != ComposerState.DETACHED) { - this.state = Widget.ComposerState.PANED; - // XXX move the two lines below to the controller - this.container.remove_composer(); - GearyApplication.instance.controller.main_window.conversation_viewer.do_compose(this); - } - } - } else if (new_type != this.compose_type) { - bool recipients_modified = this.to_entry.modified || this.cc_entry.modified || this.bcc_entry.modified; - switch (new_type) { - case ComposeType.REPLY: - case ComposeType.REPLY_ALL: - this.subject = this.reply_subject; - if (!recipients_modified) { - this.to_entry.addresses = reply_to_addresses; - this.cc_entry.addresses = (new_type == ComposeType.REPLY_ALL) ? - reply_cc_addresses : null; - this.to_entry.modified = this.cc_entry.modified = false; - } else { - this.to_entry.select_region(0, -1); - } - break; - - case ComposeType.FORWARD: - if (this.state == ComposerState.INLINE_COMPACT) - this.state = ComposerState.INLINE; - this.subject = forward_subject; - if (!recipients_modified) { - this.to = ""; - this.cc = ""; - this.to_entry.modified = this.cc_entry.modified = false; - } else { - this.to_entry.select_region(0, -1); - } - break; - - default: - assert_not_reached(); - } - this.compose_type = new_type; } update_composer_view(); - this.container.present(); - set_focus(); } private void add_recipients_and_ids(ComposeType type, Geary.Email referred, @@ -1206,7 +1262,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { if (this.is_blank) return CloseStatus.DO_CLOSE; - this.container.present(); + present(); CloseStatus status = CloseStatus.PENDING_CLOSE; if (this.can_save) { @@ -1261,81 +1317,25 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { return status; } - private void on_close(SimpleAction action, Variant? param) { - if (should_close() == CloseStatus.DO_CLOSE) { - close(); - } + public override bool key_press_event(Gdk.EventKey event) { + // Override the method since key-press-event is run last, and + // we want this behaviour to take precedence over the default + // key handling + return check_send_on_return(event) && base.key_press_event(event); } - private void on_close_and_save(SimpleAction action, Variant? param) { - if (this.should_save) - save_and_exit_async.begin(); - else - this.container.close_container(); - } - - private void on_close_and_discard(SimpleAction action, Variant? param) { - discard_and_exit_async.begin(); - } - - private void on_detach() { - if (this.state == ComposerState.DETACHED) - return; - - Gtk.Widget? focused_widget = this.container.top_window.get_focus(); - this.container.remove_composer(); - Window new_window = new Window(this, this.application); - - // Workaround a GTK+ crasher, Bug 771812. When the composer is - // re-parented, its menu_button's popover keeps a reference to - // the conversation window's viewport, so when that is removed - // it has a null parent and we crash. To reproduce: Reply - // inline, detach the composer, then choose a different - // conversation back in the main window. The workaround here - // sets a new menu model and hence the menu_button constructs - // a new popover. - this.composer_actions.change_action_state( - ACTION_COMPOSE_AS_HTML, - this.application.config.compose_as_html - ); - - this.state = DETACHED; - update_composer_view(); - - // If the previously focused widget is in the new composer - // window then focus that, else focus something useful. - bool refocus = true; - if (focused_widget != null) { - Window? focused_window = focused_widget.get_toplevel() as Window; - if (new_window == focused_window) { - focused_widget.grab_focus(); - refocus = false; - } - } - if (refocus) { - set_focus(); - } - } - - public void embed_header() { + internal void embed_header() { if (this.header.parent == null) { this.header_area.add(this.header); this.header.hexpand = true; } } - public void free_header() { + internal void free_header() { if (this.header.parent != null) this.header.parent.remove(this.header); } - public override bool key_press_event(Gdk.EventKey event) { - // Override the method since key-press-event is run last, and - // we want this behaviour to take precedence over the default - // key handling - return check_send_on_return(event) && base.key_press_event(event); - } - // Updates the composer's UI after its state has changed private void update_composer_view() { this.recipients.set_visible(this.state != ComposerState.INLINE_COMPACT); @@ -1397,9 +1397,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { // Used internally by on_send() private async void on_send_async() { - this.editor.disable(); - this.container.vanish(); - this.is_closing = true; + set_enabled(false); // Perform send. try { @@ -1419,8 +1417,9 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } } - // Only close window after draft is deleted; this closes the drafts folder. - this.container.close_container(); + if (this.container != null) { + this.container.close(); + } } /** @@ -1488,7 +1487,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } private async void close_draft_manager_async(Cancellable? cancellable) - throws Error { + throws GLib.Error { this.draft_status_text = ""; get_action(ACTION_CLOSE_AND_SAVE).set_enabled(false); @@ -1581,42 +1580,19 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { return null; } - // Used while waiting for draft to save before closing widget. - private void make_gui_insensitive() { - this.container.vanish(); - this.draft_timer.reset(); - } - private async void save_and_exit_async() { - make_gui_insensitive(); - this.is_closing = true; - + set_enabled(false); yield save_draft(); - try { - yield close_draft_manager_async(null); - } catch (Error err) { - // ignored - } - container.close_container(); + yield close(); } private async void discard_and_exit_async() { - make_gui_insensitive(); - this.is_closing = true; - - // This method can be called even if drafts are not being - // saved, hence we need to check the draft manager - if (draft_manager != null) { + set_enabled(false); + if (this.draft_manager != null) { discard_draft(); draft_manager.discard_on_close = true; - try { - yield close_draft_manager_async(null); - } catch (Error err) { - // ignored - } } - - this.container.close_container(); + yield close(); } private void update_attachments_view() { @@ -2457,7 +2433,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } private void on_draft_id_changed() { - draft_id_changed(this.draft_manager.current_draft_id); + notify_property("current-draft-id"); } private void on_draft_manager_fatal(Error err) { @@ -2471,7 +2447,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { [GtkCallback] private void on_subject_changed() { draft_changed(); - subject_changed(this.subject); + notify_property("subject"); } [GtkCallback] @@ -2487,6 +2463,10 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } } + private void on_detach() { + detach(); + } + private bool on_button_release(Gdk.Event event) { // Show the link popover on mouse release (instead of press) // so the user can still select text with a link in it, @@ -2639,6 +2619,24 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { update_cursor_actions(); } + private void on_close(SimpleAction action, Variant? param) { + if (should_close() == CloseStatus.DO_CLOSE) { + this.close.begin(); + } + } + + private void on_close_and_save(SimpleAction action, Variant? param) { + if (this.should_save) { + save_and_exit_async.begin(); + } else { + this.close.begin(); + } + } + + private void on_close_and_discard(SimpleAction action, Variant? param) { + discard_and_exit_async.begin(); + } + private void on_account_available() { update_from_field(); } diff --git a/src/client/composer/composer-window.vala b/src/client/composer/composer-window.vala index f5a7ed83..e62ea1f5 100644 --- a/src/client/composer/composer-window.vala +++ b/src/client/composer/composer-window.vala @@ -1,12 +1,13 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. +/* + * Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2019 Michael Gratton * * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. + * (version 2.1 or later). See the COPYING file in this distribution. */ /** - * A ComposerWindow is a ComposerContainer that is used to compose mails in a separate window - * (i.e. detached) of its own. + * A container detached composers, i.e. in their own separate window. */ public class Composer.Window : Gtk.ApplicationWindow, Container { @@ -14,20 +15,20 @@ public class Composer.Window : Gtk.ApplicationWindow, Container { private const string DEFAULT_TITLE = _("New Message"); - public new GearyApplication application { - get { return (GearyApplication) base.get_application(); } - set { base.set_application(value); } - } - - public Gtk.ApplicationWindow top_window { + /** {@inheritDoc} */ + public Gtk.ApplicationWindow? top_window { get { return this; } } + /** {@inheritDoc} */ + public new GearyApplication? application { + get { return base.get_application() as GearyApplication; } + set { base.set_application(value); } + } + + /** {@inheritDoc} */ internal Widget composer { get; set; } - protected Gee.MultiMap? old_accelerators { get; set; } - - private bool closing = false; public Window(Widget composer, GearyApplication application) { Object(application: application, type: Gtk.WindowType.TOPLEVEL); @@ -47,13 +48,19 @@ public class Composer.Window : Gtk.ApplicationWindow, Container { set_titlebar(this.composer.header); } - composer.subject_changed.connect(() => { update_title(); } ); + composer.notify["subject"].connect(() => { update_title(); } ); update_title(); show(); set_position(Gtk.WindowPosition.CENTER); } + /** {@inheritDoc} */ + public new void close() { + remove(this.composer); + destroy(); + } + public override void show() { Gdk.Display? display = Gdk.Display.get_default(); if (display != null) { @@ -104,22 +111,13 @@ public class Composer.Window : Gtk.ApplicationWindow, Container { this.save_window_geometry(); } - public void close_container() { - this.closing = true; - destroy(); - } - public override bool delete_event(Gdk.EventAny event) { - return !(this.closing || - ((Widget) get_child()).should_close() == Widget.CloseStatus.DO_CLOSE); - } - - public void vanish() { - hide(); - } - - public void remove_composer() { - warning("Detached composer received remove"); + bool ret = Gdk.EVENT_PROPAGATE; + Widget? composer = get_child() as Widget; + if (composer != null && composer.should_close() == CANCEL_CLOSE) { + ret = Gdk.EVENT_STOP; + } + return ret; } private void update_title() { diff --git a/src/client/conversation-viewer/conversation-list-box.vala b/src/client/conversation-viewer/conversation-list-box.vala index 1e636244..377f66e6 100644 --- a/src/client/conversation-viewer/conversation-list-box.vala +++ b/src/client/conversation-viewer/conversation-list-box.vala @@ -851,7 +851,9 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { add(row); this.has_composer = true; - embed.composer.draft_id_changed.connect((id) => { this.draft_id = id; }); + embed.composer.notify["current-draft-id"].connect( + (id) => { this.draft_id = embed.composer.current_draft_id; } + ); embed.vanished.connect(() => { this.has_composer = false; this.draft_id = null; diff --git a/src/client/conversation-viewer/conversation-viewer.vala b/src/client/conversation-viewer/conversation-viewer.vala index 6ea1fe8d..bf769704 100644 --- a/src/client/conversation-viewer/conversation-viewer.vala +++ b/src/client/conversation-viewer/conversation-viewer.vala @@ -184,7 +184,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { if (this.current_list != null) { this.current_list.add_embedded_composer( embed, - composer.draft_id != null + composer.current_draft_id != null ); } diff --git a/ui/composer-menus.ui b/ui/composer-menus.ui index 1bd5b64b..66554ac2 100644 --- a/ui/composer-menus.ui +++ b/ui/composer-menus.ui @@ -66,7 +66,7 @@
Show Extended Fields - win.show-extended + win.show-extended-headers