[geary/wip/714104-refine-account-dialog: 25/69] Implement editing for the account editor pane.



commit d9b94c1c612fa79596f35d15fc639b0a8b12b9e6
Author: Michael James Gratton <mike vee net>
Date:   Wed Jun 13 17:44:17 2018 +1000

    Implement editing for the account editor pane.

 src/client/accounts/accounts-editor-edit-pane.vala | 707 ++++++++++++++++++---
 src/client/accounts/accounts-editor-row.vala       | 203 +++++-
 .../accounts/accounts-editor-servers-pane.vala     | 139 +---
 src/client/accounts/accounts-editor.vala           | 122 +++-
 src/client/application/geary-controller.vala       |   2 +
 src/engine/api/geary-account-information.vala      |  75 +++
 ui/accounts_editor.ui                              |  33 +-
 ui/accounts_editor_edit_pane.ui                    |  75 ++-
 ui/client-web-view.js                              |   2 +
 ui/geary.css                                       |  13 +
 10 files changed, 1103 insertions(+), 268 deletions(-)
---
diff --git a/src/client/accounts/accounts-editor-edit-pane.vala 
b/src/client/accounts/accounts-editor-edit-pane.vala
index b46c61d4..260e9aa5 100644
--- a/src/client/accounts/accounts-editor-edit-pane.vala
+++ b/src/client/accounts/accounts-editor-edit-pane.vala
@@ -12,19 +12,23 @@
 public class Accounts.EditorEditPane : Gtk.Grid {
 
 
-    private weak Editor editor; // circular ref
-    private Geary.AccountInformation account;
+    /** The editor this pane belongs to. */
+    internal weak Editor editor; // circular ref
+
+    /** The account being displayed by this pane. */
+    internal Geary.AccountInformation account;
 
     [GtkChild]
     private Gtk.ListBox details_list;
 
     [GtkChild]
-    private Gtk.ListBox addresses_list;
+    private Gtk.ListBox senders_list;
 
     [GtkChild]
-    private Gtk.ScrolledWindow signature_scrolled;
+    private Gtk.Frame signature_frame;
 
     private ClientWebView signature_preview;
+    private bool signature_changed = false;
 
     [GtkChild]
     private Gtk.ListBox settings_list;
@@ -35,58 +39,85 @@ public class Accounts.EditorEditPane : Gtk.Grid {
         this.editor = editor;
         this.account = account;
 
-        PropertyRow nickname_row = new PropertyRow(
-            account,
-            "nickname",
-            // Translators: This label in the account editor is for
-            // the user's name for an account.
-            _("Account name")
-        );
-        nickname_row.set_dim_label(true);
-
         this.details_list.set_header_func(Editor.seperator_headers);
-        this.details_list.add(nickname_row);
-
-        this.addresses_list.set_header_func(Editor.seperator_headers);
-        this.addresses_list.add(
-            new AddressRow(account.primary_mailbox, get_login_session_name())
-        );
+        this.details_list.add(new NicknameRow(account));
 
-        string? default_name = account.primary_mailbox.name;
-        if (Geary.String.is_empty_or_whitespace(default_name)) {
-            default_name = null;
-        }
-        if (account.alternate_mailboxes != null) {
-            foreach (Geary.RFC822.MailboxAddress alt
-                     in account.alternate_mailboxes) {
-                this.addresses_list.add(new AddressRow(alt, default_name));
-            }
+        this.senders_list.set_header_func(Editor.seperator_headers);
+        foreach (Geary.RFC822.MailboxAddress sender
+                 in account.get_sender_mailboxes()) {
+            this.senders_list.add(new MailboxRow(account, sender));
         }
-
-        this.addresses_list.add(new AddRow());
+        this.senders_list.add(new AddMailboxRow(this));
 
         this.signature_preview = new ClientWebView(
             ((GearyApplication) editor.application).config
         );
-        this.signature_preview.load_html(account.email_signature);
+        this.signature_preview.events = (
+            this.signature_preview.events | Gdk.EventType.FOCUS_CHANGE
+        );
+        this.signature_preview.content_loaded.connect(() => {
+                debug("Signature loaded");
+                // Only enable editability after the content has fully
+                // loaded to avoid the WebProcess crashing.
+                this.signature_preview.set_editable.begin(true, null);
+            });
+        this.signature_preview.document_modified.connect(() => {
+                debug("Signature changed");
+                this.signature_changed = true;
+            });
+        this.signature_preview.focus_in_event.connect(() => {
+                debug("Sig focus in");
+                return Gdk.EVENT_PROPAGATE;
+            });
+        this.signature_preview.focus_out_event.connect(() => {
+                debug("Sig focus out");
+                // This event will also be fired if the top-level
+                // window loses focus, e.g. if the user alt-tabs away,
+                // so don't execute the command if the signature web
+                // view no longer the focus widget
+                if (!this.signature_preview.is_focus &&
+                    this.signature_changed) {
+                    editor.commands.execute.begin(
+                        new SignatureChangedCommand(
+                            this.signature_preview, account
+                        ),
+                        null
+                    );
+                }
+                return Gdk.EVENT_PROPAGATE;
+            });
+
         this.signature_preview.show();
+        this.signature_preview.load_html(
+            Geary.HTML.smart_escape(account.email_signature)
+        );
 
-        this.signature_scrolled.add(this.signature_preview);
+        this.signature_frame.add(this.signature_preview);
 
         this.settings_list.set_header_func(Editor.seperator_headers);
-        // No settings to show at the moment, so hide the list and its
-        // frame until we do.
-        this.settings_list.get_parent().hide();
+        this.settings_list.add(new EmailPrefetchRow(editor, this.account));
     }
 
-    private string? get_login_session_name() {
-        string? name = Environment.get_real_name();
-        if (Geary.String.is_empty(name) || name == "Unknown") {
-            name = null;
+    internal string? get_default_name() {
+        string? name = account.primary_mailbox.name;
+
+        if (Geary.String.is_empty_or_whitespace(name)) {
+            name = Environment.get_real_name();
+            if (Geary.String.is_empty(name) || name == "Unknown") {
+                name = null;
+            }
         }
+
         return name;
     }
 
+    [GtkCallback]
+    private void on_setting_activated(Gtk.ListBoxRow row) {
+        EditorRow? setting = row as EditorRow;
+        if (setting != null) {
+            setting.activated(this.editor);
+        }
+    }
 
     [GtkCallback]
     private void on_server_settings_clicked() {
@@ -97,79 +128,601 @@ public class Accounts.EditorEditPane : Gtk.Grid {
     private void on_remove_account_clicked() {
         this.editor.push(new EditorRemovePane(this.editor, this.account));
     }
+
 }
 
 
-private class Accounts.PropertyRow : LabelledEditorRow {
+private class Accounts.NicknameRow : AccountRow<Gtk.Label> {
 
 
-    private GLib.Object object;
-    private string property_name;
+    public NicknameRow(Geary.AccountInformation account) {
+        base(
+            account,
+            // Translators: Label in the account editor for the user's
+            // custom name for an account.
+            _("Account name"),
+            new Gtk.Label("")
+        );
+        update();
+    }
+
+    public override void activated(Accounts.Editor editor) {
+        EditorPopover popover = new EditorPopover();
+
+        string? value = this.account.nickname;
+        Gtk.Entry entry = new Gtk.Entry();
+        entry.set_text(value ?? "");
+        entry.set_placeholder_text(value ?? "");
+        entry.set_width_chars(20);
+        entry.activate.connect(() => {
+                editor.commands.execute.begin(
+                    new PropertyCommand<string>(
+                        this.account,
+                        this.account,
+                        "nickname",
+                        entry.get_text(),
+                        // Translators: Tooltip used to undo changing
+                        // the name of an account. The string
+                        // substitution is the old name of the
+                        // account.
+                        _("Change account name back to “%s”")
+                    ),
+                    null
+                );
+                popover.popdown();
+            });
+        entry.show();
+
+        popover.add_labelled_row(
+            // Translators: Label used when editing the account's
+            // name.
+            _("Account name:"),
+            entry
+        );
+
+        popover.set_relative_to(this);
+        popover.layout.add(entry);
+        popover.popup();
+    }
+
+    public override void update() {
+        this.value.set_text(this.account.nickname);
+    }
+
+}
+
+
+private class Accounts.AddMailboxRow : AddRow {
+
+
+    private EditorEditPane edit_pane;
+
+
+    public AddMailboxRow(EditorEditPane edit_pane) {
+        this.edit_pane = edit_pane;
+
+        // Translators: Tooltip for adding a new email sender/from
+        // address's address to an account
+        this.set_tooltip_text(_("Add a new sender email address"));
+    }
+
+    public override void activated(Accounts.Editor editor) {
+        MailboxEditorPopover popover = new MailboxEditorPopover(
+            this.edit_pane.get_default_name() ?? "", "", false
+        );
+        popover.activated.connect(() => {
+                editor.commands.execute.begin(
+                    new AppendMailboxCommand(
+                        (Gtk.ListBox) get_parent(),
+                        new MailboxRow(
+                            this.edit_pane.account,
+                            new Geary.RFC822.MailboxAddress(
+                                popover.display_name,
+                                popover.address
+                            )
+                        )
+                    ),
+                    null
+                );
+                popover.popdown();
+            });
+
+        popover.set_relative_to(this);
+        popover.popup();
+    }
+}
+
 
-    private Gtk.Label value = new Gtk.Label("");
+private class Accounts.MailboxRow : AccountRow<Gtk.Label> {
 
 
-    public PropertyRow(Object object,
-                       string property_name,
-                       string label) {
-        base(label);
+    internal Geary.RFC822.MailboxAddress mailbox;
 
-        this.object = object;
-        this.property_name = property_name;
 
-        this.value.show();
-        this.layout.add(this.value);
+    public MailboxRow(Geary.AccountInformation account,
+                      Geary.RFC822.MailboxAddress mailbox) {
+        base(account, "", new Gtk.Label(""));
+        this.mailbox = mailbox;
 
         update();
     }
 
-    public void update() {
-        string? value = null;
-        this.object.get(this.property_name, ref value);
+    public override void activated(Accounts.Editor editor) {
+        MailboxEditorPopover popover = new MailboxEditorPopover(
+            this.mailbox.name ?? "",
+            this.mailbox.address,
+            this.account.get_sender_mailboxes().size > 1
+        );
+        popover.activated.connect(() => {
+                editor.commands.execute.begin(
+                    new UpdateMailboxCommand(
+                        this,
+                        new Geary.RFC822.MailboxAddress(
+                            popover.display_name,
+                            popover.address
+                        )
+                    ),
+                    null
+                );
+                popover.popdown();
+            });
+        popover.remove_clicked.connect(() => {
+                editor.commands.execute.begin(
+                    new RemoveMailboxCommand(this), null
+                );
+                popover.popdown();
+            });
+
+        popover.set_relative_to(this);
+        popover.popup();
+    }
 
-        if (value != null) {
-            this.value.set_text(value);
+    public override void update() {
+        string? name = this.mailbox.name;
+        if (Geary.String.is_empty_or_whitespace(name)) {
+            // Translators: Label used to indicate the user has
+            // provided no display name for one of their sender
+            // email addresses in their account settings.
+            name = _("Name not set");
+            set_dim_label(true);
+        } else {
+            set_dim_label(false);
         }
+
+        this.label.set_text(name);
+        this.value.set_text(mailbox.address.strip());
     }
 
 }
 
+internal class Accounts.MailboxEditorPopover : EditorPopover {
 
-private class Accounts.AddressRow : LabelledEditorRow {
 
+    public string display_name { get; private set; }
+    public string address { get; private set; }
 
-    private Geary.RFC822.MailboxAddress address;
-    private string? fallback_name;
 
-    private Gtk.Label value = new Gtk.Label("");
+    private Gtk.Entry name_entry = new Gtk.Entry();
+    private Gtk.Entry address_entry = new Gtk.Entry();
+    private Gtk.Button remove_button;
+    private bool is_valid = true;
+    private Geary.TimeoutManager validation_timeout;
 
 
-    public AddressRow(Geary.RFC822.MailboxAddress address,
-                      string? fallback_name) {
-        base("");
+    public signal void activated();
+    public signal void remove_clicked();
+
+
+    public MailboxEditorPopover(string? display_name,
+                                string? address,
+                                bool can_remove) {
+        this.display_name = display_name;
         this.address = address;
-        this.fallback_name = fallback_name;
 
-        this.value.show();
-        this.layout.add(this.value);
+        this.validation_timeout = new Geary.TimeoutManager.milliseconds(
+            150, () => { validate(); }
+        );
 
-        update();
+        this.name_entry.set_text(display_name ?? "");
+        this.name_entry.set_placeholder_text(
+            // Translators: This is used as a placeholder for the
+            // display name for an email address when editing a user's
+            // sender address preferences for an account.
+            _("Sender Name")
+        );
+        this.name_entry.set_width_chars(20);
+        this.name_entry.changed.connect(on_name_changed);
+        this.name_entry.activate.connect(on_activate);
+        this.name_entry.show();
+
+        this.address_entry.input_purpose = Gtk.InputPurpose.EMAIL;
+        this.address_entry.set_text(address ?? "");
+        this.address_entry.set_placeholder_text(
+            // Translators: This is used as a placeholder for the
+            // address part of an email address when editing a user's
+            // sender address preferences for an account.
+            _("person example com")
+        );
+        this.address_entry.set_width_chars(20);
+        this.address_entry.changed.connect(on_address_changed);
+        this.address_entry.activate.connect(on_activate);
+        this.address_entry.show();
+
+        this.remove_button = new Gtk.Button.with_label(_("Remove"));
+        this.remove_button.halign = Gtk.Align.END;
+        this.remove_button.get_style_context().add_class(
+            "geary-setting-remove"
+        );
+        this.remove_button.get_style_context().add_class(
+            Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION
+        );
+        this.remove_button.clicked.connect(on_remove_clicked);
+        this.remove_button.show();
+
+        add_labelled_row(
+            // Translators: Label used for the display name part of an
+            // email address when editing a user's sender address
+            // preferences for an account.
+            _("Sender name:"),
+            this.name_entry
+        );
+        add_labelled_row(
+            // Translators: Label used for the address part of an
+            // email address when editing a user's sender address
+            // preferences for an account.
+            _("Email address:"),
+            this.address_entry
+        );
+
+        if (can_remove) {
+            this.layout.attach(this.remove_button, 0, 2, 2, 1);
+        }
+
+        this.popup_focus = this.name_entry;
     }
 
-    public void update() {
-        string? name = Geary.String.is_empty_or_whitespace(this.address.name)
-            ? this.fallback_name
-            : this.address.name;
+    ~MailboxEditorPopover() {
+        this.validation_timeout.reset();
 
-        if (Geary.String.is_empty_or_whitespace(name)) {
-            name = _("No name set");
-            set_dim_label(true);
+        this.name_entry.changed.disconnect(on_name_changed);
+        this.name_entry.activate.disconnect(on_activate);
+
+        this.address_entry.changed.disconnect(on_address_changed);
+        this.address_entry.activate.disconnect(on_activate);
+
+        this.remove_button.clicked.disconnect(on_remove_clicked);
+    }
+
+    private void validate() {
+        Gtk.Entry entry = this.address_entry;
+        this.is_valid = Geary.RFC822.MailboxAddress.is_valid_address(
+            this.address
+        );
+        Gtk.StyleContext style = entry.get_style_context();
+        Gtk.EntryIconPosition pos = Gtk.EntryIconPosition.SECONDARY;
+        if (!this.is_valid) {
+            style.add_class(Gtk.STYLE_CLASS_ERROR);
+            entry.set_icon_from_icon_name(
+                pos, "dialog-error-symbolic"
+            );
+            entry.set_tooltip_text(
+                _("Email address is not valid, e.g. person example com")
+            );
         } else {
-            set_dim_label(false);
+            style.remove_class(Gtk.STYLE_CLASS_ERROR);
+            entry.set_icon_from_icon_name(pos, null);
+            entry.set_tooltip_text("");
         }
+    }
 
-        this.label.set_text(name);
-        this.value.set_text(this.address.address.strip());
+    private void on_name_changed() {
+        this.display_name = this.name_entry.get_text().strip();
+    }
+
+    private void on_address_changed() {
+        this.address = this.address_entry.get_text().strip();
+        this.validation_timeout.start();
+    }
+
+    private void on_remove_clicked() {
+        remove_clicked();
+    }
+
+    private void on_activate() {
+        if (this.address != "" && this.is_valid) {
+            activated();
+        }
+    }
+
+}
+
+
+internal class Accounts.AppendMailboxCommand : Application.Command {
+
+
+    private Gtk.ListBox senders_list;
+    private MailboxRow new_row = null;
+
+    private int mailbox_index;
+
+
+    public AppendMailboxCommand(Gtk.ListBox senders_list, MailboxRow new_row) {
+        this.senders_list = senders_list;
+        this.new_row = new_row;
+
+        this.mailbox_index = new_row.account.get_sender_mailboxes().size;
+
+        // Translators: Label used as the undo tooltip after adding an
+        // new sender email address to an account. The string
+        // substitution is the email address added.
+        this.undo_label = _("Remove “%s”").printf(new_row.mailbox.address);
+    }
+
+    public async override void execute(GLib.Cancellable? cancellable) {
+        this.senders_list.insert(this.new_row, this.mailbox_index);
+        this.new_row.account.append_sender_mailbox(this.new_row.mailbox);
+        this.new_row.account.information_changed();
+    }
+
+    public async override void undo(GLib.Cancellable? cancellable) {
+        this.senders_list.remove(this.new_row);
+        this.new_row.account.remove_sender_mailbox(this.new_row.mailbox);
+        this.new_row.account.information_changed();
+    }
+
+}
+
+
+internal class Accounts.UpdateMailboxCommand : Application.Command {
+
+
+    private MailboxRow row;
+    private Geary.RFC822.MailboxAddress new_mailbox;
+
+    private Geary.RFC822.MailboxAddress old_mailbox;
+    private int mailbox_index;
+
+
+    public UpdateMailboxCommand(MailboxRow row,
+                                Geary.RFC822.MailboxAddress new_mailbox) {
+        this.row = row;
+        this.new_mailbox = new_mailbox;
+
+        this.old_mailbox = row.mailbox;
+        this.mailbox_index =
+            row.account.get_sender_mailboxes().index_of(this.old_mailbox);
+
+        // Translators: Label used as the undo tooltip after editing a
+        // sender address for an account. The string substitution is
+        // the email address edited.
+        this.undo_label = _("Undo changes to “%s”").printf(
+            this.old_mailbox.address
+        );
+    }
+
+    public async override void execute(GLib.Cancellable? cancellable) {
+        this.row.mailbox = this.new_mailbox;
+        this.row.account.remove_sender_mailbox(this.old_mailbox);
+        this.row.account.insert_sender_mailbox(this.mailbox_index, this.new_mailbox);
+        this.row.account.information_changed();
+    }
+
+    public async override void undo(GLib.Cancellable? cancellable) {
+        this.row.mailbox = this.old_mailbox;
+        this.row.account.remove_sender_mailbox(this.new_mailbox);
+        this.row.account.insert_sender_mailbox(this.mailbox_index, this.old_mailbox);
+        this.row.account.information_changed();
+    }
+
+}
+
+
+internal class Accounts.RemoveMailboxCommand : Application.Command {
+
+
+    private MailboxRow row;
+
+    private Geary.RFC822.MailboxAddress mailbox;
+    private int mailbox_index;
+    private Gtk.ListBox list;
+
+
+    public RemoveMailboxCommand(MailboxRow row) {
+        this.row = row;
+
+        this.mailbox = row.mailbox;
+        this.mailbox_index =
+            row.account.get_sender_mailboxes().index_of(mailbox);
+        this.list = (Gtk.ListBox) row.get_parent();
+
+        // Translators: Label used as the undo tooltip after removing
+        // a sender address from an account. The string substitution
+        // is the email address edited.
+        this.undo_label = _("Add “%s” back").printf(this.mailbox.address);
+    }
+
+    public async override void execute(GLib.Cancellable? cancellable) {
+        this.list.remove(this.row);
+        this.row.account.remove_sender_mailbox(this.mailbox);
+        this.row.account.information_changed();
+    }
+
+    public async override void undo(GLib.Cancellable? cancellable) {
+        this.list.insert(this.row, this.mailbox_index);
+        this.row.account.insert_sender_mailbox(this.mailbox_index, this.mailbox);
+        this.row.account.information_changed();
+    }
+
+}
+
+
+internal class Accounts.SignatureChangedCommand : Application.Command {
+
+
+    private ClientWebView signature_view;
+    private Geary.AccountInformation account;
+
+    private string old_value;
+    private string? new_value = null;
+
+
+    public SignatureChangedCommand(ClientWebView signature_view,
+                                   Geary.AccountInformation account) {
+        this.signature_view = signature_view;
+        this.account = account;
+
+        this.old_value = Geary.HTML.smart_escape(account.email_signature);
+
+        // Translators: Label used as the undo tooltip after removing
+        // a sender address from an account. The string substitution
+        // is the email address edited.
+        this.undo_label = _("Undo signature changes");
+    }
+
+    public async override void execute(GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        this.new_value = yield this.signature_view.get_html();
+        update_account_signature(this.new_value);
+    }
+
+    public async override void undo(GLib.Cancellable? cancellable) {
+        this.signature_view.load_html(this.old_value);
+        update_account_signature(this.old_value);
+    }
+
+    public async override void redo(GLib.Cancellable? cancellable) {
+        this.signature_view.load_html(this.new_value);
+        update_account_signature(this.new_value);
+    }
+
+    private inline void update_account_signature(string value) {
+        this.account.email_signature = value;
+        this.account.information_changed();
+    }
+
+}
+
+
+private class Accounts.EmailPrefetchRow : AccountRow<Gtk.ComboBoxText> {
+
+
+    private static bool row_separator(Gtk.TreeModel model, Gtk.TreeIter iter) {
+        GLib.Value v;
+        model.get_value(iter, 0, out v);
+        return v.get_string() == ".";
+    }
+
+
+    public EmailPrefetchRow(Accounts.Editor editor,
+                            Geary.AccountInformation account) {
+        base(
+            account,
+            // Translators: This label describes the account
+            // preference for the length of time (weeks, months or
+            // years) that past email should be downloaded.
+            _("Download mail"),
+            new Gtk.ComboBoxText()
+        );
+        set_activatable(false);
+
+        this.value.set_row_separator_func(row_separator);
+
+        // Populate the model
+        get_label(14, true);
+        get_label(30, true);
+        get_label(90, true);
+        get_label(180, true);
+        get_label(365, true);
+        get_label(720, true);
+        get_label(1461, true);
+        get_label(-1, true);
+
+        // Update before connecting to the changed signal to avoid
+        // getting a spurious command.
+        update();
+
+        this.value.changed.connect(() => {
+                editor.commands.execute.begin(
+                    new PropertyCommand<int>(
+                        this.account,
+                        this.account,
+                        "prefetch-period-days",
+                        int.parse(this.value.get_active_id()),
+                        // Translators: Tooltip for undoing a change
+                        // to the length of time that past email
+                        // should be downloaded for an account. The
+                        // string substitution is the duration,
+                        // e.g. "1 month back".
+                        _("Change download period back to: %s").printf(
+                            get_label(this.account.prefetch_period_days)
+                        )
+                    ),
+                    null
+                );
+            });
+    }
+
+    public override void update() {
+        string id = this.account.prefetch_period_days.to_string();
+        if (this.value.get_active_id() != id) {
+            this.value.set_active_id(id);
+        }
+    }
+
+    private string get_label(int duration, bool append = false) {
+        string label = "";
+        bool is_custom = false;
+        switch (duration) {
+        case -1:
+            label = _("Everything");
+            break;
+
+        case 14:
+            label = _("2 weeks back");
+            break;
+
+        case 30:
+            label = _("1 month back");
+            break;
+
+        case 90:
+            label = _("3 months back");
+            break;
+
+        case 180:
+            label = _("6 months back");
+            break;
+
+        case 365:
+            label = _("1 year back");
+            break;
+
+        case 720:
+            label = _("2 years back");
+            break;
+
+        case 1461:
+            label = _("4 years back");
+            break;
+
+        default:
+            is_custom = true;
+            label = GLib.ngettext(
+                "%d day back",
+                "%d days back",
+                duration
+            ).printf(duration);
+            break;
+        }
+
+        if (append) {
+            if (duration == -1 || is_custom) {
+                this.value.append(".", "."); // Separator
+            }
+            this.value.append(duration.to_string(), label);
+        }
+
+        return label;
     }
 
 }
diff --git a/src/client/accounts/accounts-editor-row.vala b/src/client/accounts/accounts-editor-row.vala
index 0b9159eb..38718b29 100644
--- a/src/client/accounts/accounts-editor-row.vala
+++ b/src/client/accounts/accounts-editor-row.vala
@@ -22,22 +22,35 @@ internal class Accounts.EditorRow : Gtk.ListBoxRow {
         this.show();
     }
 
+    public virtual void activated(Accounts.Editor editor) {
+        // No-op by default
+    }
+
 }
 
 
-internal class Accounts.LabelledEditorRow : EditorRow {
+internal class Accounts.LabelledEditorRow<V> : EditorRow {
 
 
     protected Gtk.Label label { get; private set; default = new Gtk.Label(""); }
+    protected V value;
 
 
-    public LabelledEditorRow(string label) {
-        this.label.set_text(label);
-        this.label.set_hexpand(true);
+    public LabelledEditorRow(string label, V value) {
+        this.label.hexpand = true;
         this.label.halign = Gtk.Align.START;
+        this.label.valign = Gtk.Align.CENTER;
+        this.label.set_text(label);
         this.label.show();
-
         this.layout.add(this.label);
+
+        this.value = value;
+        Gtk.Widget? widget = value as Gtk.Widget;
+        if (widget != null) {
+            widget.valign = Gtk.Align.CENTER;
+            widget.show();
+            this.layout.add(widget);
+        }
     }
 
     public void set_dim_label(bool is_dim) {
@@ -65,5 +78,185 @@ internal class Accounts.AddRow : EditorRow {
         this.layout.add(add_icon);
     }
 
+}
+
+
+internal abstract class Accounts.AccountRow<V> : LabelledEditorRow<V> {
+
+
+    internal Geary.AccountInformation account { get; private set; }
+
+
+    public AccountRow(Geary.AccountInformation account, string label, V value) {
+        base(label, value);
+        this.account = account;
+        this.account.information_changed.connect(on_account_changed);
+
+        set_dim_label(true);
+    }
+
+    ~AccountRow() {
+        this.account.information_changed.disconnect(on_account_changed);
+    }
+
+    public abstract void update();
+
+    private void on_account_changed() {
+        update();
+    }
+
+}
+
+
+private abstract class Accounts.ServiceRow<V> : AccountRow<V> {
+
+
+    internal Geary.ServiceInformation service { get; private set; }
+
+    protected virtual bool is_value_editable {
+        get {
+            return (
+                this.account.service_provider == Geary.ServiceProvider.OTHER &&
+                !this.is_goa_account
+            );
+        }
+    }
+
+    // XXX convenience method until we get a better way of doing this.
+    protected bool is_goa_account {
+        get { return (this.service.mediator is GoaMediator); }
+    }
+
+
+    public ServiceRow(Geary.AccountInformation account,
+                      Geary.ServiceInformation service,
+                      string label,
+                      V value) {
+        base(account, label, value);
+        this.service = service;
+
+        bool is_editable = this.is_value_editable;
+        set_activatable(is_editable);
+
+        Gtk.Widget? widget = value as Gtk.Widget;
+        if (widget != null && !is_editable) {
+            if (widget is Gtk.Label) {
+                widget.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
+            } else {
+                widget.set_sensitive(false);
+            }
+        }
+    }
+
+}
+
+
+internal class Accounts.EditorPopover : Gtk.Popover {
+
+
+    internal Gtk.Grid layout { get; private set; default = new Gtk.Grid(); }
+
+    protected Gtk.Widget popup_focus = null;
+
+
+    public EditorPopover() {
+        get_style_context().add_class("geary-editor");
+
+        this.layout.orientation = Gtk.Orientation.VERTICAL;
+        this.layout.set_row_spacing(6);
+        this.layout.set_column_spacing(12);
+        this.layout.show();
+        add(this.layout);
+
+        this.closed.connect_after(on_closed);
+    }
+
+    ~EditorPopover() {
+        this.closed.disconnect(on_closed);
+    }
+
+    /** {@inheritdoc} */
+    public new void popup() {
+        // Work-around GTK+ issue #1138
+        Gtk.Widget target = get_relative_to();
+
+        Gtk.Allocation content_area;
+        target.get_allocation(out content_area);
+
+        Gtk.StyleContext style = target.get_style_context();
+        Gtk.StateFlags flags = style.get_state();
+        Gtk.Border margin = style.get_margin(flags);
+
+        content_area.x = margin.left;
+        content_area.y =  margin.bottom;
+        content_area.width -= (content_area.x + margin.right);
+        content_area.height -= (content_area.y + margin.top);
+
+        set_pointing_to(content_area);
+
+        base.popup();
+
+        if (this.popup_focus != null) {
+            this.popup_focus.grab_focus();
+        }
+    }
+
+    public void add_labelled_row(string label, Gtk.Widget value) {
+        Gtk.Label label_widget = new Gtk.Label(label);
+        label_widget.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
+        label_widget.halign = Gtk.Align.END;
+        label_widget.show();
+
+        this.layout.add(label_widget);
+        this.layout.attach_next_to(value, label_widget, Gtk.PositionType.RIGHT);
+    }
+
+    private void on_closed() {
+        destroy();
+    }
+
+}
+
+
+internal class PropertyCommand<T> : Application.Command {
+
+
+    private Geary.AccountInformation account;
+    private GLib.Object object;
+    private string property_name;
+    private T? new_value;
+    private T? old_value;
+
+
+    public PropertyCommand(Geary.AccountInformation account,
+                           GLib.Object object,
+                           string property_name,
+                           T? new_value,
+                           string? undo_label = null,
+                           string? redo_label = null,
+                           string? executed_label = null,
+                           string? undone_label = null) {
+        this.account = account;
+        this.object = object;
+        this.property_name = property_name;
+        this.new_value = new_value;
+
+        this.object.get(this.property_name, ref this.old_value);
+
+        this.undo_label = undo_label.printf(this.old_value);
+        this.redo_label = redo_label.printf(this.new_value);
+        this.executed_label = executed_label.printf(this.new_value);
+        this.undone_label = undone_label.printf(this.old_value);
+    }
+
+    public async override void execute(GLib.Cancellable? cancellable) {
+        this.object.set(this.property_name, this.new_value);
+        this.account.information_changed();
+    }
+
+    public async override void undo(GLib.Cancellable? cancellable) {
+        this.object.set(this.property_name, this.old_value);
+        this.account.information_changed();
+    }
 
 }
diff --git a/src/client/accounts/accounts-editor-servers-pane.vala 
b/src/client/accounts/accounts-editor-servers-pane.vala
index ebb4ec15..7a25d695 100644
--- a/src/client/accounts/accounts-editor-servers-pane.vala
+++ b/src/client/accounts/accounts-editor-servers-pane.vala
@@ -36,7 +36,6 @@ public class Accounts.EditorServersPane : Gtk.Grid {
         if (this.account.imap.mediator is GoaMediator) {
             this.details_list.add(new AccountProviderRow(this.account));
         }
-        this.details_list.add(new EmailPrefetchRow(this.account));
         this.details_list.add(new SaveDraftsRow(this.account));
 
         this.receiving_list.set_header_func(Editor.seperator_headers);
@@ -56,38 +55,7 @@ public class Accounts.EditorServersPane : Gtk.Grid {
 }
 
 
-private abstract class Accounts.ServerAccountRow<V> : LabelledEditorRow {
-
-
-    protected Geary.AccountInformation account;
-
-    protected V value;
-
-
-    public ServerAccountRow(Geary.AccountInformation account,
-                            string label,
-                            V value) {
-        base(label);
-        this.account = account;
-
-        set_dim_label(true);
-
-        this.value = value;
-
-        Gtk.Widget? widget = value as Gtk.Widget;
-        if (widget != null) {
-            widget.valign = Gtk.Align.CENTER;
-            widget.show();
-            this.layout.add(widget);
-        }
-    }
-
-    public abstract void update();
-
-}
-
-
-private class Accounts.ServiceProviderRow : ServerAccountRow<Gtk.Label> {
+private class Accounts.ServiceProviderRow : AccountRow<Gtk.Label> {
 
 
     public ServiceProviderRow(Geary.AccountInformation account) {
@@ -100,6 +68,10 @@ private class Accounts.ServiceProviderRow : ServerAccountRow<Gtk.Label> {
             new Gtk.Label("")
         );
 
+        // Can't change this, so dim it out
+        this.value.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
+        set_activatable(false);
+
         update();
     }
 
@@ -119,14 +91,12 @@ private class Accounts.ServiceProviderRow : ServerAccountRow<Gtk.Label> {
             break;
         }
         this.value.set_text(details);
-        // Can't change this, so dim it out
-        this.value.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
     }
 
 }
 
 
-private class Accounts.AccountProviderRow : ServerAccountRow<Gtk.Label> {
+private class Accounts.AccountProviderRow : AccountRow<Gtk.Label> {
 
 
     public AccountProviderRow(Geary.AccountInformation account) {
@@ -139,6 +109,10 @@ private class Accounts.AccountProviderRow : ServerAccountRow<Gtk.Label> {
             new Gtk.Label("")
         );
 
+        // Can't change this, so dim it out
+        this.value.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
+        this.set_activatable(false);
+
         update();
     }
 
@@ -150,14 +124,12 @@ private class Accounts.AccountProviderRow : ServerAccountRow<Gtk.Label> {
             source = _("Geary");
         }
         this.value.set_text(source);
-        // Can't change this, so dim it out
-        this.value.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
     }
 
 }
 
 
-private class Accounts.SaveDraftsRow : ServerAccountRow<Gtk.Switch> {
+private class Accounts.SaveDraftsRow : AccountRow<Gtk.Switch> {
 
 
     public SaveDraftsRow(Geary.AccountInformation account) {
@@ -179,89 +151,7 @@ private class Accounts.SaveDraftsRow : ServerAccountRow<Gtk.Switch> {
 }
 
 
-private class Accounts.EmailPrefetchRow : ServerAccountRow<Gtk.ComboBoxText> {
-
-
-    private static bool row_separator(Gtk.TreeModel model, Gtk.TreeIter iter) {
-        GLib.Value v;
-        model.get_value(iter, 0, out v);
-        return v.get_string() == ".";
-    }
-
-
-    public EmailPrefetchRow(Geary.AccountInformation account) {
-        Gtk.ComboBoxText combo = new Gtk.ComboBoxText();
-        combo.set_row_separator_func(row_separator);
-        combo.append("14", _("2 weeks back")); // IDs are # of days
-        combo.append("30", _("1 month back"));
-        combo.append("90", _("3 months back"));
-        combo.append("180", _("6 months back"));
-        combo.append("365", _("1 year back"));
-        combo.append("730", _("2 years back"));
-        combo.append("1461", _("4 years back"));
-        combo.append(".", "."); // Separator
-        combo.append("-1", _("Everything"));
-
-        base(
-            account,
-            // Translators: This label describes the account
-            // preference for the length of time (weeks, months or
-            // years) that past email should be downloaded.
-            _("Download mail"),
-            combo
-        );
-
-        update();
-    }
-
-    public override void update() {
-        this.value.set_active_id(this.account.prefetch_period_days.to_string());
-    }
-
-}
-
-
-private abstract class Accounts.ServerServiceRow<V> : ServerAccountRow<V> {
-
-
-    protected Geary.ServiceInformation service;
-
-    public virtual bool is_value_editable {
-        get {
-            return (
-                this.account.service_provider == Geary.ServiceProvider.OTHER &&
-                !this.is_goa_account
-            );
-        }
-    }
-
-    // XXX convenience method until we get a better way of doing this.
-    protected bool is_goa_account {
-        get { return (this.service.mediator is GoaMediator); }
-    }
-
-
-    public ServerServiceRow(Geary.AccountInformation account,
-                            Geary.ServiceInformation service,
-                            string label,
-                            V value) {
-        base(account, label, value);
-        this.service = service;
-
-        Gtk.Widget? widget = value as Gtk.Widget;
-        if (widget != null && !this.is_value_editable) {
-            if (widget is Gtk.Label) {
-                widget.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
-            } else {
-                widget.set_sensitive(false);
-            }
-        }
-    }
-
-}
-
-
-private class Accounts.ServiceHostRow : ServerServiceRow<Gtk.Label> {
+private class Accounts.ServiceHostRow : ServiceRow<Gtk.Label> {
 
     public ServiceHostRow(Geary.AccountInformation account,
                           Geary.ServiceInformation service) {
@@ -301,8 +191,7 @@ private class Accounts.ServiceHostRow : ServerServiceRow<Gtk.Label> {
 }
 
 
-private class Accounts.ServiceSecurityRow :
-    ServerServiceRow<Gtk.ComboBoxText> {
+private class Accounts.ServiceSecurityRow : ServiceRow<Gtk.ComboBoxText> {
 
     private const string INSECURE_ICON = "channel-insecure-symbolic";
     private const string SECURE_ICON = "channel-secure-symbolic";
@@ -357,7 +246,7 @@ private class Accounts.ServiceSecurityRow :
 }
 
 
-private class Accounts.ServiceAuthRow : ServerServiceRow<Gtk.Label> {
+private class Accounts.ServiceAuthRow : ServiceRow<Gtk.Label> {
 
     public ServiceAuthRow(Geary.AccountInformation account,
                           Geary.ServiceInformation service) {
diff --git a/src/client/accounts/accounts-editor.vala b/src/client/accounts/accounts-editor.vala
index b4a5efe4..48102c6b 100644
--- a/src/client/accounts/accounts-editor.vala
+++ b/src/client/accounts/accounts-editor.vala
@@ -12,6 +12,12 @@
 public class Accounts.Editor : Gtk.Dialog {
 
 
+    private const ActionEntry[] ACTION_ENTRIES = {
+        { GearyController.ACTION_REDO, on_redo },
+        { GearyController.ACTION_UNDO, on_undo },
+    };
+
+
     internal static void seperator_headers(Gtk.ListBoxRow row,
                                            Gtk.ListBoxRow? first) {
         if (first == null) {
@@ -22,8 +28,8 @@ public class Accounts.Editor : Gtk.Dialog {
     }
 
     private static int ordinal_sort(Gtk.ListBoxRow a, Gtk.ListBoxRow b) {
-        AccountRow? account_a = a as AccountRow;
-        AccountRow? account_b = b as AccountRow;
+        AccountListRow? account_a = a as AccountListRow;
+        AccountListRow? account_b = b as AccountListRow;
 
         if (account_a == null) {
             return (account_b == null) ? 0 : 1;
@@ -37,17 +43,20 @@ public class Accounts.Editor : Gtk.Dialog {
     }
 
 
-    /**
-     * The current application instance.
-     *
-     * Note this hides the {@link GtkWindow.application} property
-     * since we don't want the application to know about this dialog -
-     * it should not prevent the app from closing.
-     */
-    internal new GearyApplication application { get; private set; }
+    /** The command stack for this pane. */
+    internal Application.CommandStack commands {
+        get; private set; default = new Application.CommandStack();
+    }
+
+    /** The current account being edited, if any. */
+    private Geary.AccountInformation selected_account {
+        get; private set; default = null;
+    }
 
     private AccountManager accounts;
 
+    private SimpleActionGroup actions = new SimpleActionGroup();
+
     [GtkChild]
     private Gtk.HeaderBar default_header;
 
@@ -57,6 +66,9 @@ public class Accounts.Editor : Gtk.Dialog {
     [GtkChild]
     private Gtk.Button back_button;
 
+    [GtkChild]
+    private Gtk.Button undo_button;
+
     [GtkChild]
     private Gtk.Grid list_pane;
 
@@ -67,14 +79,17 @@ public class Accounts.Editor : Gtk.Dialog {
         new Gee.LinkedList<Gtk.Widget>();
 
 
-
     public Editor(GearyApplication application, Gtk.Window parent) {
         this.application = application;
         this.accounts = application.controller.account_manager;
 
+        this.actions.add_action_entries(ACTION_ENTRIES, this);
+        insert_action_group("win", this.actions);
+
         set_titlebar(this.default_header);
         set_transient_for(parent);
-        set_modal(true);
+        //set_modal(true);
+        set_modal(false);
 
         // XXX Glade 3.22 won't let us set this
         get_content_area().border_width = 2;
@@ -90,12 +105,23 @@ public class Accounts.Editor : Gtk.Dialog {
 
         this.accounts_list.add(new AddRow());
 
-        accounts.account_added.connect(on_account_added);
-        accounts.account_status_changed.connect(on_account_status_changed);
-        accounts.account_removed.connect(on_account_removed);
+        this.accounts.account_added.connect(on_account_added);
+        this.accounts.account_status_changed.connect(on_account_status_changed);
+        this.accounts.account_removed.connect(on_account_removed);
+
+        this.commands.executed.connect(on_command);
+        this.commands.undone.connect(on_command);
+        this.commands.redone.connect(on_command);
+
+        get_action(GearyController.ACTION_UNDO).set_enabled(false);
+        get_action(GearyController.ACTION_REDO).set_enabled(false);
     }
 
     ~Editor() {
+        this.commands.executed.disconnect(on_command);
+        this.commands.undone.disconnect(on_command);
+        this.commands.redone.disconnect(on_command);
+
         this.accounts.account_added.disconnect(on_account_added);
         this.accounts.account_status_changed.disconnect(on_account_status_changed);
         this.accounts.account_removed.disconnect(on_account_removed);
@@ -117,10 +143,11 @@ public class Accounts.Editor : Gtk.Dialog {
         this.editor_panes.add(child);
         this.editor_panes.set_visible_child(child);
         this.back_button.show();
+        this.undo_button.show();
     }
 
     internal void pop() {
-        // We can't simply remove old panes fro the GTK stack since
+        // One can't simply remove old panes fro the GTK stack since
         // there won't be any transition between them - the old one
         // will simply disappear. So we need to keep old, popped panes
         // around until a new one is pushed on.
@@ -129,27 +156,35 @@ public class Accounts.Editor : Gtk.Dialog {
         // them?
         Gtk.Widget current = this.editor_panes.get_visible_child();
         int next = this.editor_pane_stack.index_of(current) - 1;
-        
+
         this.editor_panes.set_visible_child(this.editor_pane_stack.get(next));
 
+        // Don't carry commands over from one pane to another
+        this.commands.clear();
+        get_action(GearyController.ACTION_UNDO).set_enabled(false);
+        get_action(GearyController.ACTION_REDO).set_enabled(false);
+
         if (next == 0) {
+            this.selected_account = null;
             this.back_button.hide();
+            this.undo_button.hide();
         }
     }
 
     private void add_account(Geary.AccountInformation account,
                              AccountManager.Status status) {
-        this.accounts_list.add(new AccountRow(account, status));
+        this.accounts_list.add(new AccountListRow(account, status));
     }
 
     private void show_account(Geary.AccountInformation account) {
+        this.selected_account = account;
         push(new EditorEditPane(this, account));
     }
 
-    private AccountRow? get_account_row(Geary.AccountInformation account) {
-        AccountRow? row = null;
+    private AccountListRow? get_account_row(Geary.AccountInformation account) {
+        AccountListRow? row = null;
         this.accounts_list.foreach((child) => {
-                AccountRow? account_row = child as AccountRow;
+                AccountListRow? account_row = child as AccountListRow;
                 if (account_row != null && account_row.account == account) {
                     row = account_row;
                 }
@@ -157,6 +192,10 @@ public class Accounts.Editor : Gtk.Dialog {
         return row;
     }
 
+    private inline GLib.SimpleAction get_action(string name) {
+        return (GLib.SimpleAction) this.actions.lookup_action(name);
+    }
+
     private void on_account_added(Geary.AccountInformation account,
                                   AccountManager.Status status) {
         add_account(account, status);
@@ -164,22 +203,51 @@ public class Accounts.Editor : Gtk.Dialog {
 
     private void on_account_status_changed(Geary.AccountInformation account,
                                            AccountManager.Status status) {
-        AccountRow? row = get_account_row(account);
+        AccountListRow? row = get_account_row(account);
         if (row != null) {
             row.update(status);
         }
     }
 
     private void on_account_removed(Geary.AccountInformation account) {
-        AccountRow? row = get_account_row(account);
+        AccountListRow? row = get_account_row(account);
         if (row != null) {
             this.accounts_list.remove(row);
         }
+
+        if (this.selected_account == account) {
+            while (this.editor_panes.get_visible_child() != this.list_pane) {
+                pop();
+            }
+        }
+    }
+
+    private void on_undo() {
+        this.commands.undo.begin(null);
+    }
+
+    private void on_redo() {
+        this.commands.redo.begin(null);
+    }
+
+    private void on_command() {
+        get_action(GearyController.ACTION_UNDO).set_enabled(
+            this.commands.can_undo
+        );
+        get_action(GearyController.ACTION_REDO).set_enabled(
+            this.commands.can_redo
+        );
+
+        Application.Command next_undo = this.commands.peek_undo();
+        this.undo_button.set_tooltip_text(
+            (next_undo != null && next_undo.undo_label != null)
+            ? next_undo.undo_label : ""
+        );
     }
 
     [GtkCallback]
     private void on_accounts_list_row_activated(Gtk.ListBoxRow activated) {
-        AccountRow? row = activated as AccountRow;
+        AccountListRow? row = activated as AccountListRow;
         if (row != null) {
             show_account(row.account);
         }
@@ -192,7 +260,7 @@ public class Accounts.Editor : Gtk.Dialog {
 
 }
 
-private class Accounts.AccountRow : EditorRow {
+private class Accounts.AccountListRow : EditorRow {
 
 
     internal Geary.AccountInformation account;
@@ -204,8 +272,8 @@ private class Accounts.AccountRow : EditorRow {
     private Gtk.Label account_details = new Gtk.Label("");
 
 
-    public AccountRow(Geary.AccountInformation account,
-                      AccountManager.Status status) {
+    public AccountListRow(Geary.AccountInformation account,
+                          AccountManager.Status status) {
         this.account = account;
 
         this.account_name.show();
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 6bfeaab9..ea91ead0 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -32,6 +32,7 @@ public class GearyController : Geary.BaseObject {
     public const string ACTION_EMPTY_SPAM = "empty-spam";
     public const string ACTION_EMPTY_TRASH = "empty-trash";
     public const string ACTION_UNDO = "undo";
+    public const string ACTION_REDO = "redo";
     public const string ACTION_FIND_IN_CONVERSATION = "conv-find";
     public const string ACTION_ZOOM = "zoom";
     public const string ACTION_SHOW_MARK_MENU = "mark-message-menu";
@@ -566,6 +567,7 @@ public class GearyController : Geary.BaseObject {
         add_window_accelerators(ACTION_TRASH_CONVERSATION, { "Delete", "BackSpace" });
         add_window_accelerators(ACTION_DELETE_CONVERSATION, { "<Shift>Delete", "<Shift>BackSpace" });
         add_window_accelerators(ACTION_UNDO, { "<Ctrl>Z" });
+        add_window_accelerators(ACTION_REDO, { "<Ctrl><Shift>Z" });
         add_window_accelerators(ACTION_ZOOM+("('in')"), { "<Ctrl>equal", "equal" });
         add_window_accelerators(ACTION_ZOOM+("('out')"), { "<Ctrl>minus", "minus" });
         add_window_accelerators(ACTION_ZOOM+("('normal')"), { "<Ctrl>0", "0" });
diff --git a/src/engine/api/geary-account-information.vala b/src/engine/api/geary-account-information.vala
index 7efab6d7..50e6a9ff 100644
--- a/src/engine/api/geary-account-information.vala
+++ b/src/engine/api/geary-account-information.vala
@@ -267,6 +267,81 @@ public class Geary.AccountInformation : BaseObject {
         this.data_dir = data;
     }
 
+    /**
+     * Return a read only, ordered list of the account's sender mailboxes.
+     */
+    public Gee.List<Geary.RFC822.MailboxAddress> get_sender_mailboxes() {
+        Gee.List<RFC822.MailboxAddress> all =
+            new Gee.LinkedList<RFC822.MailboxAddress>();
+
+        all.add(this.primary_mailbox);
+        if (alternate_mailboxes != null) {
+            all.add_all(alternate_mailboxes);
+        }
+
+        return all.read_only_view;
+    }
+
+    /**
+     * Appends a sender mailbox to the account.
+     */
+    public void append_sender_mailbox(Geary.RFC822.MailboxAddress mailbox) {
+        if (this.primary_mailbox == null) {
+            this.primary_mailbox = mailbox;
+        } else {
+            if (this.alternate_mailboxes == null) {
+                this.alternate_mailboxes =
+                    new Gee.LinkedList<Geary.RFC822.MailboxAddress>();
+            }
+            this.alternate_mailboxes.add(mailbox);
+        }
+    }
+
+    /**
+     * Appends a sender mailbox to the account.
+     */
+    public void insert_sender_mailbox(int index,
+                                      Geary.RFC822.MailboxAddress mailbox) {
+        Geary.RFC822.MailboxAddress? alt_insertion = null;
+        int actual_index = index;
+        if (actual_index == 0) {
+            if (this.primary_mailbox == null) {
+                this.primary_mailbox = mailbox;
+            } else {
+                this.primary_mailbox = mailbox;
+                alt_insertion = this.primary_mailbox;
+                actual_index = 0;
+            }
+        } else {
+            alt_insertion = mailbox;
+            actual_index--;
+        }
+
+        if (alt_insertion != null) {
+            if (this.alternate_mailboxes == null) {
+                this.alternate_mailboxes =
+                    new Gee.LinkedList<Geary.RFC822.MailboxAddress>();
+            }
+            this.alternate_mailboxes.insert(actual_index, alt_insertion);
+        }
+    }
+
+    /**
+     * Removes a sender mailbox for the account.
+     */
+    public void remove_sender_mailbox(Geary.RFC822.MailboxAddress mailbox) {
+        if (this.primary_mailbox == mailbox) {
+            this.primary_mailbox = (
+                this.alternate_mailboxes != null &&
+                !this.alternate_mailboxes.is_empty
+            ) ? this.alternate_mailboxes.remove_at(0) : null;
+        } else if (this.alternate_mailboxes != null) {
+            this.alternate_mailboxes.remove_at(
+                this.alternate_mailboxes.index_of(mailbox)
+            );
+        }
+    }
+
     /**
      * Return a list of the primary and all alternate email addresses.
      */
diff --git a/ui/accounts_editor.ui b/ui/accounts_editor.ui
index e15b5fd9..f957839e 100644
--- a/ui/accounts_editor.ui
+++ b/ui/accounts_editor.ui
@@ -101,14 +101,14 @@
             </child>
           </object>
           <packing>
-            <property name="expand">True</property>
+            <property name="expand">False</property>
             <property name="fill">True</property>
             <property name="position">1</property>
           </packing>
         </child>
       </object>
     </child>
-    <child>
+    <child type="titlebar">
       <placeholder/>
     </child>
     <style>
@@ -146,5 +146,34 @@
         </child>
       </object>
     </child>
+    <child>
+      <object class="GtkGrid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <child>
+          <object class="GtkButton" id="undo_button">
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="action_name">win.undo</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="no_show_all">True</property>
+                <property name="icon_name">edit-undo-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="pack_type">end</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
   </object>
 </interface>
diff --git a/ui/accounts_editor_edit_pane.ui b/ui/accounts_editor_edit_pane.ui
index 0b2e817d..e38c0fa0 100644
--- a/ui/accounts_editor_edit_pane.ui
+++ b/ui/accounts_editor_edit_pane.ui
@@ -3,7 +3,6 @@
 <interface>
   <requires lib="gtk+" version="3.20"/>
   <template class="AccountsEditorEditPane" parent="GtkGrid">
-    <property name="name">1</property>
     <property name="visible">True</property>
     <property name="can_focus">False</property>
     <child>
@@ -34,6 +33,7 @@
                         <property name="visible">True</property>
                         <property name="can_focus">False</property>
                         <property name="selection_mode">none</property>
+                        <signal name="row-activated" handler="on_setting_activated" swapped="no"/>
                       </object>
                     </child>
                     <child type="label_item">
@@ -68,10 +68,11 @@
                     <property name="label_xalign">0</property>
                     <property name="shadow_type">in</property>
                     <child>
-                      <object class="GtkListBox" id="addresses_list">
+                      <object class="GtkListBox" id="senders_list">
                         <property name="visible">True</property>
                         <property name="can_focus">False</property>
                         <property name="selection_mode">none</property>
+                        <signal name="row-activated" handler="on_setting_activated" swapped="no"/>
                       </object>
                     </child>
                     <child type="label_item">
@@ -98,6 +99,43 @@
                     <property name="top_attach">3</property>
                   </packing>
                 </child>
+                <child>
+                  <object class="GtkFrame" id="signature_frame">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label_xalign">0</property>
+                    <property name="shadow_type">in</property>
+                    <child>
+                      <placeholder/>
+                    </child>
+                    <child type="label_item">
+                      <placeholder/>
+                    </child>
+                    <style>
+                      <class name="geary-settings"/>
+                      <class name="geary-signature"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">4</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                    <property name="label" translatable="yes">Settings</property>
+                    <style>
+                      <class name="geary-settings-heading"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">5</property>
+                  </packing>
+                </child>
                 <child>
                   <object class="GtkFrame">
                     <property name="visible">True</property>
@@ -110,6 +148,7 @@
                         <property name="visible">True</property>
                         <property name="can_focus">False</property>
                         <property name="selection_mode">none</property>
+                        <signal name="row-activated" handler="on_setting_activated" swapped="no"/>
                       </object>
                     </child>
                     <child type="label_item">
@@ -118,7 +157,7 @@
                   </object>
                   <packing>
                     <property name="left_attach">0</property>
-                    <property name="top_attach">5</property>
+                    <property name="top_attach">6</property>
                   </packing>
                 </child>
                 <child>
@@ -167,35 +206,7 @@
                   </object>
                   <packing>
                     <property name="left_attach">0</property>
-                    <property name="top_attach">6</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkFrame">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="label_xalign">0</property>
-                    <property name="shadow_type">in</property>
-                    <child>
-                      <object class="GtkScrolledWindow" id="signature_scrolled">
-                        <property name="height_request">80</property>
-                        <property name="visible">True</property>
-                        <property name="can_focus">True</property>
-                        <property name="vexpand">True</property>
-                        <property name="vscrollbar_policy">never</property>
-                        <property name="min_content_height">40</property>
-                        <child>
-                          <placeholder/>
-                        </child>
-                      </object>
-                    </child>
-                    <child type="label_item">
-                      <placeholder/>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="left_attach">0</property>
-                    <property name="top_attach">4</property>
+                    <property name="top_attach">7</property>
                   </packing>
                 </child>
                 <style>
diff --git a/ui/client-web-view.js b/ui/client-web-view.js
index c2a9d24a..76d4456e 100644
--- a/ui/client-web-view.js
+++ b/ui/client-web-view.js
@@ -172,3 +172,5 @@ PageState.prototype = {
         }
     }
 };
+
+var geary = new PageState();
diff --git a/ui/geary.css b/ui/geary.css
index 359be70b..2055b4e4 100644
--- a/ui/geary.css
+++ b/ui/geary.css
@@ -230,6 +230,11 @@ row.geary-settings > grid > image:dir(rtl) {
   margin-left: 6px;
 }
 
+frame.geary-settings.geary-signature {
+  min-height: 5em;
+}
+
+
 row.geary-settings > grid > combobox,
 row.geary-settings:not(.geary-add-row) > grid > image,
 row.geary-settings > grid > switch {
@@ -242,3 +247,11 @@ row.geary-settings > grid > switch {
 buttonbox.geary-settings  {
   margin-top: 36px;
 }
+
+popover.geary-editor > grid {
+  margin: 6px;
+}
+
+popover.geary-editor > grid > button.geary-setting-remove {
+  margin-top: 6px;
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]