[geary] Fix delay showing composer for accounts with large numbers of messages.



commit 6b58da6b99a2acd1f12c7ee42b4a841374e4c447
Author: Michael James Gratton <mike vee net>
Date:   Fri Sep 23 23:31:01 2016 +1000

    Fix delay showing composer for accounts with large numbers of messages.
    
    * src/client/application/geary-controller.vala
      (GearyController::create_compose_widget_async): Load draft manager and
      email entry completion model asynchronously after the UI has been made
      visible.
    
    * src/client/composer/composer-widget.vala (ComposerWidget): Rename
      set_entry_completions to load_entry_completions, make loading
      async. Make open_draft_manager_async and load_entry_completions public
      so they can be invoked by the controller.
    
    * src/client/composer/contact-list-store.vala (ContactListStore): Load
      contacts asynchronously in smaller batches, so the UI
      remains responsive.

 src/client/application/geary-controller.vala |   13 +++
 src/client/composer/composer-widget.vala     |  138 +++++++++++++-------------
 src/client/composer/contact-list-store.vala  |   67 ++++++++-----
 3 files changed, 123 insertions(+), 95 deletions(-)
---
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 34e6045..a88d1bb 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -2258,6 +2258,19 @@ public class GearyController : Geary.BaseObject {
             new ComposerWindow(widget);
             widget.state = ComposerWidget.ComposerState.DETACHED;
         }
