Commit d531c7ee authored by Jim Nelson's avatar Jim Nelson

Merge branch 'master' into feature/attachments

parents 09dd5587 851f8f03
...@@ -294,6 +294,7 @@ client/accounts/login-dialog.vala ...@@ -294,6 +294,7 @@ client/accounts/login-dialog.vala
client/composer/composer-window.vala client/composer/composer-window.vala
client/composer/contact-entry-completion.vala client/composer/contact-entry-completion.vala
client/composer/contact-list-store.vala
client/composer/email-entry.vala client/composer/email-entry.vala
client/composer/webview-edit-fixer.vala client/composer/webview-edit-fixer.vala
...@@ -323,6 +324,7 @@ client/notification/unity-launcher.vala ...@@ -323,6 +324,7 @@ client/notification/unity-launcher.vala
client/sidebar/sidebar-branch.vala client/sidebar/sidebar-branch.vala
client/sidebar/sidebar-common.vala client/sidebar/sidebar-common.vala
client/sidebar/sidebar-count-cell-renderer.vala
client/sidebar/sidebar-entry.vala client/sidebar/sidebar-entry.vala
client/sidebar/sidebar-tree.vala client/sidebar/sidebar-tree.vala
......
...@@ -129,6 +129,8 @@ public class ComposerWindow : Gtk.Window { ...@@ -129,6 +129,8 @@ public class ComposerWindow : Gtk.Window {
public ComposeType compose_type { get; private set; default = ComposeType.NEW_MESSAGE; } public ComposeType compose_type { get; private set; default = ComposeType.NEW_MESSAGE; }
private ContactListStore? contact_list_store = null;
private string? body_html = null; private string? body_html = null;
private Gee.Set<File> attachment_files = new Gee.HashSet<File>(Geary.Files.nullable_hash, private Gee.Set<File> attachment_files = new Gee.HashSet<File>(Geary.Files.nullable_hash,
Geary.Files.nullable_equal); Geary.Files.nullable_equal);
...@@ -1492,10 +1494,14 @@ public class ComposerWindow : Gtk.Window { ...@@ -1492,10 +1494,14 @@ public class ComposerWindow : Gtk.Window {
} }
private void set_entry_completions() { private void set_entry_completions() {
Geary.ContactStore contact_store = account.get_contact_store(); if (contact_list_store != null && contact_list_store.contact_store == account.get_contact_store())
to_entry.completion = new ContactEntryCompletion(contact_store); return;
cc_entry.completion = new ContactEntryCompletion(contact_store);
bcc_entry.completion = new ContactEntryCompletion(contact_store); contact_list_store = new ContactListStore(account.get_contact_store());
to_entry.completion = new ContactEntryCompletion(contact_list_store);
cc_entry.completion = new ContactEntryCompletion(contact_list_store);
bcc_entry.completion = new ContactEntryCompletion(contact_list_store);
} }
} }
...@@ -5,98 +5,26 @@ ...@@ -5,98 +5,26 @@
*/ */
public class ContactEntryCompletion : Gtk.EntryCompletion { public class ContactEntryCompletion : Gtk.EntryCompletion {
// Sort column indices. private ContactListStore list_store;
private const int SORT_COLUMN = 0;
// Minimum visibility for the contact to appear in autocompletion.
private const Geary.ContactImportance CONTACT_VISIBILITY_THRESHOLD = Geary.ContactImportance.TO_TO;
private Gtk.ListStore list_store;
private Gtk.TreeIter? last_iter = null; private Gtk.TreeIter? last_iter = null;
private enum Column { public ContactEntryCompletion(ContactListStore list_store) {
CONTACT_OBJECT, this.list_store = list_store;
CONTACT_MARKUP_NAME,
LAST_KEY;
public static Type[] get_types() {
return {
typeof (Geary.Contact), // CONTACT_OBJECT
typeof (string), // CONTACT_MARKUP_NAME
typeof (string) // LAST_KEY
};
}
}
public ContactEntryCompletion(Geary.ContactStore? contact_store) {
list_store = new Gtk.ListStore.newv(Column.get_types());
list_store.set_sort_func(SORT_COLUMN, sort_func);
list_store.set_sort_column_id(SORT_COLUMN, Gtk.SortType.ASCENDING);
if (contact_store == null)
return;
foreach (Geary.Contact contact in contact_store.contacts)
add_contact(contact);
contact_store.contact_added.connect(on_contact_added);
contact_store.contact_updated.connect(on_contact_updated);
model = list_store; model = list_store;
set_match_func(completion_match_func); set_match_func(completion_match_func);
Gtk.CellRendererText text_renderer = new Gtk.CellRendererText(); Gtk.CellRendererText text_renderer = new Gtk.CellRendererText();
pack_start(text_renderer, true); pack_start(text_renderer, true);
add_attribute(text_renderer, "markup", Column.CONTACT_MARKUP_NAME); add_attribute(text_renderer, "markup", ContactListStore.Column.CONTACT_MARKUP_NAME);
set_inline_selection(true); set_inline_selection(true);
match_selected.connect(on_match_selected); match_selected.connect(on_match_selected);
cursor_on_match.connect(on_cursor_on_match); cursor_on_match.connect(on_cursor_on_match);
} }
private void add_contact(Geary.Contact contact) {
if (contact.highest_importance < CONTACT_VISIBILITY_THRESHOLD)
return;
string full_address = contact.get_rfc822_address().get_full_address();
Gtk.TreeIter iter;
list_store.append(out iter);
list_store.set(iter,
Column.CONTACT_OBJECT, contact,
Column.CONTACT_MARKUP_NAME, Markup.escape_text(full_address),
Column.LAST_KEY, "");
}
private void update_contact(Geary.Contact updated_contact) {
Gtk.TreeIter iter;
if (!list_store.get_iter_first(out iter))
return;
do {
if (get_contact(iter) != updated_contact)
continue;
Gtk.TreePath? path = list_store.get_path(iter);
if (path != null)
list_store.row_changed(path, iter);
return;
} while (list_store.iter_next(ref iter));
}
private void on_contact_added(Geary.Contact contact) {
add_contact(contact);
}
private void on_contact_updated(Geary.Contact contact) {
update_contact(contact);
}
private bool on_match_selected(Gtk.EntryCompletion sender, Gtk.TreeModel model, Gtk.TreeIter iter) { private bool on_match_selected(Gtk.EntryCompletion sender, Gtk.TreeModel model, Gtk.TreeIter iter) {
string? full_address = get_full_address(iter); string full_address = list_store.get_full_address(iter);
if (full_address == null)
return false;
Gtk.Entry? entry = sender.get_entry() as Gtk.Entry; Gtk.Entry? entry = sender.get_entry() as Gtk.Entry;
if (entry == null) if (entry == null)
...@@ -137,43 +65,21 @@ public class ContactEntryCompletion : Gtk.EntryCompletion { ...@@ -137,43 +65,21 @@ public class ContactEntryCompletion : Gtk.EntryCompletion {
last_iter = null; last_iter = null;
} }
private Geary.Contact? get_contact(Gtk.TreeIter iter) {
GLib.Value contact_value;
list_store.get_value(iter, Column.CONTACT_OBJECT, out contact_value);
return contact_value.get_object() as Geary.Contact;
}
private string? get_full_address(Gtk.TreeIter iter) {
Geary.Contact? contact = get_contact(iter);
return contact == null ? null : contact.get_rfc822_address().to_rfc822_string();
}
private bool completion_match_func(Gtk.EntryCompletion completion, string key, Gtk.TreeIter iter) { private bool completion_match_func(Gtk.EntryCompletion completion, string key, Gtk.TreeIter iter) {
// We don't use the provided key, because the user can enter multiple addresses. // We don't use the provided key, because the user can enter multiple addresses.
int current_address_index; int current_address_index;
string current_address_key; string current_address_key;
get_addresses(completion, out current_address_index, out current_address_key); get_addresses(completion, out current_address_index, out current_address_key);
Geary.Contact? contact = get_contact(iter); Geary.Contact? contact = list_store.get_contact(iter);
if (contact == null) if (contact == null)
return false; return false;
string highlighted_result; string highlighted_result;
if (!match_prefix_contact(current_address_key, contact, out highlighted_result)) if (!match_prefix_contact(current_address_key, contact, out highlighted_result))
return false; return false;
// Changing a row in the list store causes Gtk.EntryCompletion to re-evaluate list_store.set_highlighted_result(iter, highlighted_result, current_address_key);
// completion_match_func for that row. Thus we need to make sure the key has
// actually changed before settings the highlighting--otherwise we will cause
// an infinite loop.
GLib.Value last_key_value;
list_store.get_value(iter, Column.LAST_KEY, out last_key_value);
string? last_key = last_key_value.get_string();
if (current_address_key != last_key) {
list_store.set(iter,
Column.CONTACT_MARKUP_NAME, highlighted_result,
Column.LAST_KEY, current_address_key, -1);
}
return true; return true;
} }
...@@ -258,14 +164,15 @@ public class ContactEntryCompletion : Gtk.EntryCompletion { ...@@ -258,14 +164,15 @@ public class ContactEntryCompletion : Gtk.EntryCompletion {
private bool match_prefix_string(string needle, string? haystack = null, private bool match_prefix_string(string needle, string? haystack = null,
out string highlighted_result = null) { out string highlighted_result = null) {
bool matched = false;
highlighted_result = ""; highlighted_result = "";
if (haystack == null)
if (Geary.String.is_empty(haystack) || Geary.String.is_empty(needle))
return false; return false;
// Default result if there is no match or we encounter an error. // Default result if there is no match or we encounter an error.
highlighted_result = haystack; highlighted_result = haystack;
bool matched = false;
try { try {
string escaped_needle = Regex.escape_string(needle.normalize()); string escaped_needle = Regex.escape_string(needle.normalize());
Regex regex = new Regex("\\b" + escaped_needle, RegexCompileFlags.CASELESS); Regex regex = new Regex("\\b" + escaped_needle, RegexCompileFlags.CASELESS);
...@@ -279,6 +186,7 @@ public class ContactEntryCompletion : Gtk.EntryCompletion { ...@@ -279,6 +186,7 @@ public class ContactEntryCompletion : Gtk.EntryCompletion {
highlighted_result = Markup.escape_text(highlighted_result) highlighted_result = Markup.escape_text(highlighted_result)
.replace("&#x91;", "<b>").replace("&#x92;", "</b>"); .replace("&#x91;", "<b>").replace("&#x92;", "</b>");
return matched; return matched;
} }
...@@ -291,43 +199,5 @@ public class ContactEntryCompletion : Gtk.EntryCompletion { ...@@ -291,43 +199,5 @@ public class ContactEntryCompletion : Gtk.EntryCompletion {
return false; return false;
} }
private int sort_func(Gtk.TreeModel model, Gtk.TreeIter aiter, Gtk.TreeIter biter) {
// Order by importance, then by real name, then by email.
GLib.Value avalue, bvalue;
model.get_value(aiter, Column.CONTACT_OBJECT, out avalue);
model.get_value(biter, Column.CONTACT_OBJECT, out bvalue);
Geary.Contact? acontact = avalue.get_object() as Geary.Contact;
Geary.Contact? bcontact = bvalue.get_object() as Geary.Contact;
// Contacts can be null if the sort func is called between TreeModel.append and
// TreeModel.set.
if (acontact == bcontact)
return 0;
if (acontact == null && bcontact != null)
return -1;
if (acontact != null && bcontact == null)
return 1;
// First order by importance.
if (acontact.highest_importance > bcontact.highest_importance)
return -1;
if (acontact.highest_importance < bcontact.highest_importance)
return 1;
// Then order by real name.
string? anormalized_real_name = acontact.real_name == null ? null :
acontact.real_name.normalize().casefold();
string? bnormalized_real_name = bcontact.real_name == null ? null :
bcontact.real_name.normalize().casefold();
// strcmp correctly marks 'null' as first in lexigraphic order, so we don't need to
// special-case it.
int result = strcmp(anormalized_real_name, bnormalized_real_name);
if (result != 0)
return result;
// Finally, order by email.
return strcmp(acontact.normalized_email, bcontact.normalized_email);
}
} }
/* Copyright 2013 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class ContactListStore : Gtk.ListStore {
// Minimum visibility for the contact to appear in autocompletion.
private const Geary.ContactImportance CONTACT_VISIBILITY_THRESHOLD = Geary.ContactImportance.TO_TO;
public enum Column {
CONTACT_OBJECT,
CONTACT_MARKUP_NAME,
LAST_KEY;
public static Type[] get_types() {
return {
typeof (Geary.Contact), // CONTACT_OBJECT
typeof (string), // CONTACT_MARKUP_NAME
typeof (string) // LAST_KEY
};
}
}
public Geary.ContactStore contact_store { get; private set; }
public ContactListStore(Geary.ContactStore contact_store) {
set_column_types(Column.get_types());
this.contact_store = contact_store;
foreach (Geary.Contact contact in contact_store.contacts)
add_contact(contact);
// set sort function *after* adding all the contacts
set_sort_func(Column.CONTACT_OBJECT, sort_func);
set_sort_column_id(Column.CONTACT_OBJECT, Gtk.SortType.ASCENDING);
contact_store.contact_added.connect(on_contact_added);
contact_store.contact_updated.connect(on_contact_updated);
}
~ContactListStore() {
contact_store.contact_added.disconnect(on_contact_added);
contact_store.contact_updated.disconnect(on_contact_updated);
}
public Geary.Contact get_contact(Gtk.TreeIter iter) {
GLib.Value contact_value;
get_value(iter, Column.CONTACT_OBJECT, out contact_value);
return (Geary.Contact) contact_value.get_object();
}
public string get_full_address(Gtk.TreeIter iter) {
return get_contact(iter).get_rfc822_address().get_full_address();
}
// Highlighted result should be Markup.escaped for presentation to the user
public void set_highlighted_result(Gtk.TreeIter iter, string highlighted_result,
string current_address_key) {
// get the last key for this row for comparison
GLib.Value last_key_value;
get_value(iter, Column.LAST_KEY, out last_key_value);
string? last_key = last_key_value.get_string();
// Changing a row in the list store causes Gtk.EntryCompletion to re-evaluate
// completion_match_func for that row. Thus we need to make sure the key has
// actually changed before settings the highlighting--otherwise we will cause
// an infinite loop.
if (current_address_key != last_key) {
set(iter,
Column.CONTACT_MARKUP_NAME, highlighted_result,
Column.LAST_KEY, current_address_key, -1);
}
}
private void add_contact(Geary.Contact contact) {
if (contact.highest_importance < CONTACT_VISIBILITY_THRESHOLD)
return;
string full_address = contact.get_rfc822_address().get_full_address();
Gtk.TreeIter iter;
append(out iter);
set(iter,
Column.CONTACT_OBJECT, contact,
Column.CONTACT_MARKUP_NAME, Markup.escape_text(full_address),
Column.LAST_KEY, "");
}
private void update_contact(Geary.Contact updated_contact) {
Gtk.TreeIter iter;
if (!get_iter_first(out iter))
return;
do {
if (get_contact(iter) != updated_contact)
continue;
Gtk.TreePath? path = get_path(iter);
if (path != null)
row_changed(path, iter);
return;
} while (iter_next(ref iter));
}
private void on_contact_added(Geary.Contact contact) {
add_contact(contact);
}
private void on_contact_updated(Geary.Contact contact) {
update_contact(contact);
}
private int sort_func(Gtk.TreeModel model, Gtk.TreeIter aiter, Gtk.TreeIter biter) {
// Order by importance, then by real name, then by email.
GLib.Value avalue, bvalue;
model.get_value(aiter, Column.CONTACT_OBJECT, out avalue);
model.get_value(biter, Column.CONTACT_OBJECT, out bvalue);
Geary.Contact? acontact = avalue.get_object() as Geary.Contact;
Geary.Contact? bcontact = bvalue.get_object() as Geary.Contact;
// Contacts can be null if the sort func is called between TreeModel.append and
// TreeModel.set.
if (acontact == bcontact)
return 0;
if (acontact == null && bcontact != null)
return -1;
if (acontact != null && bcontact == null)
return 1;
// First order by importance.
if (acontact.highest_importance > bcontact.highest_importance)
return -1;
if (acontact.highest_importance < bcontact.highest_importance)
return 1;
// Then order by real name.
string? anormalized_real_name = acontact.real_name == null ? null :
acontact.real_name.normalize().casefold();
string? bnormalized_real_name = bcontact.real_name == null ? null :
bcontact.real_name.normalize().casefold();
// strcmp correctly marks 'null' as first in lexigraphic order, so we don't need to
// special-case it.
int result = strcmp(anormalized_real_name, bnormalized_real_name);
if (result != 0)
return result;
// Finally, order by email.
return strcmp(acontact.normalized_email, bcontact.normalized_email);
}
}
...@@ -22,6 +22,8 @@ public abstract class FolderList.AbstractFolderEntry : Geary.BaseObject, Sidebar ...@@ -22,6 +22,8 @@ public abstract class FolderList.AbstractFolderEntry : Geary.BaseObject, Sidebar
public abstract Icon? get_sidebar_icon(); public abstract Icon? get_sidebar_icon();
public abstract int get_count();
public virtual string to_string() { public virtual string to_string() {
return "AbstractFolderEntry: " + get_sidebar_name(); return "AbstractFolderEntry: " + get_sidebar_name();
} }
......
...@@ -22,10 +22,7 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In ...@@ -22,10 +22,7 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In
} }
public override string get_sidebar_name() { public override string get_sidebar_name() {
return (folder.properties.email_unread == 0 ? folder.get_display_name() : return 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(), folder.properties.email_unread));
} }
public override string? get_sidebar_tooltip() { public override string? get_sidebar_tooltip() {
...@@ -103,7 +100,11 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In ...@@ -103,7 +100,11 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In
} }
private void on_email_unread_count_changed() { private void on_email_unread_count_changed() {
sidebar_name_changed(get_sidebar_name()); sidebar_count_changed(get_count());
sidebar_tooltip_changed(get_sidebar_tooltip()); sidebar_tooltip_changed(get_sidebar_tooltip());
} }
public override int get_count() {
return folder.properties.email_unread;
}
} }
...@@ -16,10 +16,7 @@ public class FolderList.InboxFolderEntry : FolderList.FolderEntry { ...@@ -16,10 +16,7 @@ public class FolderList.InboxFolderEntry : FolderList.FolderEntry {
} }
public override string get_sidebar_name() { public override string get_sidebar_name() {
return (folder.properties.email_unread == 0 ? folder.account.information.nickname : return folder.account.information.nickname;
/// This string gets the account nickname and the unread messages count,
/// e.g. Work (5).
_("%s (%d)").printf(folder.account.information.nickname, folder.properties.email_unread));
} }
public Geary.AccountInformation get_account_information() { public Geary.AccountInformation get_account_information() {
......
...@@ -59,5 +59,9 @@ public class FolderList.SearchEntry : FolderList.AbstractFolderEntry { ...@@ -59,5 +59,9 @@ public class FolderList.SearchEntry : FolderList.AbstractFolderEntry {
private void on_email_total_changed() { private void on_email_total_changed() {
sidebar_tooltip_changed(get_sidebar_tooltip()); sidebar_tooltip_changed(get_sidebar_tooltip());
} }
public override int get_count() {
return 0;
}
} }
...@@ -49,6 +49,10 @@ public class Sidebar.Grouping : Object, Sidebar.Entry, Sidebar.ExpandableEntry, ...@@ -49,6 +49,10 @@ public class Sidebar.Grouping : Object, Sidebar.Entry, Sidebar.ExpandableEntry,
return closed_icon; return closed_icon;
} }
public int get_count() {
return -1;
}
public string to_string() { public string to_string() {
return name; return name;
} }
......
/* Copyright 2013 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Cell renderer for counter in sidebar.
*/
public class SidebarCountCellRenderer : Gtk.CellRenderer {
private const int HORIZONTAL_MARGIN = 4;
public int counter { get; set; }
public SidebarCountCellRenderer() {
}
public override Gtk.SizeRequestMode get_request_mode() {
return Gtk.SizeRequestMode.WIDTH_FOR_HEIGHT;
}
public override void get_preferred_width(Gtk.Widget widget, out int minimum_size, out int natural_size) {
minimum_size = render_counter(widget, null, null, false); // Calculate width.
natural_size = minimum_size;
}
public override void render(Cairo.Context ctx, Gtk.Widget widget, Gdk.Rectangle background_area,
Gdk.Rectangle cell_area, Gtk.CellRendererState flags) {
render_counter(widget, cell_area, ctx, false);
}
// Renders the counter. Returns its own width.
private int render_counter(Gtk.Widget widget, Gdk.Rectangle? cell_area, Cairo.Context? ctx,
bool selected) {
if (counter < 1)
return 0;
string unread_string =
"<span background='#888888' foreground='white' font='%d' weight='bold'> %d </span>"
.printf(8, counter);
Pango.Layout layout_num = widget.create_pango_layout(null);