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.
This commit is contained in:
Michael James Gratton 2016-09-23 23:31:01 +10:00
parent ad1fc5bafc
commit 6b58da6b99
3 changed files with 123 additions and 95 deletions

View file

@ -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,

View file

@ -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);
Geary.EmailIdentifier? editing_draft_id = null;
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);
}
// 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,23 +2350,10 @@ public class ComposerWidget : Gtk.EventBox {
return false;
this.account = new_account;
set_entry_completions();
load_entry_completions.begin();
return true;
}
private void set_entry_completions() {
if (this.contact_list_store != null
&& this.contact_list_store.contact_store == account.get_contact_store())
return;
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);
}
}

View file

@ -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))