diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7c825e41..f2bc0d45 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -45,6 +45,7 @@ engine/api/geary-folder-supports-remove.vala engine/api/geary-logging.vala engine/api/geary-named-flag.vala engine/api/geary-named-flags.vala +engine/api/geary-progress-monitor.vala engine/api/geary-search-folder.vala engine/api/geary-service-provider.vala engine/api/geary-special-folder-type.vala @@ -267,6 +268,7 @@ client/ui/folder-menu.vala client/ui/icon-factory.vala client/ui/main-toolbar.vala client/ui/main-window.vala +client/ui/monitored-progress-bar.vala client/util/util-date.vala client/util/util-email.vala diff --git a/src/client/geary-application.vala b/src/client/geary-application.vala index b1497580..f53a21e1 100644 --- a/src/client/geary-application.vala +++ b/src/client/geary-application.vala @@ -77,6 +77,12 @@ along with Geary; if not, write to the Free Software Foundation, Inc., public Configuration config { get; private set; } + /** + * Fired when the current account in the UI has changed. + * TODO: This signal really belongs in the controller. See #7032 for the refactoring ticket. + */ + public signal void current_account_changed(Geary.Account? account); + private static GearyApplication _instance = null; private GearyController? controller = null; @@ -484,5 +490,9 @@ along with Geary; if not, write to the Free Software Foundation, Inc., return 0; // on error } + + public void notify_current_account_changed(Geary.Account? account) { + current_account_changed(account); + } } diff --git a/src/client/geary-controller.vala b/src/client/geary-controller.vala index d5402ac6..9c8ded54 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -327,8 +327,6 @@ public class GearyController { account.email_sent.connect(on_sent); main_window.folder_list.set_user_folders_root_name(account, _("Labels")); - - update_search_placeholder_text(); } public async void disconnect_account_async(Geary.Account account, Cancellable? cancellable = null) { @@ -373,8 +371,6 @@ public class GearyController { } catch (Error e) { message("Error enumerating accounts: %s", e.message); } - - update_search_placeholder_text(); } // Returns the number of open accounts. @@ -398,7 +394,6 @@ public class GearyController { // by other utility methods private void update_ui() { update_tooltips(); - update_search_placeholder_text(); Gtk.Action delete_message = GearyApplication.instance.actions.get_action(ACTION_DELETE_MESSAGE); if (current_folder is Geary.FolderSupport.Archive) { delete_message.label = ARCHIVE_MESSAGE_LABEL; @@ -473,7 +468,10 @@ public class GearyController { debug("switching to %s", folder.to_string()); current_folder = folder; - current_account = folder.account; + if (current_account != folder.account) { + current_account = folder.account; + GearyApplication.instance.notify_current_account_changed(current_account); + } if (!(current_folder is Geary.SearchFolder)) previous_non_search_folder = current_folder; @@ -1492,11 +1490,5 @@ public class GearyController { main_window.folder_list.set_search(folder); } - - private void update_search_placeholder_text() { - main_window.main_toolbar.set_search_placeholder_text( - current_account == null || GearyApplication.instance.get_num_accounts() == 1 ? - _("Search") : _("Search %s account").printf(current_account.information.nickname)); - } } diff --git a/src/client/ui/main-toolbar.vala b/src/client/ui/main-toolbar.vala index 22b14587..b0161c04 100644 --- a/src/client/ui/main-toolbar.vala +++ b/src/client/ui/main-toolbar.vala @@ -7,6 +7,7 @@ // Draws the main toolbar. public class MainToolbar : Gtk.Box { private const string ICON_CLEAR_NAME = "edit-clear-symbolic"; + private const string DEFAULT_SEARCH_TEXT = _("Search"); private Gtk.Toolbar toolbar; public FolderMenu copy_folder_menu { get; private set; } @@ -14,13 +15,18 @@ public class MainToolbar : Gtk.Box { private GtkUtil.ToggleToolbarDropdown mark_menu_dropdown; private GtkUtil.ToggleToolbarDropdown app_menu_dropdown; + private Gtk.ToolItem search_container; private Gtk.Entry search_entry; + private Geary.ProgressMonitor? search_upgrade_progress_monitor = null; + private MonitoredProgressBar search_upgrade_progress_bar = new MonitoredProgressBar(); public signal void search_text_changed(string search_text); public MainToolbar() { Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0); - + + GearyApplication.instance.current_account_changed.connect(on_account_changed); + Gtk.Builder builder = GearyApplication.instance.create_builder("toolbar.glade"); toolbar = builder.get_object("toolbar") as Gtk.Toolbar; @@ -61,6 +67,7 @@ public class MainToolbar : Gtk.Box { mark_menu_dropdown.attach(mark_menu_button); // Search bar. + search_container = (Gtk.ToolItem) builder.get_object("search_container"); search_entry = (Gtk.Entry) builder.get_object("search_entry"); search_entry.changed.connect(on_search_entry_changed); search_entry.icon_release.connect(on_search_entry_icon_release); @@ -84,7 +91,11 @@ public class MainToolbar : Gtk.Box { toolbar.get_style_context().add_class("primary-toolbar"); + search_upgrade_progress_bar.show_text = true; + search_upgrade_progress_bar.margin_top = search_upgrade_progress_bar.margin_bottom = 3; + add(toolbar); + set_search_placeholder_text(DEFAULT_SEARCH_TEXT); } private Gtk.ToolButton set_toolbutton_action(Gtk.Builder builder, string action) { @@ -119,5 +130,41 @@ public class MainToolbar : Gtk.Box { return false; } + + private void on_search_upgrade_start() { + search_container.remove(search_container.get_child()); + search_container.add(search_upgrade_progress_bar); + search_upgrade_progress_bar.show(); + } + + private void on_search_upgrade_finished() { + search_container.remove(search_container.get_child()); + search_container.add(search_entry); + } + + private void on_account_changed(Geary.Account? account) { + on_search_upgrade_finished(); // Reset search box. + + if (search_upgrade_progress_monitor != null) { + search_upgrade_progress_monitor.start.disconnect(on_search_upgrade_start); + search_upgrade_progress_monitor.finish.disconnect(on_search_upgrade_finished); + search_upgrade_progress_monitor = null; + } + + if (account != null) { + search_upgrade_progress_monitor = account.search_upgrade_monitor; + search_upgrade_progress_bar.set_progress_monitor(search_upgrade_progress_monitor); + + search_upgrade_progress_monitor.start.connect(on_search_upgrade_start); + search_upgrade_progress_monitor.finish.connect(on_search_upgrade_finished); + if (search_upgrade_progress_monitor.is_in_progress) + on_search_upgrade_start(); // Remove search box, we're already in progress. + } + + search_upgrade_progress_bar.text = _("Indexing %s account").printf(account.information.nickname); + + set_search_placeholder_text(account == null || GearyApplication.instance.get_num_accounts() == 1 ? + DEFAULT_SEARCH_TEXT : _("Search %s account").printf(account.information.nickname)); + } } diff --git a/src/client/ui/monitored-progress-bar.vala b/src/client/ui/monitored-progress-bar.vala new file mode 100644 index 00000000..c6028528 --- /dev/null +++ b/src/client/ui/monitored-progress-bar.vala @@ -0,0 +1,34 @@ +/* Copyright 2013 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * Adapts a progress bar to automatically display progress of a Geary.ProgressMonitor. + */ +public class MonitoredProgressBar : Gtk.ProgressBar { + private Geary.ProgressMonitor? monitor = null; + + public void set_progress_monitor(Geary.ProgressMonitor monitor) { + this.monitor = monitor; + monitor.start.connect(on_start); + monitor.finish.connect(on_finish); + monitor.update.connect(on_update); + + fraction = monitor.progress; + } + + private void on_start() { + fraction = 0.0; + } + + private void on_update(double total_progress, double change, Geary.ProgressMonitor monitor) { + fraction = total_progress; + } + + private void on_finish() { + fraction = 1.0; + } +} + diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala index dbea4ccb..b0449b1b 100644 --- a/src/engine/abstract/geary-abstract-account.vala +++ b/src/engine/abstract/geary-abstract-account.vala @@ -6,6 +6,7 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account { public Geary.AccountInformation information { get; protected set; } + public Geary.ProgressMonitor search_upgrade_monitor { get; protected set; } private string name; diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala index b94fa71c..a783abcd 100644 --- a/src/engine/api/geary-account.vala +++ b/src/engine/api/geary-account.vala @@ -15,6 +15,8 @@ public interface Geary.Account : BaseObject { public abstract Geary.AccountInformation information { get; protected set; } + public abstract Geary.ProgressMonitor search_upgrade_monitor { get; protected set; } + public signal void opened(); public signal void closed(); diff --git a/src/engine/api/geary-progress-monitor.vala b/src/engine/api/geary-progress-monitor.vala new file mode 100644 index 00000000..701c5c81 --- /dev/null +++ b/src/engine/api/geary-progress-monitor.vala @@ -0,0 +1,205 @@ +/* Copyright 2013 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * Type of progress monitor. + */ +public enum Geary.ProgressType { + AGGREGATED, + ACTIVITY, + DB_UPGRADE, + SEARCH_INDEX +} + +/** + * Base class for progress monitoring. + */ +public abstract class Geary.ProgressMonitor : BaseObject { + public const double MIN = 0.0; + public const double MAX = 1.0; + + public double progress { get; protected set; default = MIN; } + public bool is_in_progress { get; protected set; default = false; } + public Geary.ProgressType progress_type { get; protected set; } + + /** + * The start signal is fired just before progress begins. It will not fire again until after + * {@link finish} has fired. + */ + public signal void start(); + + /** + * Notifies the user of existing progress. Note that monitor refers to the monitor that + * invoked this update, which may not be the same as this object. + */ + public signal void update(double total_progress, double change, Geary.ProgressMonitor monitor); + + /** + * Finish is fired when progress has completed. + */ + public signal void finish(); + + /** + * Users must call this before calling update. Must not be called again until + * {@link notify_finish()} has been called. + */ + public virtual void notify_start() { + assert(!is_in_progress); + progress = MIN; + is_in_progress = true; + + start(); + } + + /** + * Users must call this when progress has completed. Must only be called after {@link notify_start()} + */ + public virtual void notify_finish() { + assert(is_in_progress); + is_in_progress = false; + + finish(); + } +} + +/** + * Captures the progress of a single action. + */ +public class Geary.SimpleProgressMonitor : Geary.ProgressMonitor { + /** + * Creates a new progress monitor of the given type. + */ + public SimpleProgressMonitor(ProgressType type) { + this.progress_type = type; + } + + /** + * Updates the progress by the given value. Must be between {@link MIN} and {@link MAX}. Must only + * be called after {@link notify_start()} and before {@link notify_finish()} + */ + public void increment(double value) { + assert(value > 0); + assert(is_in_progress); + + if (progress + value > MAX) + value = MAX - progress; + + progress += value; + update(progress, value, this); + } +} + +/** + * Monitors the progress of a countable interval. Note that min and max are inclusive. + */ +public class Geary.IntervalProgressMonitor : Geary.ProgressMonitor { + private int min_interval; + private int max_interval; + private int current = 0; + + /** + * Creates a new progress monitor with the given interval range. + */ + public IntervalProgressMonitor(ProgressType type, int min, int max) { + this.progress_type = type; + this.min_interval = min; + this.max_interval = max; + } + + /** + * Sets a new interval. Must not be done while in progress. + */ + public void set_interval(int min, int max) { + assert(!is_in_progress); + this.min_interval = min; + this.max_interval = max; + } + + public override void notify_start() { + current = 0; + base.notify_start(); + } + + /** + * Incrememts the progress + */ + public void increment(int count = 1) { + assert(is_in_progress); + assert(count + progress >= min_interval); + assert(count + progress <= max_interval); + + current += count; + + double new_progress = (1.0 * current - min_interval) / (1.0 * max_interval - min_interval); + double change = new_progress - progress; + progress = new_progress; + + update(progress, change, this); + } +} + +/** + * Captures progress of multiple actions by composing + * many progress monitors into one. + */ +public class Geary.AggregateProgressMonitor : Geary.ProgressMonitor { + private Gee.HashSet monitors = new Gee.HashSet(); + + /** + * Creates an aggregate progress monitor. + */ + public AggregateProgressMonitor() { + this.progress_type = Geary.ProgressType.AGGREGATED; + } + + /** + * Adds a new progress monitor to this aggregate. + */ + public void add(Geary.ProgressMonitor pm) { + // TODO: Handle the case where we add a new monitor during progress. + monitors.add(pm); + pm.start.connect(on_start); + pm.update.connect(on_update); + pm.finish.connect(on_finish); + } + + private void on_start() { + if (!is_in_progress) + notify_start(); + } + + private void on_update(double total_progress, double change, ProgressMonitor monitor) { + assert(is_in_progress); + + double updated_progress = MIN; + foreach(Geary.ProgressMonitor pm in monitors) + updated_progress += pm.progress; + + updated_progress /= monitors.size; + + double aggregated_change = updated_progress - progress; + if (aggregated_change < 0) + aggregated_change = 0; + + progress += updated_progress; + + if (progress > MAX) + progress = MAX; + + update(progress, aggregated_change, monitor); + } + + private void on_finish() { + // Only signal completion once all progress monitors are complete. + foreach(Geary.ProgressMonitor pm in monitors) { + if (pm.is_in_progress) + return; + } + + notify_finish(); + } +} + diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala index e572c549..f6c553d0 100644 --- a/src/engine/imap-db/imap-db-account.vala +++ b/src/engine/imap-db/imap-db-account.vala @@ -26,6 +26,8 @@ private class Geary.ImapDB.Account : BaseObject { new Gee.HashMap(); private Cancellable? background_cancellable = null; public ImapEngine.ContactStore contact_store { get; private set; } + public IntervalProgressMonitor search_index_monitor { get; private set; + default = new IntervalProgressMonitor(ProgressType.SEARCH_INDEX, 0, 0); } public Account(Geary.AccountInformation account_information) { this.account_information = account_information; @@ -698,15 +700,35 @@ private class Geary.ImapDB.Account : BaseObject { debug("Deleted %d duplicate folders", count); } + public async int get_email_count_async(Cancellable? cancellable) throws Error { + check_open(); + + int count = 0; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + count = do_get_email_count(cx, cancellable); + + return Db.TransactionOutcome.SUCCESS; + }, cancellable); + + return count; + } + private async void populate_search_table_async(Cancellable? cancellable) { - // TODO: send processing signal upwards. debug("Populating search table"); try { + int total = yield get_email_count_async(cancellable); + search_index_monitor.set_interval(0, total); + search_index_monitor.notify_start(); + while (!yield populate_search_table_batch_async(100, cancellable)) ; } catch (Error e) { debug("Error populating search table: %s", e.message); } + + if (search_index_monitor.is_in_progress) + search_index_monitor.notify_finish(); + debug("Done populating search table"); } @@ -749,6 +771,8 @@ private class Geary.ImapDB.Account : BaseObject { } ++count; + search_index_monitor.increment(); + result.next(cancellable); } @@ -925,5 +949,17 @@ private class Geary.ImapDB.Account : BaseObject { stmt.exec(cancellable); } + + private int do_get_email_count(Db.Connection cx, Cancellable? cancellable) + throws Error { + Db.Statement stmt = cx.prepare( + "SELECT COUNT(*) FROM MessageTable"); + + Db.Result results = stmt.exec(cancellable); + if (results.finished) + return 0; + + return results.int_at(0); + } } diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala b/src/engine/imap-engine/imap-engine-generic-account.vala index 9a3bd33d..aeeda22c 100644 --- a/src/engine/imap-engine/imap-engine-generic-account.vala +++ b/src/engine/imap-engine/imap-engine-generic-account.vala @@ -33,6 +33,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { this.remote.login_failed.connect(on_login_failed); this.remote.email_sent.connect(on_email_sent); + search_upgrade_monitor = local.search_index_monitor; + if (inbox_path == null) { inbox_path = new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR, Imap.Folder.CASE_SENSITIVE); diff --git a/ui/toolbar.glade b/ui/toolbar.glade index 7530c9c0..4318abee 100644 --- a/ui/toolbar.glade +++ b/ui/toolbar.glade @@ -6,11 +6,9 @@ False - False True False Start new conversation (Ctrl+N, N) - False True New Message True @@ -18,15 +16,13 @@ False - False + True - False True False - False False @@ -34,11 +30,9 @@ - False True False Reply to last message in conversation (Ctrl+R, R) - False True Reply True @@ -46,16 +40,14 @@ False - False + True - False True False Reply to everyone in last message of conversation (Ctrl+Shift+R, Shift+R) - False True Reply All True @@ -63,16 +55,14 @@ False - False + True - False True False Send copy of last message in conversation (Ctrl+L, F) - False True Forward True @@ -80,15 +70,13 @@ False - False + True - False True False - False False @@ -96,52 +84,44 @@ - False True False - False Mark edit-mark False - False + True - False True False - False Label as tag-new False - False + True - False True False - False Move to mail-move False - False + True - False True False - False False @@ -156,15 +136,13 @@ False - False + True - False True False - False @@ -177,12 +155,13 @@ True False + True True True + True - 35 edit-find-symbolic edit-clear-symbolic False @@ -190,23 +169,21 @@ - False + True True - False True False - False Menu True application-menu False - False + True