[geary/mjog/493-undo-send: 13/25] Clean up the Composer API
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/mjog/493-undo-send: 13/25] Clean up the Composer API
- Date: Tue, 12 Nov 2019 21:48:57 +0000 (UTC)
commit 70c02047aa408fb97b660090842f936347aea128
Author: Michael Gratton <mike vee net>
Date: Sat Nov 9 16:46:41 2019 +1100
Clean up the Composer API
Make ComposerWidget::close method handle a lot more common code,
allowing many handlers to be simplified. Make access for some properties
more private and add accessors as appropriate, replace some "notify is
too hard" singnals with actual notify calls. Rename a few other
properties to better indicate what they do. Reintroduce `is-draft`
argument to ::load so we can accurately determine if we are loading a
draft, and so the `draft_id` param can be removed from the ctor.
Introduce a ::set_enabled method that can be used to disable and hide
the composer before closing it. Rename ::change_compose_type to
::append_to_email and reduce its scope drastically.
Drastically simplify ComposerContainer's API and its implementing
classes, reducing the API surface down to a single method call. Ensure
its properties that could be null are nullable, update call sites.
Remove dead code in all of the above classes, add more API docs.
src/client/application/application-controller.vala | 55 ++-
src/client/components/main-window.vala | 33 +-
src/client/composer/composer-box.vala | 33 +-
src/client/composer/composer-container.vala | 41 +-
src/client/composer/composer-embed.vala | 49 +-
src/client/composer/composer-web-view.vala | 7 -
src/client/composer/composer-widget.vala | 514 ++++++++++-----------
src/client/composer/composer-window.vala | 54 ++-
.../conversation-viewer/conversation-list-box.vala | 4 +-
.../conversation-viewer/conversation-viewer.vala | 2 +-
ui/composer-menus.ui | 2 +-
11 files changed, 390 insertions(+), 404 deletions(-)
---
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 <mike vee net>
*
* 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<string, string>? 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.
- internal abstract Widget composer { get; set; }
-
- // We use old_accelerators to keep track of the accelerators we temporarily disabled.
- protected abstract Gee.MultiMap<string, string>? old_accelerators { get; set; }
+ /** The top-level window for the container, if any. */
+ public abstract Gtk.ApplicationWindow? top_window { get; }
- // The toplevel window for the container. Note that it needs to be a GtkApplicationWindow.
- public abstract Gtk.ApplicationWindow top_window { get; }
+ /** The container's current composer, if any. */
+ internal abstract Widget composer { get; set; }
+ /** 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).
- */
- public abstract void vanish();
-
/**
- * Removes the composer from this ComposerContainer (e.g. when detaching)
+ * Removes the composer and destroys the container.
*/
- 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 <mike vee net>
*
* 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<string, string>? 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 a7e467a0..a13cc329 100644
--- a/src/client/composer/composer-web-view.vala
+++ b/src/client/composer/composer-web-view.vala
@@ -187,13 +187,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 3dba017b..8b2b10a6 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 <mike vee net>
+ * Copyright 2017-2019 Michael Gratton <mike vee net>
*
* 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) {
@@ -183,48 +184,22 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
_("attach|attaching|attaches|attachment|attachments|attached|enclose|enclosed|enclosing|encloses|enclosure|enclosures");
+ /** The account the email is being sent from. */
public Geary.Account account { get; private set; }
- private Gee.Map<string, Geary.AccountInformation> 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 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<Geary.RFC822.MessageID> in_reply_to = new Gee.HashSet<Geary.RFC822.MessageID>();
- public string references { get; set; }
-
- public string subject {
- get { return this.subject_entry.get_text(); }
- set { this.subject_entry.set_text(value); }
+ public Geary.EmailIdentifier? current_draft_id {
+ get {
+ return this.draft_manager != null
+ ? this.draft_manager.current_draft_id : null;
+ }
}
- 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<Geary.EmailIdentifier> referred_ids = new Gee.HashSet<Geary.EmailIdentifier>();
-
/** Determines if the composer is completely empty. */
public bool is_blank {
get {
@@ -238,33 +213,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; }
+ public WebView editor { get; private set; }
+
+ internal Headerbar header { get; private set; }
+
+ internal string subject {
+ get { return this.subject_entry.get_text(); }
+ private set { this.subject_entry.set_text(value); }
}
- /** 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 Geary.RFC822.MailboxAddresses from { get; private set; }
+
+ private string to {
+ get { return this.to_entry.get_text(); }
+ set { this.to_entry.set_text(value); }
}
- public Headerbar header { get; private set; }
+ private string cc {
+ get { return this.cc_entry.get_text(); }
+ set { this.cc_entry.set_text(value); }
+ }
- public WebView editor { get; private set; }
+ 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); }
+ }
- public string window_title { get; set; }
+ private Gee.Set<Geary.RFC822.MessageID> in_reply_to = new Gee.HashSet<Geary.RFC822.MessageID>();
- private string body_html = "";
+ 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;
@@ -356,6 +344,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<string, Geary.AccountInformation> accounts;
+
+ private string body_html = "";
+
private SpellCheckPopover? spell_check_popover = null;
private string? pointer_url = null;
private string? cursor_url = null;
@@ -367,6 +373,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<Geary.EmailIdentifier> referred_ids =
+ new Gee.HashSet<Geary.EmailIdentifier>();
+
private Gee.List<Geary.Attachment>? pending_attachments = null;
private AttachPending pending_include = AttachPending.INLINE_ONLY;
private Gee.Set<File> attached_files = new Gee.HashSet<File>(Geary.Files.nullable_hash,
@@ -400,31 +410,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();
@@ -561,33 +559,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<string, string> headers = new Gee.HashMultiMap<string, string>();
if (mailto.has_prefix(MAILTO_URI_PREFIX)) {
@@ -637,15 +619,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<Geary.EmailIdentifier> 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 {
@@ -655,21 +648,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();
}
}
@@ -685,12 +673,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) {
@@ -698,6 +686,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.
*/
@@ -825,10 +910,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;
@@ -858,6 +943,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
public void present() {
this.container.present();
+ set_focus();
}
public void set_focus() {
@@ -897,7 +983,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
);
@@ -1023,6 +1111,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(
@@ -1077,69 +1166,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,
@@ -1198,7 +1254,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) {
@@ -1253,81 +1309,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();
- }
- }
-
- 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 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);
}
- 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);
@@ -1389,9 +1389,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 {
@@ -1411,8 +1409,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();
+ }
}
/**
@@ -1480,7 +1479,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);
@@ -1573,42 +1572,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() {
@@ -2382,7 +2358,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) {
@@ -2396,7 +2372,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]
@@ -2412,6 +2388,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,
@@ -2561,6 +2541,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 <mike vee net>
*
* 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); }
+ /** {@inheritDoc} */
+ public Gtk.ApplicationWindow? top_window {
+ get { return this; }
}
- 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<string, string>? 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 @@
<section>
<item>
<attribute name="label" translatable="yes">Show Extended Fields</attribute>
- <attribute name="action">win.show-extended</attribute>
+ <attribute name="action">win.show-extended-headers</attribute>
</item>
</section>
</menu>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]