+
+        if (is_draft) {
+            try {
+                yield widget.open_draft_manager_async(referred.id);
+            } catch (Error e) {
+                message("Could not open draft manager: %s", e.message);
+            }
+        }
+
+        // For accounts with large numbers of contacts, loading the
+        // entry completions can some time, so do it after the UI has
+        // been shown
+        yield widget.load_entry_completions();
     }
     
     private bool should_create_new_composer(ComposerWidget.ComposeType? compose_type,
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index ecc2981..4471cdc 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -429,9 +429,6 @@ public class ComposerWidget : Gtk.EventBox {
         this.plain_menu = (Menu) builder.get_object("plain_menu_model");
         this.context_menu_model = (Menu) builder.get_object("context_menu_model");
 
-        // TODO: It would be nicer to set the completions inside the EmailEntry constructor. But in
-        // testing, this can cause non-deterministic segfaults. Investigate why, and fix if possible.
-        set_entry_completions();
         this.subject_entry.bind_property("text", this, "window-title", BindingFlags.SYNC_CREATE,
             (binding, source_value, ref target_value) => {
                 target_value = Geary.String.is_empty_or_whitespace(this.subject_entry.text)
@@ -457,9 +454,8 @@ public class ComposerWidget : Gtk.EventBox {
 
         this.from = new Geary.RFC822.MailboxAddresses.single(account.information.primary_mailbox);
 
-        Geary.EmailIdentifier? editing_draft_id = null;
         if (referred != null)
-            editing_draft_id = fill_in_from_referred(referred, quote, is_referred_draft);
+            fill_in_from_referred(referred, quote);
 
         update_from_field();
 
@@ -500,8 +496,6 @@ public class ComposerWidget : Gtk.EventBox {
         this.editor.navigation_policy_decision_requested.connect(on_navigation_policy_decision_requested);
         this.editor.new_window_policy_decision_requested.connect(on_navigation_policy_decision_requested);
 
-        open_draft_manager_async.begin(editing_draft_id);
-
         GearyApplication.instance.config.settings.changed[Configuration.SPELL_CHECK_KEY].connect(
             on_spell_check_changed);
 
@@ -599,10 +593,37 @@ public class ComposerWidget : Gtk.EventBox {
         update_actions();
     }
 
+    /**
+     * Loads and sets contact auto-complete data for the current account.
+     */
+    public async void load_entry_completions() {
+        // XXX Since ContactListStore hooks into ContactStore to
+        // listen for contacts being added and removed,
+        // GearyController or some composer-related controller should
+        // construct an instance per account and keep it around for
+        // the lifetime of the app, since there can be tens of
+        // thousands of contacts for large accounts.
+        Geary.ContactStore contacts = this.account.get_contact_store();
+        if (this.contact_list_store == null ||
+            this.contact_list_store.contact_store != contacts) {
+            ContactListStore store = new ContactListStore(contacts);
+            this.contact_list_store = store;
+            yield store.load();
+
+            this.to_entry.completion = new ContactEntryCompletion(store);
+            this.cc_entry.completion = new ContactEntryCompletion(store);
+            this.bcc_entry.completion = new ContactEntryCompletion(store);
+            this.reply_to_entry.completion = new ContactEntryCompletion(store);
+        }
+    }
+
+    /**
+     * Restores the composer's widget state from its draft.
+     */
     public async void restore_draft_state_async(Geary.Account account) {
         bool first_email = true;
         
-        foreach (Geary.RFC822.MessageID mid in in_reply_to) {
+        foreach (Geary.RFC822.MessageID mid in this.in_reply_to) {
             Gee.MultiMap<Geary.Email, Geary.FolderPath?>? email_map;
             try {
                 email_map =
@@ -664,12 +685,46 @@ public class ComposerWidget : Gtk.EventBox {
         }
     }
 
-    // Copies the addresses (e.g. From/To/CC) and content from referred into this one
-    private Geary.EmailIdentifier? fill_in_from_referred(Geary.Email referred, string? quote, bool 
is_referred_draft) {
-        if (referred == null)
-            return null;
+    /**
+     * Creates and opens the composer's draft manager.
+     */
+    public async void open_draft_manager_async(
+        Geary.EmailIdentifier? editing_draft_id = null,
+        Cancellable? cancellable = null)
+    throws Error {
+        this.draft_save_text = "";
+        yield close_draft_manager_async(cancellable);
+
+        SimpleAction close_and_save = get_action(ACTION_CLOSE_AND_SAVE);
+        if (!this.account.information.save_drafts) {
+            this.header.save_and_close_button.hide();
+            return;
+        }
+
+        this.draft_manager = new Geary.App.DraftManager(account);
+        try {
+            yield this.draft_manager.open_async(editing_draft_id, cancellable);
+        } catch (Error err) {
+            debug("Unable to open draft manager %s: %s",
+                  this.draft_manager.to_string(), err.message);
+
+            this.draft_manager = null;
+
+            throw err;
+        }
+
+        close_and_save.set_enabled(true);
+        this.header.save_and_close_button.show();
+
+        this.draft_manager.notify[Geary.App.DraftManager.PROP_DRAFT_STATE]
+            .connect(on_draft_state_changed);
+        this.draft_manager.notify[Geary.App.DraftManager.PROP_CURRENT_DRAFT_ID]
+            .connect(on_draft_id_changed);
+        this.draft_manager.fatal.connect(on_draft_manager_fatal);
+    }
 
-        Geary.EmailIdentifier? editing_draft_id = null;
+    // Copies the addresses (e.g. From/To/CC) and content from referred into this one
+    private void fill_in_from_referred(Geary.Email referred, string? quote) {
         if (this.compose_type != ComposeType.NEW_MESSAGE) {
             add_recipients_and_ids(this.compose_type, referred);
             this.reply_subject = Geary.RFC822.Utils.create_subject_for_reply(referred);
@@ -703,9 +758,6 @@ public class ComposerWidget : Gtk.EventBox {
                     debug("Error getting message body: %s", error.message);
                 }
 
-                if (is_referred_draft)
-                    editing_draft_id = referred.id;
-
                 add_attachments(referred.attachments);
             break;
 
@@ -730,7 +782,6 @@ public class ComposerWidget : Gtk.EventBox {
                 this.pending_attachments = referred.attachments;
             break;
         }
-        return editing_draft_id;
     }
 
     public void set_focus() {
@@ -1332,42 +1383,6 @@ public class ComposerWidget : Gtk.EventBox {
         this.draft_save_text = DRAFT_ERROR_TEXT;
     }
 
-    // Returns the drafts folder for the current From account.
-    private async void open_draft_manager_async(
-        Geary.EmailIdentifier? editing_draft_id = null,
-        Cancellable? cancellable = null)
-    throws Error {
-        this.draft_save_text = "";
-        yield close_draft_manager_async(cancellable);
-
-        SimpleAction close_and_save = get_action(ACTION_CLOSE_AND_SAVE);
-        if (!this.account.information.save_drafts) {
-            this.header.save_and_close_button.hide();
-            return;
-        }
-
-        this.draft_manager = new Geary.App.DraftManager(account);
-        try {
-            yield this.draft_manager.open_async(editing_draft_id, cancellable);
-        } catch (Error err) {
-            debug("Unable to open draft manager %s: %s",
-                  this.draft_manager.to_string(), err.message);
-
-            this.draft_manager = null;
-
-            throw err;
-        }
-
-        close_and_save.set_enabled(true);
-        this.header.save_and_close_button.show();
-
-        this.draft_manager.notify[Geary.App.DraftManager.PROP_DRAFT_STATE]
-            .connect(on_draft_state_changed);
-        this.draft_manager.notify[Geary.App.DraftManager.PROP_CURRENT_DRAFT_ID]
-            .connect(on_draft_id_changed);
-        this.draft_manager.fatal.connect(on_draft_manager_fatal);
-    }
-
     private async void close_draft_manager_async(Cancellable? cancellable) throws Error {
         get_action(ACTION_CLOSE_AND_SAVE).set_enabled(false);
         if (this.draft_manager == null)
@@ -2335,22 +2350,9 @@ public class ComposerWidget : Gtk.EventBox {
             return false;
 
         this.account = new_account;
-        set_entry_completions();
-        
-        return true;
-    }
-
-    private void set_entry_completions() {
-        if (this.contact_list_store != null
-            && this.contact_list_store.contact_store == account.get_contact_store())
-            return;
+        load_entry_completions.begin();
 
-        this.contact_list_store = new ContactListStore(account.get_contact_store());
-
-        this.to_entry.completion = new ContactEntryCompletion(this.contact_list_store);
-        this.cc_entry.completion = new ContactEntryCompletion(this.contact_list_store);
-        this.bcc_entry.completion = new ContactEntryCompletion(this.contact_list_store);
-        this.reply_to_entry.completion = new ContactEntryCompletion(this.contact_list_store);
+        return true;
     }
 
 }
diff --git a/src/client/composer/contact-list-store.vala b/src/client/composer/contact-list-store.vala
index 50b5f42..1bc30ea 100644
--- a/src/client/composer/contact-list-store.vala
+++ b/src/client/composer/contact-list-store.vala
@@ -5,9 +5,13 @@
  */
 
 public class ContactListStore : Gtk.ListStore {
+
     // Minimum visibility for the contact to appear in autocompletion.
     private const Geary.ContactImportance CONTACT_VISIBILITY_THRESHOLD = Geary.ContactImportance.TO_TO;
-    
+
+    // Batch size for loading contacts asynchronously
+    private uint LOAD_BATCH_SIZE = 4096;
+
     public enum Column {
         CONTACT_OBJECT,
         CONTACT_MARKUP_NAME,
@@ -26,25 +30,35 @@ public class ContactListStore : Gtk.ListStore {
     
     public ContactListStore(Geary.ContactStore contact_store) {
         set_column_types(Column.get_types());
-        
         this.contact_store = contact_store;
-        
-        foreach (Geary.Contact contact in contact_store.contacts)
-            add_contact(contact);
-        
-        // set sort function *after* adding all the contacts
-        set_sort_func(Column.CONTACT_OBJECT, sort_func);
-        set_sort_column_id(Column.CONTACT_OBJECT, Gtk.SortType.ASCENDING);
-        
         contact_store.contact_added.connect(on_contact_added);
         contact_store.contact_updated.connect(on_contact_updated);
     }
-    
+
     ~ContactListStore() {
-        contact_store.contact_added.disconnect(on_contact_added);
-        contact_store.contact_updated.disconnect(on_contact_updated);
+        this.contact_store.contact_added.disconnect(on_contact_added);
+        this.contact_store.contact_updated.disconnect(on_contact_updated);
     }
-    
+
+    /**
+     * Loads contacts from the model's contact store.
+     */
+    public async void load() {
+        uint count = 0;
+        foreach (Geary.Contact contact in this.contact_store.contacts) {
+            add_contact(contact);
+            count++;
+            if (count % LOAD_BATCH_SIZE == 0) {
+                Idle.add(load.callback);
+                yield;
+            }
+        }
+
+        // set sort function *after* adding all the contacts
+        set_sort_func(Column.CONTACT_OBJECT, sort_func);
+        set_sort_column_id(Column.CONTACT_OBJECT, Gtk.SortType.ASCENDING);
+    }
+
     public Geary.Contact get_contact(Gtk.TreeIter iter) {
         GLib.Value contact_value;
         get_value(iter, Column.CONTACT_OBJECT, out contact_value);
@@ -73,20 +87,19 @@ public class ContactListStore : Gtk.ListStore {
             set(iter, Column.CONTACT_MARKUP_NAME, highlighted_result, -1);
         }
     }
-    
-    private void add_contact(Geary.Contact contact) {
-        if (contact.highest_importance < CONTACT_VISIBILITY_THRESHOLD)
-            return;
-        
-        string full_address = contact.get_rfc822_address().to_rfc822_string();
-        Gtk.TreeIter iter;
-        append(out iter);
-        set(iter,
-            Column.CONTACT_OBJECT, contact,
-            Column.CONTACT_MARKUP_NAME, Markup.escape_text(full_address),
-            Column.PRIOR_KEYS, new Gee.HashSet<string>());
+
+    private inline void add_contact(Geary.Contact contact) {
+        if (contact.highest_importance >= CONTACT_VISIBILITY_THRESHOLD) {
+            string full_address = contact.get_rfc822_address().to_rfc822_string();
+            Gtk.TreeIter iter;
+            append(out iter);
+            set(iter,
+                Column.CONTACT_OBJECT, contact,
+                Column.CONTACT_MARKUP_NAME, Markup.escape_text(full_address),
+                Column.PRIOR_KEYS, new Gee.HashSet<string>());
+        }
     }
-    
+
     private void update_contact(Geary.Contact updated_contact) {
         Gtk.TreeIter iter;
         if (!get_iter_first(out iter))


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