diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5c8dfd4b..c20bf782 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ common/common-yorba-application.vala set(ENGINE_SRC engine/abstract/geary-abstract-account.vala engine/abstract/geary-abstract-folder.vala +engine/abstract/geary-abstract-local-folder.vala engine/api/geary-account.vala engine/api/geary-account-information.vala @@ -42,6 +43,7 @@ engine/api/geary-folder-supports-mark.vala engine/api/geary-folder-supports-move.vala engine/api/geary-folder-supports-remove.vala engine/api/geary-logging.vala +engine/api/geary-search-folder.vala engine/api/geary-service-provider.vala engine/api/geary-special-folder-type.vala @@ -235,11 +237,13 @@ client/dialogs/alert-dialog.vala client/dialogs/password-dialog.vala client/dialogs/preferences-dialog.vala +client/folder-list/folder-list-abstract-folder-entry.vala client/folder-list/folder-list-account-branch.vala client/folder-list/folder-list-folder-entry.vala client/folder-list/folder-list-tree.vala client/folder-list/folder-list-inboxes-branch.vala client/folder-list/folder-list-inbox-folder-entry.vala +client/folder-list/folder-list-search-branch.vala client/folder-list/folder-list-special-grouping.vala client/models/conversation-list-store.vala diff --git a/src/client/accounts/account-dialog-account-list-pane.vala b/src/client/accounts/account-dialog-account-list-pane.vala index 87f07921..bbcbfda0 100644 --- a/src/client/accounts/account-dialog-account-list-pane.vala +++ b/src/client/accounts/account-dialog-account-list-pane.vala @@ -124,17 +124,8 @@ public class AccountDialogAccountListPane : AccountDialogPane { private void update_buttons() { edit_action.sensitive = get_selected_account() != null; - delete_action.sensitive = edit_action.sensitive && get_num_accounts() > 1; - } - - private int get_num_accounts() { - try { - return Geary.Engine.instance.get_accounts().size; - } catch (Error e) { - debug("Error getting number of accounts: %s", e.message); - } - - return 0; // on error + delete_action.sensitive = edit_action.sensitive && + GearyApplication.instance.get_num_accounts() > 1; } private void on_account_added(Geary.AccountInformation account) { diff --git a/src/client/folder-list/folder-list-abstract-folder-entry.vala b/src/client/folder-list/folder-list-abstract-folder-entry.vala new file mode 100644 index 00000000..307fc581 --- /dev/null +++ b/src/client/folder-list/folder-list-abstract-folder-entry.vala @@ -0,0 +1,29 @@ +/* Copyright 2011-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. + */ + +/** + * Abstract base class for sidebar entries that represent folders. This covers only + * the basics needed for any type of folder, and is intended to work with both local + * and remote folder types. + */ +public abstract class FolderList.AbstractFolderEntry : Geary.BaseObject, Sidebar.Entry, Sidebar.SelectableEntry { + public Geary.Folder folder { get; private set; } + + public AbstractFolderEntry(Geary.Folder folder) { + this.folder = folder; + } + + public abstract string get_sidebar_name(); + + public abstract string? get_sidebar_tooltip(); + + public abstract Icon? get_sidebar_icon(); + + public virtual string to_string() { + return "AbstractFolderEntry: " + get_sidebar_name(); + } +} + diff --git a/src/client/folder-list/folder-list-account-branch.vala b/src/client/folder-list/folder-list-account-branch.vala index 98cfce59..4be55d1e 100644 --- a/src/client/folder-list/folder-list-account-branch.vala +++ b/src/client/folder-list/folder-list-account-branch.vala @@ -81,6 +81,9 @@ public class FolderList.AccountBranch : Sidebar.Branch { FolderEntry folder_entry = new FolderEntry(folder); Geary.SpecialFolderType special_folder_type = folder.get_special_folder_type(); if (special_folder_type != Geary.SpecialFolderType.NONE) { + if (special_folder_type == Geary.SpecialFolderType.SEARCH) + return; // Don't show search folder under the account. + switch (special_folder_type) { // These special folders go in the root of the account. case Geary.SpecialFolderType.INBOX: diff --git a/src/client/folder-list/folder-list-folder-entry.vala b/src/client/folder-list/folder-list-folder-entry.vala index abac9439..967a3691 100644 --- a/src/client/folder-list/folder-list-folder-entry.vala +++ b/src/client/folder-list/folder-list-folder-entry.vala @@ -5,31 +5,30 @@ */ // A folder of any type in the folder list. -public class FolderList.FolderEntry : Geary.BaseObject, Sidebar.Entry, Sidebar.InternalDropTargetEntry, - Sidebar.SelectableEntry, Sidebar.EmphasizableEntry { - public Geary.Folder folder { get; private set; } +public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.InternalDropTargetEntry, + Sidebar.EmphasizableEntry { private bool has_new; private int unread_count; public FolderEntry(Geary.Folder folder) { - this.folder = folder; + base(folder); has_new = false; unread_count = 0; } - public virtual string get_sidebar_name() { + public override string get_sidebar_name() { return (unread_count == 0 ? folder.get_display_name() : /// This string gets the folder name and the unread messages count, /// e.g. All Mail (5). _("%s (%d)").printf(folder.get_display_name(), unread_count)); } - public string? get_sidebar_tooltip() { + public override string? get_sidebar_tooltip() { return (unread_count == 0 ? null : ngettext("%d unread message", "%d unread messages", unread_count).printf(unread_count)); } - public Icon? get_sidebar_icon() { + public override Icon? get_sidebar_icon() { switch (folder.get_special_folder_type()) { case Geary.SpecialFolderType.NONE: return IconFactory.instance.get_custom_icon("tag", IconFactory.ICON_SIDEBAR); @@ -66,7 +65,7 @@ public class FolderList.FolderEntry : Geary.BaseObject, Sidebar.Entry, Sidebar.I } } - public virtual string to_string() { + public override string to_string() { return "FolderEntry: " + get_sidebar_name(); } diff --git a/src/client/folder-list/folder-list-search-branch.vala b/src/client/folder-list/folder-list-search-branch.vala new file mode 100644 index 00000000..d6291f42 --- /dev/null +++ b/src/client/folder-list/folder-list-search-branch.vala @@ -0,0 +1,54 @@ +/* Copyright 2011-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. + */ + +/** + * This branch is a top-level container for a search entry. + */ +public class FolderList.SearchBranch : Sidebar.RootOnlyBranch { + public SearchBranch(Geary.SearchFolder folder) { + base(new SearchEntry(folder)); + } + + public Geary.SearchFolder get_search_folder() { + return (Geary.SearchFolder) ((SearchEntry) get_root()).folder; + } +} + +public class FolderList.SearchEntry : FolderList.AbstractFolderEntry { + public SearchEntry(Geary.SearchFolder folder) { + base(folder); + + Geary.Engine.instance.account_available.connect(on_accounts_changed); + Geary.Engine.instance.account_unavailable.connect(on_accounts_changed); + } + + ~SearchEntry() { + Geary.Engine.instance.account_available.disconnect(on_accounts_changed); + Geary.Engine.instance.account_unavailable.disconnect(on_accounts_changed); + } + + public override string get_sidebar_name() { + return GearyApplication.instance.get_num_accounts() == 1 ? _("Search") : + _("Search %s account").printf(folder.account.information.nickname); + } + + public override string? get_sidebar_tooltip() { + return _("%d results").printf(folder.get_properties().email_total); + } + + public override Icon? get_sidebar_icon() { + return new ThemedIcon("search"); + } + + public override string to_string() { + return "SearchEntry: " + folder.to_string(); + } + + private void on_accounts_changed() { + sidebar_name_changed(get_sidebar_name()); + } +} + diff --git a/src/client/folder-list/folder-list-tree.vala b/src/client/folder-list/folder-list-tree.vala index 1463f7ef..c4263e90 100644 --- a/src/client/folder-list/folder-list-tree.vala +++ b/src/client/folder-list/folder-list-tree.vala @@ -5,11 +5,13 @@ */ public class FolderList.Tree : Sidebar.Tree { - public const Gtk.TargetEntry[] TARGET_ENTRY_LIST = { { "application/x-geary-mail", Gtk.TargetFlags.SAME_APP, 0 } }; + private const int INBOX_ORDINAL = -2; // First account branch is zero + private const int SEARCH_ORDINAL = -1; + public signal void folder_selected(Geary.Folder? folder); public signal void copy_conversation(Geary.Folder folder); public signal void move_conversation(Geary.Folder folder); @@ -17,6 +19,7 @@ public class FolderList.Tree : Sidebar.Tree { private Gee.HashMap account_branches = new Gee.HashMap(); private InboxesBranch inboxes_branch = new InboxesBranch(); + private SearchBranch? search_branch = null; private NewMessagesMonitor? monitor = null; public Tree() { @@ -43,9 +46,9 @@ public class FolderList.Tree : Sidebar.Tree { } private void on_entry_selected(Sidebar.SelectableEntry selectable) { - if (selectable is FolderEntry) { - folder_selected(((FolderEntry) selectable).folder); - } + AbstractFolderEntry? abstract_folder_entry = selectable as AbstractFolderEntry; + if (abstract_folder_entry != null) + folder_selected(abstract_folder_entry.folder); } private void on_new_messages_changed(Geary.Folder folder, int count) { @@ -87,7 +90,7 @@ public class FolderList.Tree : Sidebar.Tree { graft(account_branch, folder.account.information.ordinal); if (account_branches.size > 1 && !has_branch(inboxes_branch)) - graft(inboxes_branch, -1); // The Inboxes branch comes first. + graft(inboxes_branch, INBOX_ORDINAL); // The Inboxes branch comes first. if (folder.get_special_folder_type() == Geary.SpecialFolderType.INBOX) inboxes_branch.add_inbox(folder); @@ -191,4 +194,28 @@ public class FolderList.Tree : Sidebar.Tree { foreach (AccountBranch branch in branches_to_reorder) graft(branch, branch.account.information.ordinal); } + + public void set_search(Geary.SearchFolder search_folder) { + if (search_branch != null && has_branch(search_branch)) { + // We already have a search folder. If it's the same one, do nothing. If it's a new + // search folder, remove the old one and continue. + if (search_folder == search_branch.get_search_folder()) { + return; + } else { + remove_search(); + } + } + + search_branch = new SearchBranch(search_folder); + graft(search_branch, SEARCH_ORDINAL); + place_cursor(search_branch.get_root(), false); + } + + public void remove_search() { + if (search_branch != null) { + prune(search_branch); + search_branch = null; + } + } } + diff --git a/src/client/geary-application.vala b/src/client/geary-application.vala index bd60677c..30a404d8 100644 --- a/src/client/geary-application.vala +++ b/src/client/geary-application.vala @@ -463,5 +463,19 @@ along with Geary; if not, write to the Free Software Foundation, Inc., public Gee.List? get_composer_windows_for_account(Geary.AccountInformation account) { return controller.get_composer_windows_for_account(account); } + + /** + * Returns the number of accounts that exist in Geary. Note that not all accounts may be + * open. Zero is returned on an error. + */ + public int get_num_accounts() { + try { + return Geary.Engine.instance.get_accounts().size; + } catch (Error e) { + debug("Error getting number of accounts: %s", e.message); + } + + return 0; // on error + } } diff --git a/src/client/geary-controller.vala b/src/client/geary-controller.vala index c93a242b..7f9961f4 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -81,6 +81,7 @@ public class GearyController { private Geary.Folder? folder_to_select = null; private Geary.Nonblocking.Mutex select_folder_mutex = new Geary.Nonblocking.Mutex(); private Geary.Account? account_to_select = null; + private Geary.Folder? previous_non_search_folder = null; public GearyController() { // This initializes the IconFactory, important to do before the actions are created (as they @@ -116,6 +117,7 @@ public class GearyController { main_window.folder_list.move_conversation.connect(on_move_conversation); main_window.main_toolbar.copy_folder_menu.folder_selected.connect(on_copy_conversation); main_window.main_toolbar.move_folder_menu.folder_selected.connect(on_move_conversation); + main_window.main_toolbar.search_text_changed.connect(on_search_text_changed); main_window.conversation_viewer.link_selected.connect(on_link_selected); main_window.conversation_viewer.reply_to_message.connect(on_reply_to_message); main_window.conversation_viewer.reply_all_message.connect(on_reply_all_message); @@ -324,10 +326,15 @@ 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) { cancel_inbox(account); + + previous_non_search_folder = null; + main_window.main_toolbar.set_search_text(""); // Reset search. if (current_account == account) { cancel_folder(); cancel_message(); @@ -365,6 +372,8 @@ public class GearyController { } catch (Error e) { message("Error enumerating accounts: %s", e.message); } + + update_search_placeholder_text(); } // Returns the number of open accounts. @@ -388,6 +397,7 @@ 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; @@ -464,6 +474,9 @@ public class GearyController { current_folder = folder; current_account = folder.account; + if (!(current_folder is Geary.SearchFolder)) + previous_non_search_folder = current_folder; + main_window.conversation_list_store.set_current_folder(current_folder, conversation_cancellable); main_window.conversation_list_store.account_owner_email = current_account.information.email; @@ -1459,5 +1472,38 @@ public class GearyController { return ret.size >= 1 ? ret : null; } + + private void on_search_text_changed(string search_text) { + if (search_text == "") { + if (previous_non_search_folder != null && current_folder is Geary.SearchFolder) + main_window.folder_list.select_folder(previous_non_search_folder); + + main_window.folder_list.remove_search(); + + return; + } + + if (current_account == null) + return; + + Geary.SearchFolder? folder; + try { + folder = (Geary.SearchFolder) current_account.get_special_folder( + Geary.SpecialFolderType.SEARCH); + folder.set_search_keywords(search_text); + } catch (Error e) { + debug("Could not get search folder: %s", e.message); + + return; + } + + 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 cfaee123..22b14587 100644 --- a/src/client/ui/main-toolbar.vala +++ b/src/client/ui/main-toolbar.vala @@ -6,12 +6,17 @@ // Draws the main toolbar. public class MainToolbar : Gtk.Box { + private const string ICON_CLEAR_NAME = "edit-clear-symbolic"; + private Gtk.Toolbar toolbar; public FolderMenu copy_folder_menu { get; private set; } public FolderMenu move_folder_menu { get; private set; } private GtkUtil.ToggleToolbarDropdown mark_menu_dropdown; private GtkUtil.ToggleToolbarDropdown app_menu_dropdown; + private Gtk.Entry search_entry; + + public signal void search_text_changed(string search_text); public MainToolbar() { Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0); @@ -55,6 +60,14 @@ public class MainToolbar : Gtk.Box { Gtk.IconSize.LARGE_TOOLBAR, mark_menu, mark_proxy_menu); mark_menu_dropdown.attach(mark_menu_button); + // Search bar. + 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); + search_entry.key_press_event.connect(on_search_key_press); + on_search_entry_changed(); // set initial state + search_entry.has_focus = true; + // Setup the application menu. GearyApplication.instance.load_ui_file("toolbar_menu.ui"); Gtk.Menu application_menu = GearyApplication.instance.ui_manager.get_widget("/ui/ToolbarMenu") @@ -79,5 +92,32 @@ public class MainToolbar : Gtk.Box { button.set_related_action(GearyApplication.instance.actions.get_action(action)); return button; } + + public void set_search_text(string text) { + search_entry.text = text; + } + + public void set_search_placeholder_text(string placeholder) { + search_entry.placeholder_text = placeholder; + } + + private void on_search_entry_changed() { + search_text_changed(search_entry.text); + // Enable/disable clear button. + search_entry.secondary_icon_name = search_entry.text != "" ? ICON_CLEAR_NAME : null; + } + + private void on_search_entry_icon_release(Gtk.EntryIconPosition icon_pos, Gdk.Event event) { + if (icon_pos == Gtk.EntryIconPosition.SECONDARY) + search_entry.text = ""; + } + + private bool on_search_key_press(Gdk.EventKey event) { + // Clear box if user hits escape. + if (Gdk.keyval_name(event.keyval) == "Escape") + search_entry.text = ""; + + return false; + } } diff --git a/src/client/ui/main-window.vala b/src/client/ui/main-window.vala index 5cb7cf19..3d8bcc8e 100644 --- a/src/client/ui/main-window.vala +++ b/src/client/ui/main-window.vala @@ -47,6 +47,7 @@ public class MainWindow : Gtk.Window { set_default_icon_list(pixbuf_list); delete_event.connect(on_delete_event); + key_press_event.connect(on_key_press_event); create_layout(); } diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala index df7a17ac..14b5efd5 100644 --- a/src/engine/abstract/geary-abstract-account.vala +++ b/src/engine/abstract/geary-abstract-account.vala @@ -82,6 +82,10 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account { public abstract async Geary.Email local_fetch_email_async(Geary.EmailIdentifier email_id, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; + public abstract async Gee.Collection? local_search_async(string keywords, + Gee.Collection? folder_blacklist = null, + Gee.Collection? search_ids = null, Cancellable? cancellable = null) throws Error; + public virtual string to_string() { return name; } diff --git a/src/engine/abstract/geary-abstract-local-folder.vala b/src/engine/abstract/geary-abstract-local-folder.vala new file mode 100644 index 00000000..1334baa7 --- /dev/null +++ b/src/engine/abstract/geary-abstract-local-folder.vala @@ -0,0 +1,47 @@ +/* Copyright 2011-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. + */ + +/** + * Handles open/close for local folders. + */ +public abstract class Geary.AbstractLocalFolder : Geary.AbstractFolder { + private int open_count = 0; + + public override Geary.Folder.OpenState get_open_state() { + return open_count > 0 ? Geary.Folder.OpenState.LOCAL : Geary.Folder.OpenState.CLOSED; + } + + protected void check_open() throws EngineError { + if (open_count == 0) + throw new EngineError.OPEN_REQUIRED("%s not open", to_string()); + } + + protected bool is_open() { + return open_count > 0; + } + + public override async void wait_for_open_async(Cancellable? cancellable = null) throws Error { + if (open_count == 0) + throw new EngineError.OPEN_REQUIRED("%s not open".printf(get_display_name())); + } + + public override async void open_async(bool readonly, Cancellable? cancellable = null) + throws Error { + if (open_count++ > 0) + return; + + notify_opened(Geary.Folder.OpenState.LOCAL, get_properties().email_total); + } + + public override async void close_async(Cancellable? cancellable = null) throws Error { + if (open_count == 0 || --open_count > 0) + return; + + notify_closed(Geary.Folder.CloseReason.LOCAL_CLOSE); + notify_closed(Geary.Folder.CloseReason.FOLDER_CLOSED); + } +} + diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala index 837927f9..5a639bfc 100644 --- a/src/engine/api/geary-account.vala +++ b/src/engine/api/geary-account.vala @@ -181,6 +181,14 @@ public interface Geary.Account : BaseObject { public abstract async Geary.Email local_fetch_email_async(Geary.EmailIdentifier email_id, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; + /** + * Performs a search with the given keyword string. Optionally, a list of folders not to search + * can be passed as well as a list of email identifiers to restrict the search to only those messages. + */ + public abstract async Gee.Collection? local_search_async(string keywords, + Gee.Collection? folder_blacklist = null, + Gee.Collection? search_ids = null, Cancellable? cancellable = null) throws Error; + /** * Used only for debugging. Should not be used for user-visible strings. */ diff --git a/src/engine/api/geary-search-folder.vala b/src/engine/api/geary-search-folder.vala new file mode 100644 index 00000000..db8a2fd7 --- /dev/null +++ b/src/engine/api/geary-search-folder.vala @@ -0,0 +1,134 @@ +/* Copyright 2011-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. + */ + +public class Geary.SearchFolderRoot : Geary.FolderRoot { + public const string MAGIC_BASENAME = "$GearySearchFolder$"; + + public SearchFolderRoot() { + base(MAGIC_BASENAME, null, false); + } +} + +public class Geary.SearchFolderProperties : Geary.FolderProperties { + public SearchFolderProperties(int total, int unread) { + base(total, unread, Trillian.FALSE, Trillian.FALSE, Trillian.TRUE); + } + + public void set_total(int total) { + this.email_total = total; + } +} + +/** + * Special folder type used to query and display search results. + */ +public class Geary.SearchFolder : Geary.AbstractLocalFolder { + public override Account account { get { return _account; } } + + private static FolderRoot? path = null; + + private weak Account _account; + private SearchFolderProperties properties = new SearchFolderProperties(0, 0); + private Gee.HashSet exclude_folders = new Gee.HashSet(); + private Geary.SpecialFolderType[] exclude_types = { Geary.SpecialFolderType.SPAM, + Geary.SpecialFolderType.TRASH }; + + /** + * Fired when the search keywords have changed. + */ + public signal void search_keywords_changed(string keywords); + + public SearchFolder(Account account) { + _account = account; + + // TODO: The exclusion system needs to watch for changes, since the special folders are + // not always ready by the time this c'tor executes. + foreach(Geary.SpecialFolderType type in exclude_types) + exclude_special_folder(type); + } + + /** + * Sets the keyword string for this search. + */ + public void set_search_keywords(string keywords) { + search_keywords_changed(keywords); + account.local_search_async.begin(keywords, exclude_folders, null, null, on_local_search_complete); + } + + private void on_local_search_complete(Object? source, AsyncResult result) { + Gee.Collection? search_results = null; + try { + search_results = account.local_search_async.end(result); + } catch (Error e) { + debug("Error gathering search results: %s", e.message); + } + + if (search_results != null) + search_results.clear(); // TODO: something useful. + } + + public override Geary.FolderPath get_path() { + if (path == null) + path = new SearchFolderRoot(); + + return path; + } + + public override Geary.SpecialFolderType get_special_folder_type() { + return Geary.SpecialFolderType.SEARCH; + } + + public override Geary.FolderProperties get_properties() { + return properties; + } + + public override async Gee.List? list_email_async(int low, int count, + Geary.Email.Field required_fields, Folder.ListFlags flags, Cancellable? cancellable = null) + throws Error { + return yield account.get_special_folder(Geary.SpecialFolderType.INBOX).list_email_async(low, count, + required_fields, flags, cancellable); + } + + public override async Gee.List? list_email_by_id_async(Geary.EmailIdentifier initial_id, + int count, Geary.Email.Field required_fields, Folder.ListFlags flags, Cancellable? cancellable = null) + throws Error { + return yield account.get_special_folder(Geary.SpecialFolderType.INBOX).list_email_by_id_async(initial_id, + count, required_fields, flags, cancellable); + } + + public override async Gee.List? list_email_by_sparse_id_async( + Gee.Collection ids, Geary.Email.Field required_fields, Folder.ListFlags flags, + Cancellable? cancellable = null) throws Error { + return yield account.get_special_folder(Geary.SpecialFolderType.INBOX).list_email_by_sparse_id_async(ids, + required_fields, flags, cancellable); + } + + public override async Gee.Map? list_local_email_fields_async( + Gee.Collection ids, Cancellable? cancellable = null) throws Error { + return yield account.get_special_folder(Geary.SpecialFolderType.INBOX).list_local_email_fields_async( + ids, cancellable); + } + + public override async Geary.Email fetch_email_async(Geary.EmailIdentifier id, + Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, + Cancellable? cancellable = null) throws Error { + return yield account.get_special_folder(Geary.SpecialFolderType.INBOX).fetch_email_async(id, + required_fields, flags, cancellable); + } + + private void exclude_special_folder(Geary.SpecialFolderType type) { + Geary.Folder? folder = null; + try { + folder = account.get_special_folder(type); + } catch (Error e) { + debug("Could not get special folder: %s", e.message); + } + + if (folder != null) + exclude_folders.add(folder.get_path()); + } +} + diff --git a/src/engine/api/geary-special-folder-type.vala b/src/engine/api/geary-special-folder-type.vala index 2d2e4e4e..34c6033d 100644 --- a/src/engine/api/geary-special-folder-type.vala +++ b/src/engine/api/geary-special-folder-type.vala @@ -7,6 +7,7 @@ public enum Geary.SpecialFolderType { NONE, INBOX, + SEARCH, DRAFTS, SENT, FLAGGED, @@ -45,6 +46,9 @@ public enum Geary.SpecialFolderType { case OUTBOX: return _("Outbox"); + case SEARCH: + return _("Search"); + case NONE: default: return _("None"); diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala index 3ddcd9d0..6b660654 100644 --- a/src/engine/imap-db/imap-db-account.vala +++ b/src/engine/imap-db/imap-db-account.vala @@ -17,6 +17,7 @@ private class Geary.ImapDB.Account : BaseObject { // Only available when the Account is opened public SmtpOutboxFolder? outbox { get; private set; default = null; } + public SearchFolder? search_folder { get; private set; default = null; } private string name; private AccountInformation account_information; @@ -72,6 +73,9 @@ private class Geary.ImapDB.Account : BaseObject { // ImapDB.Account holds the Outbox, which is tied to the database it maintains outbox = new SmtpOutboxFolder(db, account); + // Search folder + search_folder = new SearchFolder(account); + // Need to clear duplicate folders due to old bug that caused multiple folders to be // created in the database ... benign due to other logic, but want to prevent this from // happening if possible @@ -90,6 +94,7 @@ private class Geary.ImapDB.Account : BaseObject { } outbox = null; + search_folder = null; } public async void clone_folder_async(Geary.Imap.Folder imap_folder, Cancellable? cancellable = null) diff --git a/src/engine/imap-db/outbox/smtp-outbox-folder.vala b/src/engine/imap-db/outbox/smtp-outbox-folder.vala index bbff4905..f9c7ac14 100644 --- a/src/engine/imap-db/outbox/smtp-outbox-folder.vala +++ b/src/engine/imap-db/outbox/smtp-outbox-folder.vala @@ -11,7 +11,7 @@ // on the ImapDB.Database. SmtpOutboxFolder assumes the database is opened before it's passed in // to the constructor -- it does not open or close the database itself and will start using it // immediately. -private class Geary.SmtpOutboxFolder : Geary.AbstractFolder, Geary.FolderSupport.Remove, +private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSupport.Remove, Geary.FolderSupport.Create { private class OutboxRow { public int64 id; @@ -43,7 +43,6 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractFolder, Geary.FolderSupport private ImapDB.Database db; private weak Account _account; private Geary.Smtp.ClientSession smtp; - private int open_count = 0; private Nonblocking.Mailbox outbox_queue = new Nonblocking.Mailbox(); private SmtpOutboxFolderProperties properties = new SmtpOutboxFolderProperties(0, 0); @@ -199,33 +198,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractFolder, Geary.FolderSupport } public override Geary.Folder.OpenState get_open_state() { - return open_count > 0 ? Geary.Folder.OpenState.LOCAL : Geary.Folder.OpenState.CLOSED; - } - - private void check_open() throws EngineError { - if (open_count == 0) - throw new EngineError.OPEN_REQUIRED("%s not open", to_string()); - } - - public override async void wait_for_open_async(Cancellable? cancellable = null) throws Error { - if (open_count == 0) - throw new EngineError.OPEN_REQUIRED("Outbox not open"); - } - - public override async void open_async(bool readonly, Cancellable? cancellable = null) - throws Error { - if (open_count++ > 0) - return; - - notify_opened(Geary.Folder.OpenState.LOCAL, properties.email_total); - } - - public override async void close_async(Cancellable? cancellable = null) throws Error { - if (open_count == 0 || --open_count > 0) - return; - - notify_closed(Geary.Folder.CloseReason.LOCAL_CLOSE); - notify_closed(Geary.Folder.CloseReason.FOLDER_CLOSED); + return is_open() ? Geary.Folder.OpenState.LOCAL : Geary.Folder.OpenState.CLOSED; } private async int get_email_count_async(Cancellable? cancellable) throws Error { @@ -282,7 +255,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractFolder, Geary.FolderSupport outbox_queue.send(row); // notify only if opened - if (open_count > 0) { + if (is_open()) { Gee.List list = new Gee.ArrayList(); list.add(row.outbox_id); @@ -490,7 +463,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractFolder, Geary.FolderSupport return false; // notify only if opened - if (open_count > 0) { + if (is_open()) { notify_email_removed(removed); notify_email_count_changed(final_count, CountChangeReason.REMOVED); } diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala b/src/engine/imap-engine/imap-engine-generic-account.vala index d369c85d..a4b00681 100644 --- a/src/engine/imap-engine/imap-engine-generic-account.vala +++ b/src/engine/imap-engine/imap-engine-generic-account.vala @@ -9,6 +9,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { private static Geary.FolderPath? inbox_path = null; private static Geary.FolderPath? outbox_path = null; + private static Geary.FolderPath? search_path = null; private Imap.Account remote; private ImapDB.Account local; @@ -40,6 +41,10 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { if (outbox_path == null) { outbox_path = new SmtpOutboxFolderRoot(); } + + if (search_path == null) { + search_path = new SearchFolderRoot(); + } } internal Imap.FolderProperties? get_properties_for_folder(FolderPath path) { @@ -67,6 +72,9 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { local.outbox.report_problem.connect(notify_report_problem); local_only.set(outbox_path, local.outbox); + // Search folder. + local_only.set(search_path, local.search_folder); + // need to back out local.open_async() if remote fails try { yield remote.open_async(cancellable); @@ -136,7 +144,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { // appropriate interfaces attached. The returned folder should have its SpecialFolderType // set using either the properties from the local folder or its path. // - // This won't be called to build the Outbox, but for all others (including Inbox) it will. + // This won't be called to build the Outbox or search folder, but for all others (including Inbox) it will. protected abstract GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account, ImapDB.Account local_account, ImapDB.Folder local_folder); @@ -187,8 +195,11 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { public override Gee.Collection list_folders() throws Error { check_open(); + Gee.HashSet all_folders = new Gee.HashSet(); + all_folders.add_all(existing_folders.values); + all_folders.add_all(local_only.values); - return existing_folders.values; + return all_folders; } private void reschedule_folder_refresh(bool immediate) { @@ -482,6 +493,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { return yield local.fetch_email_async(email_id, required_fields, cancellable); } + public override async Gee.Collection? local_search_async(string keywords, + Gee.Collection? folder_blacklist = null, + Gee.Collection? search_ids = null, Cancellable? cancellable = null) throws Error { + return null; // TODO: search! + } + private void on_login_failed(Geary.Credentials? credentials) { do_login_failed_async.begin(credentials); } diff --git a/ui/toolbar.glade b/ui/toolbar.glade index d9ee450a..7530c9c0 100644 --- a/ui/toolbar.glade +++ b/ui/toolbar.glade @@ -173,6 +173,27 @@ True + + + True + False + + + True + True + + 35 + edit-find-symbolic + edit-clear-symbolic + False + + + + + False + True + + False