Search UI
This commit is contained in:
parent
94b133dad1
commit
b3136b7d95
20 changed files with 478 additions and 57 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
54
src/client/folder-list/folder-list-search-branch.vala
Normal file
54
src/client/folder-list/folder-list-search-branch.vala
Normal file
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<Geary.Account, AccountBranch> account_branches
|
||||
= new Gee.HashMap<Geary.Account, AccountBranch>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -463,5 +463,19 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
|
|||
public Gee.List<ComposerWindow>? 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Geary.EmailIdentifier>? local_search_async(string keywords,
|
||||
Gee.Collection<Geary.FolderPath?>? folder_blacklist = null,
|
||||
Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws Error;
|
||||
|
||||
public virtual string to_string() {
|
||||
return name;
|
||||
}
|
||||
|
|
|
|||
47
src/engine/abstract/geary-abstract-local-folder.vala
Normal file
47
src/engine/abstract/geary-abstract-local-folder.vala
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<Geary.EmailIdentifier>? local_search_async(string keywords,
|
||||
Gee.Collection<Geary.FolderPath?>? folder_blacklist = null,
|
||||
Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws Error;
|
||||
|
||||
/**
|
||||
* Used only for debugging. Should not be used for user-visible strings.
|
||||
*/
|
||||
|
|
|
|||
134
src/engine/api/geary-search-folder.vala
Normal file
134
src/engine/api/geary-search-folder.vala
Normal file
|
|
@ -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<Geary.FolderPath> exclude_folders = new Gee.HashSet<Geary.FolderPath>();
|
||||
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<Geary.EmailIdentifier>? 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<Geary.Email>? 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<Geary.Email>? 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<Geary.Email>? list_email_by_sparse_id_async(
|
||||
Gee.Collection<Geary.EmailIdentifier> 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<Geary.EmailIdentifier, Geary.Email.Field>? list_local_email_fields_async(
|
||||
Gee.Collection<Geary.EmailIdentifier> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<OutboxRow> outbox_queue = new Nonblocking.Mailbox<OutboxRow>();
|
||||
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<SmtpOutboxEmailIdentifier> list = new Gee.ArrayList<SmtpOutboxEmailIdentifier>();
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Geary.Folder> list_folders() throws Error {
|
||||
check_open();
|
||||
Gee.HashSet<Geary.Folder> all_folders = new Gee.HashSet<Geary.Folder>();
|
||||
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<Geary.EmailIdentifier>? local_search_async(string keywords,
|
||||
Gee.Collection<Geary.FolderPath?>? folder_blacklist = null,
|
||||
Gee.Collection<Geary.EmailIdentifier>? 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,6 +173,27 @@
|
|||
<property name="expand">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolItem" id="search_container">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="search_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="invisible_char">•</property>
|
||||
<property name="width_chars">35</property>
|
||||
<property name="primary_icon_name">edit-find-symbolic</property>
|
||||
<property name="secondary_icon_name">edit-clear-symbolic</property>
|
||||
<property name="primary_icon_sensitive">False</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleToolButton" id="GearyGearMenuButton">
|
||||
<property name="use_action_appearance">False</property>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue