Move special folders to the top of the tree, put other folders in container, and start in Inbox: #3707 and #3692

This commit introduces the idea of a Personality, customizations for particular IMAP servers and services to allow the interface to configure itself for more natural use.  Also, this commit has the app start in the Inbox, and an optimization was added that makes showing what's in the Inbox (at least, what's in the local cache) come up instantly.
This commit is contained in:
Jim Nelson 2011-07-01 19:33:35 -07:00
parent 009a061a16
commit fff39e8a44
13 changed files with 363 additions and 34 deletions

View file

@ -12,9 +12,17 @@ public enum TreeSortable {
}
public class FolderListStore : Gtk.TreeStore {
private enum Internal {
SPECIAL_FOLDER,
USER_FOLDER_ROOT,
USER_FOLDER
}
public enum Column {
NAME,
FOLDER_OBJECT,
INTERNAL,
ORDERING,
N_COLUMNS;
public static Column[] all() {
@ -27,7 +35,9 @@ public class FolderListStore : Gtk.TreeStore {
public static Type[] get_types() {
return {
typeof (string),
typeof (Geary.Folder)
typeof (Geary.Folder),
typeof (Internal),
typeof (int)
};
}
@ -36,11 +46,8 @@ public class FolderListStore : Gtk.TreeStore {
case NAME:
return _("Name");
case FOLDER_OBJECT:
return "(hidden)";
default:
assert_not_reached();
return "(hidden)";
}
}
}
@ -49,19 +56,55 @@ public class FolderListStore : Gtk.TreeStore {
set_column_types(Column.get_types());
set_default_sort_func(sort_by_name);
set_sort_column_id(TreeSortable.DEFAULT_SORT_COLUMN_ID, Gtk.SortType.ASCENDING);
// add user folder root
Gtk.TreeIter iter;
append(out iter, null);
set(iter,
Column.NAME, _("Folders"),
Column.INTERNAL, Internal.USER_FOLDER_ROOT
);
}
public void add_folder(Geary.Folder folder) {
public void set_user_folders_root_name(string name) {
Gtk.TreeIter? iter = get_user_folders_iter();
if (iter == null)
return;
set(iter,
Column.NAME, name
);
}
public void add_special_folder(Geary.SpecialFolder special, Geary.Folder folder) {
Gtk.TreeIter iter;
append(out iter, null);
set(iter,
Column.NAME, special.name,
Column.FOLDER_OBJECT, folder,
Column.INTERNAL, Internal.SPECIAL_FOLDER,
Column.ORDERING, special.ordering
);
}
public void add_user_folder(Geary.Folder folder) {
Gtk.TreeIter? user_folders_root_iter = get_user_folders_iter();
if (user_folders_root_iter == null)
return;
Gtk.TreeIter? parent_iter = !folder.get_path().is_root()
? find_path(folder.get_path().get_parent())
: null;
? find_path(folder.get_path().get_parent(), user_folders_root_iter)
: user_folders_root_iter;
Gtk.TreeIter iter;
append(out iter, parent_iter);
set(iter,
Column.NAME, folder.get_path().basename,
Column.FOLDER_OBJECT, folder
Column.FOLDER_OBJECT, folder,
Column.INTERNAL, Internal.USER_FOLDER
);
}
@ -76,8 +119,25 @@ public class FolderListStore : Gtk.TreeStore {
return folder;
}
private Gtk.TreeIter? get_user_folders_iter() {
Gtk.TreeIter iter;
get_iter_first(out iter);
do {
Internal internl;
get(iter, Column.INTERNAL, out internl);
if (internl == Internal.USER_FOLDER_ROOT)
return iter;
} while (iter_next(ref iter));
debug("Unable to locate user folders root");
return null;
}
// TODO: This could be replaced with a binary search
private Gtk.TreeIter? find_path(Geary.FolderPath path, Gtk.TreeIter? parent = null) {
public Gtk.TreeIter? find_path(Geary.FolderPath path, Gtk.TreeIter? parent = null) {
Gtk.TreeIter iter;
// no parent, start at the root, otherwise start at the parent's children
if (parent == null) {
@ -107,6 +167,30 @@ public class FolderListStore : Gtk.TreeStore {
}
private int sort_by_name(Gtk.TreeModel model, Gtk.TreeIter aiter, Gtk.TreeIter biter) {
Internal ainternal;
model.get(aiter, Column.INTERNAL, out ainternal);
Internal binternal;
model.get(biter, Column.INTERNAL, out binternal);
// sort special folders in their own magical way
if (ainternal == Internal.SPECIAL_FOLDER && binternal == Internal.SPECIAL_FOLDER) {
int apos;
model.get(aiter, Column.ORDERING, out apos);
int bpos;
model.get(biter, Column.ORDERING, out bpos);
return apos - bpos;
}
// sort the USER_FOLDER_ROOT dead last
if (ainternal == Internal.USER_FOLDER_ROOT)
return 1;
else if (binternal == Internal.USER_FOLDER_ROOT)
return -1;
// sort everything else by name
string aname;
model.get(aiter, Column.NAME, out aname);

View file

@ -22,6 +22,17 @@ public class FolderListView : Gtk.TreeView {
return (FolderListStore) get_model();
}
public void select_path(Geary.FolderPath path) {
Gtk.TreeIter? iter = get_store().find_path(path);
if (iter == null)
return;
Gtk.TreePath tree_path = get_store().get_path(iter);
get_selection().select_path(tree_path);
set_cursor(tree_path, null, false);
}
private void on_selection_changed() {
Gtk.TreeModel model;
Gtk.TreePath? path = get_selection().get_selected_rows(out model).nth_data(0);

View file

@ -65,12 +65,28 @@ public class MainWindow : Gtk.Window {
this.account = account;
account.folders_added_removed.connect(on_folders_added_removed);
folder_list_store.set_user_folders_root_name(account.get_user_folders_label());
do_start.begin();
}
private async void do_start() {
try {
// pull down the root-level folders
// add all the special folders, which are assumed to always exist
Geary.SpecialFolderMap? special_folders = account.get_special_folder_map();
if (special_folders != null) {
foreach (Geary.SpecialFolder special_folder in special_folders.get_all()) {
Geary.Folder folder = yield account.fetch_folder_async(special_folder.path);
folder_list_store.add_special_folder(special_folder, folder);
}
// If inbox is specified, select that
Geary.SpecialFolder? inbox = special_folders.get_folder(Geary.SpecialFolderType.INBOX);
if (inbox != null)
folder_list_view.select_path(inbox.path);
}
// pull down the root-level user folders
Gee.Collection<Geary.Folder> folders = yield account.list_folders_async(null);
if (folders != null)
on_folders_added_removed(folders, null);
@ -253,10 +269,24 @@ public class MainWindow : Gtk.Window {
private void on_folders_added_removed(Gee.Collection<Geary.Folder>? added,
Gee.Collection<Geary.Folder>? removed) {
if (added != null && added.size > 0) {
foreach (Geary.Folder folder in added)
folder_list_store.add_folder(folder);
Gee.Set<Geary.FolderPath>? ignored_paths = account.get_ignored_paths();
search_folders_for_children.begin(added);
Gee.ArrayList<Geary.Folder> skipped = new Gee.ArrayList<Geary.Folder>();
foreach (Geary.Folder folder in added) {
if (ignored_paths != null && ignored_paths.contains(folder.get_path()))
skipped.add(folder);
else
folder_list_store.add_user_folder(folder);
}
Gee.Collection<Geary.Folder> remaining = added;
if (skipped.size > 0) {
remaining = new Gee.ArrayList<Geary.Folder>();
remaining.add_all(added);
remaining.remove_all(skipped);
}
search_folders_for_children.begin(remaining);
}
}

View file

@ -4,9 +4,15 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public abstract class Geary.EngineAccount : Geary.AbstractAccount {
public abstract class Geary.EngineAccount : Geary.AbstractAccount, Geary.Personality {
public EngineAccount(string name) {
base (name);
}
public abstract string get_user_folders_label();
public abstract Geary.SpecialFolderMap? get_special_folder_map();
public abstract Gee.Set<Geary.FolderPath>? get_ignored_paths();
}

View file

@ -56,17 +56,16 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
// wait_for_remote_to_open(), which uses a NonblockingSemaphore to indicate that the remote
// is open (or has failed to open). This allows for early calls to list and fetch emails
// can work out of the local cache until the remote is ready.
RemoteFolder folder = (RemoteFolder) yield remote.fetch_folder_async(local_folder.get_path(),
cancellable);
open_remote_async.begin(folder, readonly, cancellable, on_open_remote_completed);
open_remote_async.begin(readonly, cancellable, on_open_remote_completed);
opened = true;
notify_opened();
}
private async void open_remote_async(RemoteFolder folder, bool readonly, Cancellable? cancellable)
throws Error {
private async void open_remote_async(bool readonly, Cancellable? cancellable) throws Error {
RemoteFolder folder = (RemoteFolder) yield remote.fetch_folder_async(local_folder.get_path(),
cancellable);
yield folder.open_async(readonly, cancellable);
remote_folder = folder;

View file

@ -15,7 +15,7 @@ public class Geary.Engine {
}
// Only Gmail today
return new GenericImapAccount(
return new GmailAccount(
"Gmail account %s".printf(cred.to_string()),
new Geary.Imap.Account(cred, Imap.ClientConnection.DEFAULT_PORT_TLS),
new Geary.Sqlite.Account(cred));

View file

@ -5,7 +5,7 @@
*/
private class Geary.GenericImapAccount : Geary.EngineAccount {
public const string INBOX = "Inbox";
private SpecialFolderMap? special_folders = null;
private RemoteAccount remote;
private LocalAccount local;
@ -15,6 +15,19 @@ private class Geary.GenericImapAccount : Geary.EngineAccount {
this.remote = remote;
this.local = local;
if (special_folders == null) {
special_folders = new SpecialFolderMap();
special_folders.set_folder(
new SpecialFolder(
SpecialFolderType.INBOX,
_("Inbox"),
new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR, false),
0
)
);
}
}
public override Geary.Email.Field get_required_fields_for_writing() {
@ -117,5 +130,17 @@ private class Geary.GenericImapAccount : Geary.EngineAccount {
if (engine_added != null)
notify_folders_added_removed(engine_added, null);
}
public override string get_user_folders_label() {
return _("Folders");
}
public override Geary.SpecialFolderMap? get_special_folder_map() {
return special_folders;
}
public override Gee.Set<Geary.FolderPath>? get_ignored_paths() {
return null;
}
}

View file

@ -0,0 +1,56 @@
/* Copyright 2011 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.
*/
private class Geary.GmailAccount : Geary.GenericImapAccount {
private const string GMAIL_FOLDER = "[Gmail]";
private static SpecialFolderMap? special_folder_map = null;
private static Gee.Set<Geary.FolderPath>? ignored_paths = null;
public GmailAccount(string name, RemoteAccount remote, LocalAccount local) {
base (name, remote, local);
if (special_folder_map == null || ignored_paths == null)
initialize_personality();
}
private static void initialize_personality() {
Geary.FolderPath gmail_root = new Geary.FolderRoot(GMAIL_FOLDER, Imap.Account.ASSUMED_SEPARATOR,
true);
special_folder_map = new SpecialFolderMap();
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.INBOX, _("Inbox"),
new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR, false), 0));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.DRAFTS, _("Drafts"),
gmail_root.get_child("Drafts"), 1));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.SENT, _("Sent Mail"),
gmail_root.get_child("Sent Mail"), 2));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.FLAGGED, _("Starred"),
gmail_root.get_child("Starred"), 3));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.ALL_MAIL, _("All Mail"),
gmail_root.get_child("All Mail"), 4));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.SPAM, _("Spam"),
gmail_root.get_child("Spam"), 5));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.TRASH, _("Trash"),
gmail_root.get_child("Trash"), 6));
ignored_paths = new Gee.HashSet<Geary.FolderPath>(Hashable.hash_func, Equalable.equal_func);
ignored_paths.add(gmail_root);
}
public override string get_user_folders_label() {
return _("Labels");
}
public override Geary.SpecialFolderMap? get_special_folder_map() {
return special_folder_map;
}
public override Gee.Set<Geary.FolderPath>? get_ignored_paths() {
return ignored_paths.read_only_view;
}
}

