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
client/composer/composer-window.vala
client/composer/contact-entry-completion.vala
client/composer/contact-list-store.vala
client/composer/email-entry.vala
client/composer/webview-edit-fixer.vala
......@@ -323,6 +324,7 @@ client/notification/unity-launcher.vala
client/sidebar/sidebar-branch.vala
client/sidebar/sidebar-common.vala
client/sidebar/sidebar-count-cell-renderer.vala
client/sidebar/sidebar-entry.vala
client/sidebar/sidebar-tree.vala
......
......@@ -129,6 +129,8 @@ public class ComposerWindow : Gtk.Window {
public ComposeType compose_type { get; private set; default = ComposeType.NEW_MESSAGE; }
private ContactListStore? contact_list_store = null;
private string? body_html = null;
private Gee.Set<File> attachment_files = new Gee.HashSet<File>(Geary.Files.nullable_hash,
Geary.Files.nullable_equal);
......@@ -1492,10 +1494,14 @@ public class ComposerWindow : Gtk.Window {
}
private void set_entry_completions() {
Geary.ContactStore contact_store = account.get_contact_store();
to_entry.completion = new ContactEntryCompletion(contact_store);
cc_entry.completion = new ContactEntryCompletion(contact_store);
bcc_entry.completion = new ContactEntryCompletion(contact_store);
if (contact_list_store != null && contact_list_store.contact_store == account.get_contact_store())
return;
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 @@
*/
public class ContactEntryCompletion : Gtk.EntryCompletion {
// Sort column indices.
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 ContactListStore list_store;
private Gtk.TreeIter? last_iter = null;
private 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 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);
public ContactEntryCompletion(ContactListStore list_store) {
this.list_store = list_store;
model = list_store;
set_match_func(completion_match_func);
Gtk.CellRendererText text_renderer = new Gtk.CellRendererText();
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);
match_selected.connect(on_match_selected);
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) {
string? full_address = get_full_address(iter);
if (full_address == null)
return false;
string full_address = list_store.get_full_address(iter);
Gtk.Entry? entry = sender.get_entry() as Gtk.Entry;
if (entry == null)
......@@ -137,24 +65,13 @@ public class ContactEntryCompletion : Gtk.EntryCompletion {
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) {
// We don't use the provided key, because the user can enter multiple addresses.
int current_address_index;
string 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)
return false;
......@@ -162,18 +79,7 @@ public class ContactEntryCompletion : Gtk.EntryCompletion {
if (!match_prefix_contact(current_address_key, contact, out highlighted_result))
return false;
// 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.
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);
}
list_store.set_highlighted_result(iter, highlighted_result, current_address_key);
return true;
}
......@@ -258,14 +164,15 @@ public class ContactEntryCompletion : Gtk.EntryCompletion {
private bool match_prefix_string(string needle, string? haystack = null,
out string highlighted_result = null) {
bool matched = false;
highlighted_result = "";
if (haystack == null)
if (Geary.String.is_empty(haystack) || Geary.String.is_empty(needle))
return false;
// Default result if there is no match or we encounter an error.
highlighted_result = haystack;
bool matched = false;
try {
string escaped_needle = Regex.escape_string(needle.normalize());
Regex regex = new Regex("\\b" + escaped_needle, RegexCompileFlags.CASELESS);
......@@ -279,6 +186,7 @@ public class ContactEntryCompletion : Gtk.EntryCompletion {
highlighted_result = Markup.escape_text(highlighted_result)
.replace("&#x91;", "<b>").replace("&#x92;", "</b>");
return matched;
}
......@@ -291,43 +199,5 @@ public class ContactEntryCompletion : Gtk.EntryCompletion {
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
public abstract Icon? get_sidebar_icon();
public abstract int get_count();
public virtual string to_string() {
return "AbstractFolderEntry: " + get_sidebar_name();
}
......
......@@ -22,10 +22,7 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In
}
public override string get_sidebar_name() {
return (folder.properties.email_unread == 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(), folder.properties.email_unread));
return folder.get_display_name();
}
public override string? get_sidebar_tooltip() {
......@@ -103,7 +100,11 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In
}
private void on_email_unread_count_changed() {
sidebar_name_changed(get_sidebar_name());
sidebar_count_changed(get_count());
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 {
}
public override string get_sidebar_name() {
return (folder.properties.email_unread == 0 ? 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));
return folder.account.information.nickname;
}
public Geary.AccountInformation get_account_information() {
......
......@@ -59,5 +59,9 @@ public class FolderList.SearchEntry : FolderList.AbstractFolderEntry {
private void on_email_total_changed() {
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,
return closed_icon;
}
public int get_count() {
return -1;
}
public string to_string() {
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);
layout_num.set_markup(unread_string, -1);
Pango.Rectangle? ink_rect;
Pango.Rectangle? logical_rect;
layout_num.get_pixel_extents(out ink_rect, out logical_rect);
if (ctx != null && cell_area != null) {
// Compute x and y locations to right-align and vertically center the count.
int x = cell_area.x + (cell_area.width - logical_rect.width) - HORIZONTAL_MARGIN;
int y = cell_area.y + ((cell_area.height - logical_rect.height) / 2);
ctx.move_to(x, y);
Pango.cairo_show_layout(ctx, layout_num);
}
return ink_rect.width + (HORIZONTAL_MARGIN * 2);
}
// This is implemented because it's required; ignore it and look at get_preferred_width() instead.
public override void get_size(Gtk.Widget widget, Gdk.Rectangle? cell_area, out int x_offset,
out int y_offset, out int width, out int height) {
// Set values to avoid compiler warning.
x_offset = 0;
y_offset = 0;
width = 0;
height = 0;
}
}
......@@ -11,12 +11,16 @@ public interface Sidebar.Entry : Object {
public signal void sidebar_icon_changed(Icon? icon);
public signal void sidebar_count_changed(int count);
public abstract string get_sidebar_name();
public abstract string? get_sidebar_tooltip();
public abstract Icon? get_sidebar_icon();
public abstract int get_count();
public abstract string to_string();
internal virtual void grafted(Sidebar.Tree tree) {
......
......@@ -51,6 +51,7 @@ public class Sidebar.Tree : Gtk.TreeView {
PIXBUF,
CLOSED_PIXBUF,
OPEN_PIXBUF,
COUNTER,
N_COLUMNS
}
......@@ -60,7 +61,8 @@ public class Sidebar.Tree : Gtk.TreeView {
typeof (EntryWrapper), // WRAPPER
typeof (Gdk.Pixbuf?), // PIXBUF
typeof (Gdk.Pixbuf?), // CLOSED_PIXBUF
typeof (Gdk.Pixbuf?) // OPEN_PIXBUF
typeof (Gdk.Pixbuf?), // OPEN_PIXBUF
typeof (int) // COUNTER
);
private Gtk.IconTheme? icon_theme;
......@@ -98,7 +100,7 @@ public class Sidebar.Tree : Gtk.TreeView {
get_style_context().add_class("sidebar");
Gtk.TreeViewColumn text_column = new Gtk.TreeViewColumn();
text_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED);
text_column.set_expand(true);
Gtk.CellRendererPixbuf icon_renderer = new Gtk.CellRendererPixbuf();
text_column.pack_start(icon_renderer, false);
text_column.add_attribute(icon_renderer, "pixbuf", Columns.PIXBUF);
......@@ -112,6 +114,13 @@ public class Sidebar.Tree : Gtk.TreeView {
text_column.add_attribute(text_renderer, "markup", Columns.NAME);
append_column(text_column);
// Count column.
Gtk.TreeViewColumn count_column = new Gtk.TreeViewColumn();
SidebarCountCellRenderer unread_renderer = new SidebarCountCellRenderer();
count_column.pack_start(unread_renderer, false);
count_column.add_attribute(unread_renderer, "counter", Columns.COUNTER);
append_column(count_column);
set_headers_visible(false);
set_enable_search(false);
set_search_column(-1);
......@@ -170,6 +179,14 @@ public class Sidebar.Tree : Gtk.TreeView {
renderer.visible = !(wrapper.entry is Sidebar.Header);
}
public void counter_renderer_function(Gtk.CellLayout layout, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) {
EntryWrapper? wrapper = get_wrapper_at_iter(iter);
if (wrapper == null) {
return;
}
renderer.visible = !(wrapper.entry is Sidebar.Header);