Search UI

This commit is contained in:
Eric Gregory 2013-05-14 11:52:02 -07:00
parent 94b133dad1
commit b3136b7d95
20 changed files with 478 additions and 57 deletions

View file

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

View file

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

View file

@ -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();
}
}

View file

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

View file

@ -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();
}

View 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());
}
}

View file

@ -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;
}
}
}

View file

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

View file

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

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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;
}

View 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);
}
}

View file

@ -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.
*/

View 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());
}
}

View file

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

View file

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

View file

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

View file

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

View file

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