View file

@ -0,0 +1,14 @@
/* Copyright 2011 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 interface Geary.Personality : Object {
public abstract string get_user_folders_label();
public abstract Geary.SpecialFolderMap? get_special_folder_map();
public abstract Gee.Set<Geary.FolderPath>? get_ignored_paths();
}

View file

@ -0,0 +1,60 @@
/* Copyright 2011 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 enum Geary.SpecialFolderType {
INBOX,
DRAFTS,
SENT,
FLAGGED,
ALL_MAIL,
SPAM,
TRASH
}
public class Geary.SpecialFolder : Object {
public SpecialFolderType folder_type { get; private set; }
public string name { get; private set; }
public Geary.FolderPath path { get; private set; }
public int ordering { get; private set; }
public SpecialFolder(SpecialFolderType folder_type, string name, FolderPath path, int ordering) {
this.folder_type = folder_type;
this.name = name;
this.path = path;
this.ordering = ordering;
}
}
public class Geary.SpecialFolderMap : Object {
private Gee.HashMap<SpecialFolderType, SpecialFolder> map = new Gee.HashMap<SpecialFolderType,
SpecialFolder>();
private Gee.HashSet<Geary.FolderPath> paths = new Gee.HashSet<Geary.FolderPath>(
Hashable.hash_func, Equalable.equal_func);
public SpecialFolderMap() {
}
public void set_folder(SpecialFolder special_folder) {
map.set(special_folder.folder_type, special_folder);
}
public SpecialFolder? get_folder(SpecialFolderType folder_type) {
return map.get(folder_type);
}
public Gee.Set<SpecialFolderType> get_supported_types() {
return map.keys.read_only_view;
}
public Gee.Collection<SpecialFolder> get_all() {
return map.values.read_only_view;
}
public bool has_path(Geary.FolderPath path) {
return paths.contains(path);
}
}

View file

@ -5,6 +5,11 @@
*/
public class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount {
// all references to Inbox are converted to this string, purely for sanity sake when dealing
// with Inbox's case issues
public const string INBOX_NAME = "INBOX";
public const string ASSUMED_SEPARATOR = "/";
private ClientSessionManager session_mgr;
private Gee.HashMap<string, string?> delims = new Gee.HashMap<string, string?>();
@ -36,11 +41,14 @@ public class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount {
public override async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
Cancellable? cancellable = null) throws Error {
Geary.FolderPath? processed = process_path(parent, null,
(parent != null) ? parent.get_root().default_separator : ASSUMED_SEPARATOR);
Gee.Collection<MailboxInformation> mboxes;
try {
mboxes = (parent == null)
mboxes = (processed == null)
? yield session_mgr.list_roots(cancellable)
: yield session_mgr.list(parent.get_fullpath(), parent.get_root().default_separator,
: yield session_mgr.list(processed.get_fullpath(), processed.get_root().default_separator,
cancellable);
} catch (Error err) {
if (err is ImapError.SERVER_ERROR)
@ -51,14 +59,12 @@ public class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount {
Gee.Collection<Geary.Folder> folders = new Gee.ArrayList<Geary.Folder>();
foreach (MailboxInformation mbox in mboxes) {
if (parent == null)
delims.set(mbox.name, mbox.delim);
Geary.FolderPath path = process_path(processed, mbox.name, mbox.delim);
string basename = mbox.get_path().last();
Geary.FolderPath path = (parent != null)
? parent.get_child(basename)
: new Geary.FolderRoot(basename, mbox.delim, Folder.CASE_SENSITIVE);
// only add to delims map if root-level folder (all sub-folders respect its delimiter)
// also use the processed name, not the one reported off the wire
if (processed == null)
delims.set(path.get_root().basename, mbox.delim);
folders.add(new Geary.Imap.Folder(session_mgr, path, mbox));
}
@ -68,12 +74,17 @@ public class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount {
public override async Geary.Folder fetch_folder_async(Geary.FolderPath path,
Cancellable? cancellable = null) throws Error {
Geary.FolderPath? processed = process_path(path, null, path.get_root().default_separator);
if (processed == null)
throw new ImapError.INVALID_PATH("Invalid path %s", path.to_string());
try {
MailboxInformation? mbox = yield session_mgr.fetch_async(path.get_fullpath(), cancellable);
MailboxInformation? mbox = yield session_mgr.fetch_async(processed.get_fullpath(),
cancellable);
if (mbox == null)
throw_not_found(path);
return new Geary.Imap.Folder(session_mgr, path, mbox);
return new Geary.Imap.Folder(session_mgr, processed, mbox);
} catch (ImapError err) {
if (err is ImapError.SERVER_ERROR)
throw_not_found(path);
@ -87,4 +98,33 @@ public class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount {
throw new EngineError.NOT_FOUND("Folder %s not found on %s",
(path != null) ? path.to_string() : "root", session_mgr.to_string());
}
// This method ensures that Inbox is dealt with in a consistent fashion throughout the
// application.
private static Geary.FolderPath? process_path(Geary.FolderPath? parent, string? basename,
string? delim) throws ImapError {
// 1. Both null, done
if (parent == null && basename == null)
return null;
// 2. Parent null but basename not, create FolderRoot for Inbox
if (parent == null && basename != null && basename.up() == INBOX_NAME)
return new Geary.FolderRoot(INBOX_NAME, delim, false);
// 3. Parent and basename supplied, verify parent is not Inbox, as IMAP does not allow it
// to have children
if (parent != null && basename != null && parent.get_root().basename.up() == INBOX_NAME)
throw new ImapError.INVALID_PATH("Inbox may not have children");
// 4. Default behavior: create child of basename or basename as root, otherwise return parent
// unmodified
if (parent != null && basename != null)
return parent.get_child(basename);
if (basename != null)
return new Geary.FolderRoot(basename, delim, Folder.CASE_SENSITIVE);
return parent;
}
}

View file

@ -12,6 +12,7 @@ public errordomain Geary.ImapError {
COMMAND_FAILED,
UNAUTHENTICATED,
NOT_SUPPORTED,
NOT_SELECTED
NOT_SELECTED,
INVALID_PATH
}

View file

@ -29,8 +29,11 @@ def build(bld):
'../engine/api/geary-folder-properties.vala',
'../engine/api/geary-folder.vala',
'../engine/api/geary-generic-imap-account.vala',
'../engine/api/geary-gmail-account.vala',
'../engine/api/geary-local-interfaces.vala',
'../engine/api/geary-personality.vala',
'../engine/api/geary-remote-interfaces.vala',
'../engine/api/geary-special-folder.vala',
'../engine/common/common-interfaces.vala',
'../engine/common/common-message-data.vala',