Merge branch 'master' into feature/drafts
This commit is contained in:
commit
c8cbebc39c
48 changed files with 760 additions and 386 deletions
2
debian/control
vendored
2
debian/control
vendored
|
|
@ -13,7 +13,7 @@ Build-Depends: debhelper (>= 8),
|
|||
libxml2-dev (>= 2.7.8),
|
||||
libsecret-1-dev (>= 0.11),
|
||||
libgmime-2.6-dev (>= 2.6.0),
|
||||
valac-0.20 (>= 0.20.1),
|
||||
valac-0.22 (>= 0.21.1),
|
||||
cmake (>= 2.8.0),
|
||||
libsqlite3-dev (>= 3.7.4),
|
||||
libmessaging-menu-dev (>= 12.10.2),
|
||||
|
|
|
|||
|
|
@ -295,6 +295,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
|
||||
|
||||
|
|
@ -324,6 +325,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
|
||||
|
||||
|
|
@ -333,6 +335,7 @@ client/ui/main-toolbar.vala
|
|||
client/ui/main-window.vala
|
||||
client/ui/monitored-progress-bar.vala
|
||||
client/ui/monitored-spinner.vala
|
||||
client/ui/stock.vala
|
||||
|
||||
client/util/util-date.vala
|
||||
client/util/util-email.vala
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
public class AccountDialogAddEditPane : AccountDialogPane {
|
||||
public AddEditPage add_edit_page { get; private set; default = new AddEditPage(); }
|
||||
private Gtk.ButtonBox button_box = new Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL);
|
||||
private Gtk.Button ok_button = new Gtk.Button.from_stock(Gtk.Stock.OK);
|
||||
private Gtk.Button cancel_button = new Gtk.Button.from_stock(Gtk.Stock.CANCEL);
|
||||
private Gtk.Button ok_button = new Gtk.Button.with_mnemonic(Stock._OK);
|
||||
private Gtk.Button cancel_button = new Gtk.Button.with_mnemonic(Stock._CANCEL);
|
||||
|
||||
public signal void ok(Geary.AccountInformation info);
|
||||
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ public class LoginDialog : Gtk.Dialog {
|
|||
page.size_changed.connect(() => { resize(1, 1); });
|
||||
page.info_changed.connect(on_info_changed);
|
||||
|
||||
cancel_button = new Gtk.Button.from_stock(Gtk.Stock.CANCEL);
|
||||
cancel_button = new Gtk.Button.from_stock(Stock._CANCEL);
|
||||
add_action_widget(cancel_button, Gtk.ResponseType.CANCEL);
|
||||
ok_button = new Gtk.Button.from_stock(Gtk.Stock.ADD);
|
||||
ok_button = new Gtk.Button.from_stock(Stock._ADD);
|
||||
ok_button.can_default = true;
|
||||
add_action_widget(ok_button, Gtk.ResponseType.OK);
|
||||
set_default_response(Gtk.ResponseType.OK);
|
||||
|
|
|
|||
|
|
@ -127,6 +127,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);
|
||||
|
|
@ -650,7 +652,7 @@ public class ComposerWindow : Gtk.Window {
|
|||
if (editor.can_undo()) {
|
||||
present();
|
||||
ConfirmationDialog dialog = new ConfirmationDialog(this,
|
||||
_("Do you want to discard the unsaved message?"), null, Gtk.Stock.DISCARD);
|
||||
_("Do you want to discard the unsaved message?"), null, Stock._DISCARD);
|
||||
if (dialog.run() != Gtk.ResponseType.OK)
|
||||
return false;
|
||||
}
|
||||
|
|
@ -734,7 +736,7 @@ public class ComposerWindow : Gtk.Window {
|
|||
}
|
||||
if (confirmation != null) {
|
||||
ConfirmationDialog dialog = new ConfirmationDialog(this,
|
||||
confirmation, null, Gtk.Stock.OK);
|
||||
confirmation, null, Stock._OK);
|
||||
if (dialog.run() != Gtk.ResponseType.OK)
|
||||
return false;
|
||||
}
|
||||
|
|
@ -896,7 +898,7 @@ public class ComposerWindow : Gtk.Window {
|
|||
label.halign = Gtk.Align.START;
|
||||
label.xpad = 4;
|
||||
|
||||
Gtk.Button remove_button = new Gtk.Button.from_stock(Gtk.Stock.REMOVE);
|
||||
Gtk.Button remove_button = new Gtk.Button.with_mnemonic(Stock._REMOVE);
|
||||
box.pack_start(remove_button, false, false);
|
||||
remove_button.clicked.connect(() => remove_attachment(attachment_file, box));
|
||||
|
||||
|
|
@ -1209,10 +1211,10 @@ public class ComposerWindow : Gtk.Window {
|
|||
if (selected != null && (selected is WebKit.DOM.HTMLAnchorElement ||
|
||||
selected.get_parent_element() is WebKit.DOM.HTMLAnchorElement)) {
|
||||
existing_link = true;
|
||||
dialog.add_buttons(Gtk.Stock. REMOVE, Gtk.ResponseType.REJECT);
|
||||
dialog.add_buttons(Stock._REMOVE, Gtk.ResponseType.REJECT);
|
||||
}
|
||||
|
||||
dialog.add_buttons(Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL, Gtk.Stock.OK,
|
||||
dialog.add_buttons(Stock._CANCEL, Gtk.ResponseType.CANCEL, Stock._OK,
|
||||
Gtk.ResponseType.OK);
|
||||
|
||||
Gtk.Entry entry = new Gtk.Entry();
|
||||
|
|
@ -1377,7 +1379,7 @@ public class ComposerWindow : Gtk.Window {
|
|||
context_menu.append(new Gtk.SeparatorMenuItem());
|
||||
|
||||
// Select all.
|
||||
Gtk.MenuItem select_all_item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.SELECT_ALL, null);
|
||||
Gtk.MenuItem select_all_item = new Gtk.MenuItem.with_mnemonic(Stock.SELECT__ALL);
|
||||
select_all_item.activate.connect(on_select_all);
|
||||
context_menu.append(select_all_item);
|
||||
|
||||
|
|
@ -1560,10 +1562,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,43 +65,21 @@ 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;
|
||||
|
||||
string highlighted_result;
|
||||
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("‘", "<b>").replace("’", "</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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
154
src/client/composer/contact-list-store.vala
Normal file
154
src/client/composer/contact-list-store.vala
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
/* 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,14 +48,14 @@ abstract class AlertDialog : Object {
|
|||
|
||||
class ConfirmationDialog : AlertDialog {
|
||||
public ConfirmationDialog(Gtk.Window? parent, string primary, string? secondary, string? ok_button) {
|
||||
base (parent, Gtk.MessageType.WARNING, primary, secondary, ok_button, Gtk.Stock.CANCEL,
|
||||
base (parent, Gtk.MessageType.WARNING, primary, secondary, ok_button, Stock._CANCEL,
|
||||
null, Gtk.ResponseType.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorDialog : AlertDialog {
|
||||
public ErrorDialog(Gtk.Window? parent, string primary, string? secondary) {
|
||||
base (parent, Gtk.MessageType.ERROR, primary, secondary, Gtk.Stock.OK, null, null,
|
||||
base (parent, Gtk.MessageType.ERROR, primary, secondary, Stock._OK, null, null,
|
||||
Gtk.ResponseType.NONE);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public class AttachmentDialog : Gtk.FileChooserDialog {
|
|||
}
|
||||
|
||||
construct {
|
||||
add_button(Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL);
|
||||
add_button(Stock._CANCEL, Gtk.ResponseType.CANCEL);
|
||||
add_button(_("_Attach"), Gtk.ResponseType.ACCEPT);
|
||||
|
||||
if (!Geary.String.is_empty(current_folder)) {
|
||||
|
|
|
|||
|
|
@ -97,8 +97,8 @@ public class PasswordDialog {
|
|||
check_remember_password.active = account_information.imap_remember_password;
|
||||
|
||||
// Add action buttons
|
||||
Gtk.Button cancel_button = new Gtk.Button.from_stock(Gtk.Stock.CANCEL);
|
||||
ok_button = new Gtk.Button.from_stock(Gtk.Stock.OK);
|
||||
Gtk.Button cancel_button = new Gtk.Button.from_stock(Stock._CANCEL);
|
||||
ok_button = new Gtk.Button.from_stock(Stock._OK);
|
||||
ok_button.can_default = true;
|
||||
dialog.add_action_widget(cancel_button, Gtk.ResponseType.CANCEL);
|
||||
dialog.add_action_widget(ok_button, Gtk.ResponseType.OK);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
// Primary controller object for Geary.
|
||||
public class GearyController {
|
||||
public class GearyController : Geary.BaseObject {
|
||||
// Named actions.
|
||||
public const string ACTION_HELP = "GearyHelp";
|
||||
public const string ACTION_ABOUT = "GearyAbout";
|
||||
|
|
@ -32,7 +32,9 @@ public class GearyController {
|
|||
public const string ACTION_COPY_MENU = "GearyCopyMenuButton";
|
||||
public const string ACTION_MOVE_MENU = "GearyMoveMenuButton";
|
||||
public const string ACTION_GEAR_MENU = "GearyGearMenuButton";
|
||||
|
||||
|
||||
public const string PROP_CURRENT_CONVERSATION ="current-conversations";
|
||||
|
||||
public const int MIN_CONVERSATION_COUNT = 50;
|
||||
|
||||
private const string DELETE_MESSAGE_LABEL = _("_Delete");
|
||||
|
|
@ -62,11 +64,12 @@ public class GearyController {
|
|||
|
||||
public MainWindow main_window { get; private set; }
|
||||
|
||||
public Geary.App.ConversationMonitor? current_conversations { get; private set; default = null; }
|
||||
|
||||
private Geary.Account? current_account = null;
|
||||
private Gee.HashMap<Geary.Account, Geary.Folder> inboxes
|
||||
= new Gee.HashMap<Geary.Account, Geary.Folder>();
|
||||
private Geary.Folder? current_folder = null;
|
||||
private Geary.App.ConversationMonitor? current_conversations = null;
|
||||
private Cancellable cancellable_folder = new Cancellable();
|
||||
private Cancellable cancellable_message = new Cancellable();
|
||||
private Cancellable cancellable_search = new Cancellable();
|
||||
|
|
@ -229,20 +232,20 @@ public class GearyController {
|
|||
accounts.label = _("A_ccounts");
|
||||
entries += accounts;
|
||||
|
||||
Gtk.ActionEntry prefs = { ACTION_PREFERENCES, Gtk.Stock.PREFERENCES, TRANSLATABLE, "<Ctrl>E",
|
||||
Gtk.ActionEntry prefs = { ACTION_PREFERENCES, Stock._PREFERENCES, TRANSLATABLE, "<Ctrl>E",
|
||||
null, on_preferences };
|
||||
prefs.label = _("_Preferences");
|
||||
entries += prefs;
|
||||
|
||||
Gtk.ActionEntry help = { ACTION_HELP, Gtk.Stock.HELP, TRANSLATABLE, "F1", null, on_help };
|
||||
Gtk.ActionEntry help = { ACTION_HELP, Stock._HELP, TRANSLATABLE, "F1", null, on_help };
|
||||
help.label = _("_Help");
|
||||
entries += help;
|
||||
|
||||
Gtk.ActionEntry about = { ACTION_ABOUT, Gtk.Stock.ABOUT, TRANSLATABLE, null, null, on_about };
|
||||
Gtk.ActionEntry about = { ACTION_ABOUT, Stock._ABOUT, TRANSLATABLE, null, null, on_about };
|
||||
about.label = _("_About");
|
||||
entries += about;
|
||||
|
||||
Gtk.ActionEntry quit = { ACTION_QUIT, Gtk.Stock.QUIT, TRANSLATABLE, "<Ctrl>Q", null, on_quit };
|
||||
Gtk.ActionEntry quit = { ACTION_QUIT, Stock._QUIT, TRANSLATABLE, "<Ctrl>Q", null, on_quit };
|
||||
quit.label = _("_Quit");
|
||||
entries += quit;
|
||||
|
||||
|
|
@ -760,7 +763,6 @@ public class GearyController {
|
|||
if (current_conversations != null) {
|
||||
yield current_conversations.stop_monitoring_async(!current_is_inbox, null);
|
||||
current_conversations = null;
|
||||
main_window.set_progress_monitor(null);
|
||||
} else if (current_folder != null && !current_is_inbox) {
|
||||
yield current_folder.close_async();
|
||||
}
|
||||
|
|
@ -802,10 +804,6 @@ public class GearyController {
|
|||
current_conversations.scan_error.connect(on_scan_error);
|
||||
current_conversations.seed_completed.connect(on_seed_completed);
|
||||
|
||||
main_window.conversation_list_store.set_conversation_monitor(current_conversations);
|
||||
main_window.conversation_list_view.set_conversation_monitor(current_conversations);
|
||||
main_window.set_progress_monitor(current_conversations.progress_monitor);
|
||||
|
||||
if (!current_conversations.is_monitoring)
|
||||
yield current_conversations.start_monitoring_async(conversation_cancellable);
|
||||
|
||||
|
|
@ -1034,7 +1032,7 @@ public class GearyController {
|
|||
} catch (Error error) {
|
||||
debug("Error showing help: %s", error.message);
|
||||
Gtk.Dialog dialog = new Gtk.Dialog.with_buttons("Error", null,
|
||||
Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.Stock.CLOSE, Gtk.ResponseType.CLOSE, null);
|
||||
Gtk.DialogFlags.DESTROY_WITH_PARENT, Stock._CLOSE, Gtk.ResponseType.CLOSE, null);
|
||||
dialog.response.connect(() => { dialog.destroy(); });
|
||||
dialog.get_content_area().add(new Gtk.Label("Error showing help: %s".printf(error.message)));
|
||||
dialog.show_all();
|
||||
|
|
@ -1301,7 +1299,7 @@ public class GearyController {
|
|||
QuestionDialog ask_to_open = new QuestionDialog.with_checkbox(main_window,
|
||||
_("Are you sure you want to open \"%s\"?").printf(attachment.filename),
|
||||
_("Attachments may cause damage to your system if opened. Only open files from trusted sources."),
|
||||
Gtk.Stock.OPEN, Gtk.Stock.CANCEL, _("Don't _ask me again"), false);
|
||||
Stock._OPEN, Stock._CANCEL, _("Don't _ask me again"), false);
|
||||
if (ask_to_open.run() != Gtk.ResponseType.OK)
|
||||
return;
|
||||
|
||||
|
|
@ -1337,7 +1335,7 @@ public class GearyController {
|
|||
? Gtk.FileChooserAction.SAVE
|
||||
: Gtk.FileChooserAction.SELECT_FOLDER;
|
||||
Gtk.FileChooserDialog dialog = new Gtk.FileChooserDialog(null, main_window, action,
|
||||
Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL, Gtk.Stock.SAVE, Gtk.ResponseType.ACCEPT, null);
|
||||
Stock._CANCEL, Gtk.ResponseType.CANCEL, Stock._SAVE, Gtk.ResponseType.ACCEPT, null);
|
||||
if (last_save_directory != null)
|
||||
dialog.set_current_folder(last_save_directory.get_path());
|
||||
if (attachments.size == 1) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ public class ConversationListStore : Gtk.ListStore {
|
|||
}
|
||||
|
||||
public string? account_owner_email { get; set; default = null; }
|
||||
public Geary.ProgressMonitor preview_monitor { get; private set; default =
|
||||
new Geary.SimpleProgressMonitor(Geary.ProgressType.ACTIVITY); }
|
||||
|
||||
private Geary.App.ConversationMonitor conversation_monitor;
|
||||
private Geary.Folder? current_folder = null;
|
||||
|
|
@ -56,16 +58,17 @@ public class ConversationListStore : Gtk.ListStore {
|
|||
|
||||
GearyApplication.instance.config.display_preview_changed.connect(on_display_preview_changed);
|
||||
update_id = Timeout.add_seconds_full(Priority.LOW, 60, update_date_strings);
|
||||
|
||||
GearyApplication.instance.controller.notify[GearyController.PROP_CURRENT_CONVERSATION].
|
||||
connect(on_conversation_monitor_changed);
|
||||
}
|
||||
|
||||
~ConversationListStore() {
|
||||
set_conversation_monitor(null);
|
||||
|
||||
if (update_id != 0)
|
||||
Source.remove(update_id);
|
||||
}
|
||||
|
||||
public void set_conversation_monitor(Geary.App.ConversationMonitor? new_conversation_monitor) {
|
||||
private void on_conversation_monitor_changed() {
|
||||
if (conversation_monitor != null) {
|
||||
conversation_monitor.scan_completed.disconnect(on_scan_completed);
|
||||
conversation_monitor.conversations_added.disconnect(on_conversations_added);
|
||||
|
|
@ -76,7 +79,7 @@ public class ConversationListStore : Gtk.ListStore {
|
|||
}
|
||||
|
||||
clear();
|
||||
conversation_monitor = new_conversation_monitor;
|
||||
conversation_monitor = GearyApplication.instance.controller.current_conversations;
|
||||
|
||||
if (conversation_monitor != null) {
|
||||
// add all existing conversations
|
||||
|
|
@ -148,8 +151,12 @@ public class ConversationListStore : Gtk.ListStore {
|
|||
return;
|
||||
}
|
||||
|
||||
preview_monitor.notify_start();
|
||||
|
||||
yield do_refresh_previews_async(conversation_monitor);
|
||||
|
||||
preview_monitor.notify_finish();
|
||||
|
||||
try {
|
||||
refresh_mutex.release(ref token);
|
||||
} catch (Error err) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
69
src/client/sidebar/sidebar-count-cell-renderer.vala
Normal file
69
src/client/sidebar/sidebar-count-cell-renderer.vala
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/* 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);
|
||||
}
|
||||
|
||||
private void on_drag_begin(Gdk.DragContext ctx) {
|
||||
is_internal_drag_in_progress = true;
|
||||
}
|
||||
|
|
@ -471,11 +488,13 @@ public class Sidebar.Tree : Gtk.TreeView {
|
|||
store.set(assoc_iter, Columns.TOOLTIP, entry.get_sidebar_tooltip() != null ?
|
||||
Geary.HTML.escape_markup(entry.get_sidebar_tooltip()) : null);
|
||||
store.set(assoc_iter, Columns.WRAPPER, wrapper);
|
||||
store.set(assoc_iter, Columns.COUNTER, entry.get_count());
|
||||
load_entry_icons(assoc_iter);
|
||||
|
||||
entry.sidebar_tooltip_changed.connect(on_sidebar_tooltip_changed);
|
||||
entry.sidebar_icon_changed.connect(on_sidebar_icon_changed);
|
||||
entry.sidebar_name_changed.connect(on_sidebar_name_changed);
|
||||
entry.sidebar_count_changed.connect(on_sidebar_count_changed);
|
||||
|
||||
Sidebar.EmphasizableEntry? emphasizable = entry as Sidebar.EmphasizableEntry;
|
||||
if (emphasizable != null)
|
||||
|
|
@ -499,6 +518,7 @@ public class Sidebar.Tree : Gtk.TreeView {
|
|||
|
||||
store.set(new_iter, Columns.NAME, get_name_for_entry(entry));
|
||||
store.set(new_iter, Columns.TOOLTIP, Geary.HTML.escape_markup(entry.get_sidebar_tooltip()));
|
||||
store.set(new_iter, Columns.COUNTER, entry.get_count());
|
||||
store.set(new_iter, Columns.WRAPPER, new_wrapper);
|
||||
load_entry_icons(new_iter);
|
||||
|
||||
|
|
@ -589,6 +609,7 @@ public class Sidebar.Tree : Gtk.TreeView {
|
|||
entry.sidebar_tooltip_changed.disconnect(on_sidebar_tooltip_changed);
|
||||
entry.sidebar_icon_changed.disconnect(on_sidebar_icon_changed);
|
||||
entry.sidebar_name_changed.disconnect(on_sidebar_name_changed);
|
||||
entry.sidebar_count_changed.disconnect(on_sidebar_count_changed);
|
||||
|
||||
Sidebar.EmphasizableEntry? emphasizable = entry as Sidebar.EmphasizableEntry;
|
||||
if (emphasizable != null)
|
||||
|
|
@ -758,6 +779,13 @@ public class Sidebar.Tree : Gtk.TreeView {
|
|||
rename_entry(entry);
|
||||
}
|
||||
|
||||
private void on_sidebar_count_changed(Sidebar.Entry entry, int coun) {
|
||||
EntryWrapper? wrapper = get_wrapper(entry);
|
||||
assert(wrapper != null);
|
||||
|
||||
store.set(wrapper.get_iter(), Columns.COUNTER, entry.get_count());
|
||||
}
|
||||
|
||||
private Gdk.Pixbuf? fetch_icon_pixbuf(GLib.Icon? gicon) {
|
||||
if (gicon == null)
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ public class MainWindow : Gtk.Window {
|
|||
|
||||
private Gtk.ScrolledWindow conversation_list_scrolled;
|
||||
private MonitoredSpinner spinner = new MonitoredSpinner();
|
||||
private Geary.AggregateProgressMonitor progress_monitor = new Geary.AggregateProgressMonitor();
|
||||
private Geary.ProgressMonitor? conversation_monitor_progress = null;
|
||||
|
||||
public MainWindow() {
|
||||
title = GearyApplication.NAME;
|
||||
|
|
@ -42,12 +44,19 @@ public class MainWindow : Gtk.Window {
|
|||
|
||||
add_accel_group(GearyApplication.instance.ui_manager.get_accel_group());
|
||||
|
||||
spinner.set_progress_monitor(progress_monitor);
|
||||
progress_monitor.add(conversation_list_store.preview_monitor);
|
||||
|
||||
GLib.List<Gdk.Pixbuf> pixbuf_list = new GLib.List<Gdk.Pixbuf>();
|
||||
pixbuf_list.append(IconFactory.instance.application_icon);
|
||||
set_default_icon_list(pixbuf_list);
|
||||
|
||||
delete_event.connect(on_delete_event);
|
||||
key_press_event.connect(on_key_press_event);
|
||||
GearyApplication.instance.controller.notify[GearyController.PROP_CURRENT_CONVERSATION].
|
||||
connect(on_conversation_monitor_changed);
|
||||
Geary.Engine.instance.account_available.connect(on_account_available);
|
||||
Geary.Engine.instance.account_unavailable.connect(on_account_unavailable);
|
||||
|
||||
create_layout();
|
||||
}
|
||||
|
|
@ -82,13 +91,6 @@ public class MainWindow : Gtk.Window {
|
|||
return base.configure_event(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the progress monitor to display in the status bar.
|
||||
*/
|
||||
public void set_progress_monitor(Geary.ProgressMonitor? monitor) {
|
||||
spinner.set_progress_monitor(monitor);
|
||||
}
|
||||
|
||||
private void create_layout() {
|
||||
Gtk.Box main_layout = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
|
||||
|
||||
|
|
@ -146,5 +148,38 @@ public class MainWindow : Gtk.Window {
|
|||
// via the default handling
|
||||
return propagate_key_event(event);
|
||||
}
|
||||
|
||||
private void on_conversation_monitor_changed() {
|
||||
Geary.App.ConversationMonitor? conversation_monitor =
|
||||
GearyApplication.instance.controller.current_conversations;
|
||||
|
||||
// Remove existing progress monitor.
|
||||
if (conversation_monitor_progress != null) {
|
||||
progress_monitor.remove(conversation_monitor_progress);
|
||||
conversation_monitor_progress = null;
|
||||
}
|
||||
|
||||
// Add new one.
|
||||
if (conversation_monitor != null) {
|
||||
conversation_monitor_progress = conversation_monitor.progress_monitor;
|
||||
progress_monitor.add(conversation_monitor_progress);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_account_available(Geary.AccountInformation account) {
|
||||
try {
|
||||
progress_monitor.add(Geary.Engine.instance.get_account_instance(account).opening_monitor);
|
||||
} catch (Error e) {
|
||||
debug("Could not access account opening progress monitor: %s", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_account_unavailable(Geary.AccountInformation account) {
|
||||
try {
|
||||
progress_monitor.remove(Geary.Engine.instance.get_account_instance(account).opening_monitor);
|
||||
} catch (Error e) {
|
||||
debug("Could not access account opening progress monitor: %s", e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
35
src/client/ui/stock.vala
Normal file
35
src/client/ui/stock.vala
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* With GtkStock deprecated in GTK+ 3.10, these strings offer replacements for commonly-needed
|
||||
* text labels.
|
||||
*
|
||||
* Plain text use all-caps constant names and an underscore indicating where in the English text the
|
||||
* mnemonic lies. This can be used to ensure that the mnemonic doesn't interfere with other custom
|
||||
* strings in the grouping.
|
||||
*/
|
||||
|
||||
namespace Stock {
|
||||
|
||||
public const string _OK = _("_OK");
|
||||
public const string _CANCEL = _("_Cancel");
|
||||
|
||||
public const string _ABOUT = _("_About");
|
||||
public const string _ADD = _("_Add");
|
||||
public const string _CLOSE = _("_Close");
|
||||
public const string _DISCARD = _("_Discard");
|
||||
public const string _HELP = _("_Help");
|
||||
public const string _OPEN = _("_Open");
|
||||
public const string _PREFERENCES = _("_Preferences");
|
||||
public const string _PRINT = _("_Print");
|
||||
public const string _QUIT = _("_Quit");
|
||||
public const string _REMOVE = _("_Remove");
|
||||
public const string _SAVE = _("_Save");
|
||||
public const string SELECT__ALL = _("Select _All");
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +144,7 @@ public CoarseDate as_coarse_date(DateTime datetime, DateTime now, TimeSpan diff)
|
|||
if (same_day(temp, now)) {
|
||||
return CoarseDate.YESTERDAY;
|
||||
}
|
||||
temp = datetime.add_weeks(1);
|
||||
temp = datetime.add_days(6);
|
||||
if (same_day(temp, now) || temp.compare(now) >= 0) {
|
||||
return CoarseDate.THIS_WEEK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,16 +69,18 @@ public class ConversationListView : Gtk.TreeView {
|
|||
Gdk.DragAction.COPY | Gdk.DragAction.MOVE);
|
||||
|
||||
GearyApplication.instance.config.display_preview_changed.connect(on_display_preview_changed);
|
||||
GearyApplication.instance.controller.notify[GearyController.PROP_CURRENT_CONVERSATION].
|
||||
connect(on_conversation_monitor_changed);
|
||||
}
|
||||
|
||||
public void set_conversation_monitor(Geary.App.ConversationMonitor? new_conversation_monitor) {
|
||||
private void on_conversation_monitor_changed() {
|
||||
if (conversation_monitor != null) {
|
||||
conversation_monitor.scan_started.disconnect(on_scan_started);
|
||||
conversation_monitor.scan_completed.disconnect(on_scan_completed);
|
||||
conversation_monitor.conversation_removed.disconnect(on_conversation_removed);
|
||||
}
|
||||
|
||||
conversation_monitor = new_conversation_monitor;
|
||||
conversation_monitor = GearyApplication.instance.controller.current_conversations;
|
||||
|
||||
if (conversation_monitor != null) {
|
||||
conversation_monitor.scan_started.connect(on_scan_started);
|
||||
|
|
|
|||
|
|
@ -1362,7 +1362,7 @@ public class ConversationViewer : Gtk.Box {
|
|||
}
|
||||
|
||||
// Print a message.
|
||||
Gtk.MenuItem print_item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.PRINT, null);
|
||||
Gtk.MenuItem print_item = new Gtk.MenuItem.with_mnemonic(Stock._PRINT);
|
||||
print_item.activate.connect(() => on_print_message(email));
|
||||
menu.append(print_item);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account {
|
|||
public Geary.AccountInformation information { get; protected set; }
|
||||
public Geary.ProgressMonitor search_upgrade_monitor { get; protected set; }
|
||||
public Geary.ProgressMonitor db_upgrade_monitor { get; protected set; }
|
||||
public Geary.ProgressMonitor opening_monitor { get; protected set; }
|
||||
|
||||
private string name;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ public interface Geary.Account : BaseObject {
|
|||
|
||||
public abstract Geary.ProgressMonitor search_upgrade_monitor { get; protected set; }
|
||||
public abstract Geary.ProgressMonitor db_upgrade_monitor { get; protected set; }
|
||||
public abstract Geary.ProgressMonitor opening_monitor { get; protected set; }
|
||||
|
||||
public signal void opened();
|
||||
|
||||
|
|
|
|||
|
|
@ -172,6 +172,28 @@ public class Geary.AggregateProgressMonitor : Geary.ProgressMonitor {
|
|||
pm.finish.connect(on_finish);
|
||||
}
|
||||
|
||||
public void remove(Geary.ProgressMonitor pm) {
|
||||
// TODO: Handle the case where we remove a new monitor during progress.
|
||||
monitors.remove(pm);
|
||||
pm.start.disconnect(on_start);
|
||||
pm.update.disconnect(on_update);
|
||||
pm.finish.disconnect(on_finish);
|
||||
|
||||
if (pm.is_in_progress) {
|
||||
// If no other PMs are in progress, we must issue a finish signal.
|
||||
bool issue_signal = true;
|
||||
foreach(ProgressMonitor p in monitors) {
|
||||
if (p.is_in_progress) {
|
||||
issue_signal = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (issue_signal)
|
||||
notify_finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void on_start() {
|
||||
if (!is_in_progress)
|
||||
notify_start();
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder {
|
|||
int result_mutex_token = yield result_mutex.claim_async();
|
||||
|
||||
Geary.EmailIdentifier[] ids = new Geary.EmailIdentifier[search_results.size];
|
||||
int initial_index = -1;
|
||||
int initial_index = 0;
|
||||
int i = 0;
|
||||
foreach (Geary.Email email in search_results) {
|
||||
if (initial_id != null && email.id.equal_to(initial_id))
|
||||
|
|
@ -284,12 +284,12 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder {
|
|||
ids[i++] = email.id;
|
||||
}
|
||||
|
||||
if (initial_id == null)
|
||||
if (initial_id == null && flags.is_all_set(Folder.ListFlags.OLDEST_TO_NEWEST))
|
||||
initial_index = ids.length - 1;
|
||||
|
||||
Gee.List<Geary.Email> results = new Gee.ArrayList<Geary.Email>();
|
||||
if (initial_index >= 0) {
|
||||
int increment = flags.is_oldest_to_newest() ? 1 : -1;
|
||||
int increment = flags.is_oldest_to_newest() ? -1 : 1;
|
||||
i = initial_index;
|
||||
if (!flags.is_including_id() && initial_id != null)
|
||||
i += increment;
|
||||
|
|
|
|||
|
|
@ -673,6 +673,9 @@ public class Geary.App.ConversationMonitor : BaseObject {
|
|||
if (get_search_blacklist().contains(folder.path))
|
||||
return;
|
||||
|
||||
if (conversations.is_empty)
|
||||
return;
|
||||
|
||||
debug("%d out of folder message(s) appended to %s, fetching to add to conversations...", appended_ids.size,
|
||||
folder.to_string());
|
||||
|
||||
|
|
@ -718,7 +721,8 @@ public class Geary.App.ConversationMonitor : BaseObject {
|
|||
if (earliest_id != null) {
|
||||
debug("ConversationMonitor (%s) reseeding starting from Email ID %s on opened %s", why,
|
||||
earliest_id.to_string(), folder.to_string());
|
||||
yield load_by_id_async(earliest_id, int.MAX, Geary.Folder.ListFlags.OLDEST_TO_NEWEST,
|
||||
yield load_by_id_async(earliest_id, int.MAX,
|
||||
Geary.Folder.ListFlags.OLDEST_TO_NEWEST | Geary.Folder.ListFlags.INCLUDING_ID,
|
||||
cancellable_monitor);
|
||||
} else {
|
||||
debug("ConversationMonitor (%s) reseeding latest %d emails on opened %s", why,
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ private class Geary.App.ConversationSet : BaseObject {
|
|||
Email? existing = null;
|
||||
foreach (Geary.Email other in conversation.get_emails(Geary.Conversation.Ordering.NONE)) {
|
||||
if (other.message_id != null && email.message_id.equal_to(other.message_id)) {
|
||||
existing = email;
|
||||
existing = other;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
|
|||
cx.set_busy_timeout_msec(Db.Connection.RECOMMENDED_BUSY_TIMEOUT_MSEC);
|
||||
cx.set_foreign_keys(true);
|
||||
cx.set_recursive_triggers(true);
|
||||
cx.set_synchronous(Db.SynchronousMode.NORMAL);
|
||||
cx.set_synchronous(Db.SynchronousMode.OFF);
|
||||
sqlite3_unicodesn_register_tokenizer(cx.db);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,47 +232,40 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|||
Gee.HashMap<Geary.Email, bool> results = new Gee.HashMap<Geary.Email, bool>();
|
||||
Gee.ArrayList<Geary.EmailIdentifier> complete_ids = new Gee.ArrayList<Geary.EmailIdentifier>();
|
||||
Gee.Collection<Contact> updated_contacts = new Gee.ArrayList<Contact>();
|
||||
Error? error = null;
|
||||
int unread_change = 0;
|
||||
try {
|
||||
int total_unread_change = 0;
|
||||
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
|
||||
foreach (Geary.Email email in emails) {
|
||||
Db.TransactionOutcome outcome = yield db.exec_transaction_async(Db.TransactionType.RW,
|
||||
(cx) => {
|
||||
Gee.Collection<Contact>? contacts_this_email = null;
|
||||
Geary.Email.Field combined_fields;
|
||||
bool created = do_create_or_merge_email(cx, email, out combined_fields,
|
||||
out contacts_this_email, ref unread_change, cancellable);
|
||||
|
||||
if (contacts_this_email != null)
|
||||
updated_contacts.add_all(contacts_this_email);
|
||||
|
||||
results.set(email, created);
|
||||
|
||||
if (combined_fields.is_all_set(Geary.Email.Field.ALL))
|
||||
complete_ids.add(email.id);
|
||||
|
||||
// Update unread count in DB.
|
||||
do_add_to_unread_count(cx, unread_change, cancellable);
|
||||
|
||||
return Db.TransactionOutcome.COMMIT;
|
||||
}, cancellable);
|
||||
Gee.Collection<Contact>? contacts_this_email = null;
|
||||
Geary.Email.Field pre_fields;
|
||||
Geary.Email.Field post_fields;
|
||||
int unread_change = 0;
|
||||
bool created = do_create_or_merge_email(cx, email, out pre_fields,
|
||||
out post_fields, out contacts_this_email, ref unread_change, cancellable);
|
||||
|
||||
if (outcome == Db.TransactionOutcome.COMMIT && updated_contacts.size > 0)
|
||||
contact_store.update_contacts(updated_contacts);
|
||||
if (contacts_this_email != null)
|
||||
updated_contacts.add_all(contacts_this_email);
|
||||
|
||||
// clear each iteration
|
||||
updated_contacts.clear();
|
||||
results.set(email, created);
|
||||
|
||||
// in essence, only fire the "email-completed" signal if the local version didn't
|
||||
// have all the fields but after the create/merge now does
|
||||
if (post_fields.is_all_set(Geary.Email.Field.ALL) && !pre_fields.is_all_set(Geary.Email.Field.ALL))
|
||||
complete_ids.add(email.id);
|
||||
|
||||
// Update unread count in DB.
|
||||
do_add_to_unread_count(cx, unread_change, cancellable);
|
||||
|
||||
total_unread_change += unread_change;
|
||||
}
|
||||
} catch (Error e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
return Db.TransactionOutcome.COMMIT;
|
||||
}, cancellable);
|
||||
|
||||
if (updated_contacts.size > 0)
|
||||
contact_store.update_contacts(updated_contacts);
|
||||
|
||||
// Update the email_unread properties.
|
||||
if (error == null) {
|
||||
properties.set_status_unseen((properties.email_unread + unread_change).clamp(0, int.MAX));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
properties.set_status_unseen((properties.email_unread + total_unread_change).clamp(0, int.MAX));
|
||||
|
||||
if (complete_ids.size > 0)
|
||||
email_complete(complete_ids);
|
||||
|
|
@ -934,8 +927,9 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|||
}
|
||||
|
||||
private bool do_create_or_merge_email(Db.Connection cx, Geary.Email email,
|
||||
out Geary.Email.Field combined_fields, out Gee.Collection<Contact> updated_contacts,
|
||||
ref int unread_count_change, Cancellable? cancellable) throws Error {
|
||||
out Geary.Email.Field pre_fields, out Geary.Email.Field post_fields,
|
||||
out Gee.Collection<Contact> updated_contacts, ref int unread_count_change,
|
||||
Cancellable? cancellable) throws Error {
|
||||
// see if message already present in current folder, if not, search for duplicate throughout
|
||||
// mailbox
|
||||
bool associated;
|
||||
|
|
@ -943,8 +937,8 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|||
|
||||
// if found, merge, and associate if necessary
|
||||
if (message_id != Db.INVALID_ROWID) {
|
||||
do_merge_email(cx, message_id, email, out combined_fields, out updated_contacts,
|
||||
ref unread_count_change, !associated, cancellable);
|
||||
do_merge_email(cx, message_id, email, out pre_fields, out post_fields,
|
||||
out updated_contacts, ref unread_count_change, !associated, cancellable);
|
||||
|
||||
// return false to indicate a merge
|
||||
return false;
|
||||
|
|
@ -953,7 +947,8 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|||
// not found, so create and associate with this folder
|
||||
MessageRow row = new MessageRow.from_email(email);
|
||||
|
||||
combined_fields = email.fields;
|
||||
pre_fields = Geary.Email.Field.NONE;
|
||||
post_fields = email.fields;
|
||||
|
||||
Db.Statement stmt = cx.prepare(
|
||||
"INSERT INTO MessageTable "
|
||||
|
|
@ -1465,23 +1460,29 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|||
}
|
||||
|
||||
private void do_merge_email(Db.Connection cx, int64 message_id, Geary.Email email,
|
||||
out Geary.Email.Field combined_fields, out Gee.Collection<Contact> updated_contacts,
|
||||
ref int unread_count_change, bool associate_with_folder,
|
||||
Cancellable? cancellable) throws Error {
|
||||
out Geary.Email.Field pre_fields, out Geary.Email.Field post_fields,
|
||||
out Gee.Collection<Contact> updated_contacts, ref int unread_count_change,
|
||||
bool associate_with_folder, Cancellable? cancellable) throws Error {
|
||||
assert(message_id != Db.INVALID_ROWID);
|
||||
|
||||
int new_unread_count = 0;
|
||||
|
||||
if (associate_with_folder) {
|
||||
// Note: no check is performed here to prevent double-adds. The caller of this method
|
||||
// is responsible for only setting associate_with_folder if required.
|
||||
do_associate_with_folder(cx, message_id, email, cancellable);
|
||||
unread_count_change++;
|
||||
}
|
||||
|
||||
// Default to an empty list, in case we never call do_merge_message_row.
|
||||
updated_contacts = new Gee.LinkedList<Contact>();
|
||||
|
||||
// fetch message from database and merge in this email
|
||||
Geary.Email.Field db_fields;
|
||||
MessageRow row = do_fetch_message_row(cx, message_id,
|
||||
email.fields | Email.REQUIRED_FOR_MESSAGE | Attachment.REQUIRED_FIELDS,
|
||||
out db_fields, cancellable);
|
||||
out pre_fields, cancellable);
|
||||
Geary.Email.Field fetched_fields = row.fields;
|
||||
combined_fields = db_fields | email.fields;
|
||||
post_fields = pre_fields | email.fields;
|
||||
row.merge_from_remote(email);
|
||||
|
||||
if (email.fields == Geary.Email.Field.NONE)
|
||||
|
|
@ -1511,13 +1512,6 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|||
do_add_email_to_search_table(cx, message_id, combined_email, cancellable);
|
||||
}
|
||||
|
||||
if (associate_with_folder) {
|
||||
// Note: no check is performed here to prevent double-adds. The caller of this method
|
||||
// is responsible for only setting associate_with_folder if required.
|
||||
do_associate_with_folder(cx, message_id, email, cancellable);
|
||||
unread_count_change++;
|
||||
}
|
||||
|
||||
unread_count_change += new_unread_count;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject {
|
||||
private const int FETCH_DATE_RECEIVED_CHUNK_COUNT = 25;
|
||||
private const int SYNC_DELAY_SEC = 15;
|
||||
|
||||
public GenericAccount account { get; private set; }
|
||||
|
||||
|
|
@ -14,6 +15,7 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject {
|
|||
private GenericFolder? current_folder = null;
|
||||
private Cancellable? bg_cancellable = null;
|
||||
private Nonblocking.Semaphore stopped = new Nonblocking.Semaphore();
|
||||
private Gee.HashSet<FolderPath> unavailable_paths = new Gee.HashSet<FolderPath>();
|
||||
|
||||
public AccountSynchronizer(GenericAccount account) {
|
||||
this.account = account;
|
||||
|
|
@ -51,6 +53,7 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject {
|
|||
bg_queue.allow_duplicates = false;
|
||||
bg_queue.requeue_duplicate = false;
|
||||
bg_cancellable = new Cancellable();
|
||||
unavailable_paths.clear();
|
||||
|
||||
// immediately start processing folders as they are announced as available
|
||||
process_queue_async.begin();
|
||||
|
|
@ -61,6 +64,7 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject {
|
|||
|
||||
bg_cancellable.cancel();
|
||||
bg_queue.clear();
|
||||
unavailable_paths.clear();
|
||||
}
|
||||
|
||||
private void on_account_prefetch_changed() {
|
||||
|
|
@ -68,7 +72,7 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject {
|
|||
// treat as an availability check (i.e. as if the account had just opened) because
|
||||
// just because this value has changed doesn't mean the contents in the folders
|
||||
// have changed
|
||||
send_all(account.list_folders(), true);
|
||||
delayed_send_all(account.list_folders(), true);
|
||||
} catch (Error err) {
|
||||
debug("Unable to schedule re-sync for %s due to prefetch time changing: %s",
|
||||
account.to_string(), err.message);
|
||||
|
|
@ -80,15 +84,38 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject {
|
|||
if (stopped.is_passed())
|
||||
return;
|
||||
|
||||
if (available != null)
|
||||
send_all(available, true);
|
||||
if (available != null) {
|
||||
foreach (Folder folder in available)
|
||||
unavailable_paths.remove(folder.path);
|
||||
|
||||
delayed_send_all(available, true);
|
||||
}
|
||||
|
||||
if (unavailable != null)
|
||||
if (unavailable != null) {
|
||||
foreach (Folder folder in unavailable)
|
||||
unavailable_paths.add(folder.path);
|
||||
|
||||
revoke_all(unavailable);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_folders_contents_altered(Gee.Collection<Folder> altered) {
|
||||
send_all(altered, false);
|
||||
delayed_send_all(altered, false);
|
||||
}
|
||||
|
||||
private void delayed_send_all(Gee.Collection<Folder> folders, bool reason_available) {
|
||||
Timeout.add_seconds(SYNC_DELAY_SEC, () => {
|
||||
// remove any unavailable folders
|
||||
Gee.ArrayList<Folder> trimmed_folders = new Gee.ArrayList<Folder>();
|
||||
foreach (Folder folder in folders) {
|
||||
if (!unavailable_paths.contains(folder.path))
|
||||
trimmed_folders.add(folder);
|
||||
}
|
||||
|
||||
send_all(trimmed_folders, reason_available);
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void send_all(Gee.Collection<Folder> folders, bool reason_available) {
|
||||
|
|
@ -244,11 +271,12 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject {
|
|||
// Oldest local email before epoch, don't sync from network
|
||||
return true;
|
||||
} else if (folder.properties.email_total == local_count) {
|
||||
// Local email is after epoch, but there's nothing before it
|
||||
// Local earliest email is after epoch, but there's nothing before it
|
||||
return true;
|
||||
} else {
|
||||
debug("Oldest local email in %s not old enough (%s vs. %s), synchronizing...",
|
||||
folder.to_string(), oldest_local.to_string(), epoch.to_string());
|
||||
debug("Oldest local email in %s not old enough (%s vs. %s), email_total=%d vs. local_count=%d, synchronizing...",
|
||||
folder.to_string(), oldest_local.to_string(), epoch.to_string(),
|
||||
folder.properties.email_total, local_count);
|
||||
}
|
||||
} else if (folder.properties.email_total == 0) {
|
||||
// no local messages, no remote messages -- this is as good as having everything up
|
||||
|
|
@ -305,12 +333,36 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject {
|
|||
|
||||
// only perform vector expansion if oldest isn't old enough
|
||||
if (oldest_local == null || oldest_local.compare(epoch) > 0) {
|
||||
Geary.EmailIdentifier? epoch_id = yield folder.find_earliest_email_async(epoch,
|
||||
oldest_local_id, bg_cancellable);
|
||||
if (epoch_id == null) {
|
||||
debug("Unable to locate epoch messages on remote folder %s%s", folder.to_string(),
|
||||
(oldest_local_id != null) ? " earlier than oldest local" : "");
|
||||
}
|
||||
// go back one month at a time to the epoch, performing a little vector expansion at a
|
||||
// time rather than all at once (which will stall the replay queue)
|
||||
DateTime current_epoch = (oldest_local != null) ? oldest_local : new DateTime.now_local();
|
||||
do {
|
||||
current_epoch = current_epoch.add_months(-1);
|
||||
|
||||
// don't go past epoch
|
||||
if (current_epoch.compare(epoch) < 0)
|
||||
current_epoch = epoch;
|
||||
|
||||
debug("Background sync'ing %s to %s", folder.to_string(), current_epoch.to_string());
|
||||
Geary.EmailIdentifier? epoch_id = yield folder.find_earliest_email_async(current_epoch,
|
||||
oldest_local_id, bg_cancellable);
|
||||
if (epoch_id == null && current_epoch.compare(epoch) <= 0) {
|
||||
debug("Unable to locate epoch messages on remote folder %s%s, fetching one past oldest...",
|
||||
folder.to_string(),
|
||||
(oldest_local_id != null) ? " earlier than oldest local" : "");
|
||||
|
||||
// if there's nothing between the oldest local and the epoch, that means the
|
||||
// mail just prior to our local oldest is oldest than the epoch; rather than
|
||||
// continually thrashing looking for something that's just out of reach, add it
|
||||
// to the folder and be done with it ... note that this even works if oldest_local_id
|
||||
// is null, as that means the local folder is empty and so we should at least
|
||||
// pull the first one to get a marker of age
|
||||
yield folder.list_email_by_id_async(oldest_local_id, 1, Geary.Email.Field.NONE,
|
||||
Geary.Folder.ListFlags.NONE, bg_cancellable);
|
||||
} else if (epoch_id != null) {
|
||||
oldest_local_id = epoch_id;
|
||||
}
|
||||
} while (current_epoch.compare(epoch) > 0);
|
||||
} else {
|
||||
debug("No expansion necessary for %s, oldest local (%s) is before epoch (%s)",
|
||||
folder.to_string(), oldest_local.to_string(), epoch.to_string());
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@
|
|||
private class Geary.ImapEngine.EmailPrefetcher : Object {
|
||||
public const int PREFETCH_DELAY_SEC = 1;
|
||||
|
||||
private const Geary.Email.Field PREFETCH_FIELDS = Geary.Email.Field.ALL;
|
||||
// Don't fetch FLAGS; those are fetched by the FlagWatcher and during normalization when a
|
||||
// standard open_async() is invoked on the Folder
|
||||
private const Geary.Email.Field PREFETCH_FIELDS = Geary.Email.Field.ALL & ~(Geary.Email.MUTABLE_FIELDS);
|
||||
private const int PREFETCH_IDS_CHUNKS = 500;
|
||||
private const int PREFETCH_CHUNK_BYTES = 128 * 1024;
|
||||
private const int PREFETCH_CHUNK_BYTES = 64 * 1024;
|
||||
|
||||
public Nonblocking.CountingSemaphore active_sem { get; private set;
|
||||
default = new Nonblocking.CountingSemaphore(null); }
|
||||
|
|
@ -169,8 +171,6 @@ private class Geary.ImapEngine.EmailPrefetcher : Object {
|
|||
if (emails.size == 0)
|
||||
return;
|
||||
|
||||
debug("do_prefetch_batch_async %s start_total=%d", folder.to_string(), emails.size);
|
||||
|
||||
// Remove anything that is fully prefetched
|
||||
Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>? fields = null;
|
||||
try {
|
||||
|
|
@ -192,6 +192,11 @@ private class Geary.ImapEngine.EmailPrefetcher : Object {
|
|||
return !fields.get(email.id).fulfills(PREFETCH_FIELDS);
|
||||
});
|
||||
|
||||
if (emails.size == 0)
|
||||
return;
|
||||
|
||||
debug("do_prefetch_batch_async %s start_total=%d", folder.to_string(), emails.size);
|
||||
|
||||
// Big TODO: The engine needs to be able to synthesize ENVELOPE (and any of the fields
|
||||
// constituting it) and PREVIEW from HEADER and BODY if available. When it can do that
|
||||
// won't need to prefetch ENVELOPE or PREVIEW; prefetching HEADER and BODY will be enough.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
|
|||
|
||||
search_upgrade_monitor = local.search_index_monitor;
|
||||
db_upgrade_monitor = local.upgrade_monitor;
|
||||
opening_monitor = new Geary.SimpleProgressMonitor(Geary.ProgressType.ACTIVITY);
|
||||
|
||||
if (outbox_path == null) {
|
||||
outbox_path = new SmtpOutboxFolderRoot();
|
||||
|
|
@ -245,6 +246,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
|
|||
|
||||
private bool on_refresh_folders() {
|
||||
in_refresh_enumerate = true;
|
||||
opening_monitor.notify_start();
|
||||
enumerate_folders_async.begin(refresh_cancellable, on_refresh_completed);
|
||||
|
||||
refresh_folder_timeout_id = 0;
|
||||
|
|
@ -253,6 +255,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
|
|||
}
|
||||
|
||||
private void on_refresh_completed(Object? source, AsyncResult result) {
|
||||
opening_monitor.notify_finish();
|
||||
try {
|
||||
enumerate_folders_async.end(result);
|
||||
} catch (Error err) {
|
||||
|
|
|
|||
|
|
@ -86,7 +86,10 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
}
|
||||
|
||||
public void set_special_folder_type(SpecialFolderType new_type) {
|
||||
SpecialFolderType old_type = _special_folder_type;
|
||||
_special_folder_type = new_type;
|
||||
if(old_type != new_type)
|
||||
notify_special_folder_type_changed(old_type, new_type);
|
||||
}
|
||||
|
||||
public override Geary.Folder.OpenState get_open_state() {
|
||||
|
|
@ -261,6 +264,10 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
Gee.ArrayList<Geary.EmailIdentifier> appended_ids = new Gee.ArrayList<Geary.EmailIdentifier>();
|
||||
Gee.ArrayList<Geary.EmailIdentifier> removed_ids = new Gee.ArrayList<Geary.EmailIdentifier>();
|
||||
for (;;) {
|
||||
// this loop can be long, so manually check for cancellation
|
||||
if (cancellable != null && cancellable.is_cancelled())
|
||||
throw new IOError.CANCELLED("Folder %s normalization cancelled", to_string());
|
||||
|
||||
Geary.Email? remote_email = null;
|
||||
Geary.Imap.UID? remote_uid = null;
|
||||
if (old_remote != null && remote_ctr < remote_length) {
|
||||
|
|
@ -791,11 +798,18 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
// marked for removal, which that helper function doesn't like
|
||||
local_position = remote_position - (remote_count - local_count);
|
||||
|
||||
debug("do_replay_remove_message: local_count=%d local_position=%d", local_count, local_position);
|
||||
|
||||
Imap.UID? uid = yield local_folder.get_uid_at_async(local_position, null);
|
||||
if (uid != null)
|
||||
owned_id = new Imap.EmailIdentifier(uid, path);
|
||||
// zero or negative means the message exists beyond the local vector's range, so
|
||||
// nothing to do there
|
||||
if (local_position > 0) {
|
||||
debug("do_replay_remove_message: local_count=%d local_position=%d", local_count, local_position);
|
||||
|
||||
Imap.UID? uid = yield local_folder.get_uid_at_async(local_position, null);
|
||||
if (uid != null)
|
||||
owned_id = new Imap.EmailIdentifier(uid, path);
|
||||
} else {
|
||||
debug("do_replay_remove_message: message not stored locally (local_count=%d local_position=%d)",
|
||||
local_count, local_position);
|
||||
}
|
||||
} catch (Error err) {
|
||||
debug("Unable to determine ID of removed message #%d from %s: %s", remote_position,
|
||||
to_string(), err.message);
|
||||
|
|
|
|||
|
|
@ -51,8 +51,7 @@ private class Geary.ImapEngine.FetchEmail : Geary.ImapEngine.SendReplayOperation
|
|||
if (email != null && email.fields.fulfills(required_fields))
|
||||
return ReplayOperation.Status.COMPLETED;
|
||||
|
||||
// If local only (or not connected) and not found fully in local store, throw NOT_FOUND;
|
||||
// there is no fallback
|
||||
// If local only and not found fully in local store, throw NOT_FOUND
|
||||
if (flags.is_all_set(Folder.ListFlags.LOCAL_ONLY)) {
|
||||
throw new EngineError.NOT_FOUND("Email %s with fields %Xh not found in %s", id.to_string(),
|
||||
required_fields, to_string());
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmail {
|
||||
private Imap.EmailIdentifier? initial_id;
|
||||
private int count;
|
||||
private int local_list_count = 0;
|
||||
private int fulfilled_count = 0;
|
||||
private bool initial_id_found = false;
|
||||
|
||||
public ListEmailByID(GenericFolder owner, Geary.EmailIdentifier? initial_id, int count,
|
||||
Geary.Email.Field required_fields, Folder.ListFlags flags, Gee.List<Geary.Email>? accumulator,
|
||||
|
|
@ -32,6 +33,9 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai
|
|||
Gee.ArrayList<Geary.Email> fulfilled = new Gee.ArrayList<Geary.Email>();
|
||||
if (list != null) {
|
||||
foreach (Geary.Email email in list) {
|
||||
if (initial_id != null && email.id.equal_to(initial_id))
|
||||
initial_id_found = true;
|
||||
|
||||
if (email.fields.fulfills(required_fields))
|
||||
fulfilled.add(email);
|
||||
else
|
||||
|
|
@ -39,8 +43,16 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai
|
|||
}
|
||||
}
|
||||
|
||||
// verify that the initial_id was found; if not, then want to get it from the remote
|
||||
// (this will force a vector expansion, if required)
|
||||
if (initial_id != null && !initial_id_found) {
|
||||
unfulfilled.set(required_fields | ImapDB.Folder.REQUIRED_FOR_DUPLICATE_DETECTION,
|
||||
initial_id);
|
||||
}
|
||||
|
||||
// report fulfilled items
|
||||
if (fulfilled.size > 0) {
|
||||
fulfilled_count = fulfilled.size;
|
||||
if (fulfilled_count > 0) {
|
||||
if (accumulator != null)
|
||||
accumulator.add_all(fulfilled);
|
||||
|
||||
|
|
@ -48,10 +60,33 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai
|
|||
cb(fulfilled, null);
|
||||
}
|
||||
|
||||
// determine if everything was listed
|
||||
bool finished = false;
|
||||
if (flags.is_local_only()) {
|
||||
// local-only operations stop here
|
||||
finished = true;
|
||||
} else if (count != int.MAX) {
|
||||
// fetching 'count' fulfilled items and no unfulfilled items means listing is done
|
||||
// this is true for both oldest-to-newest, newest-to-oldest, whether or not they have
|
||||
// an initial_id
|
||||
finished = (unfulfilled.size == 0 && fulfilled_count >= count);
|
||||
} else {
|
||||
// count == int.MAX
|
||||
// This sentinel means "get everything from this point", so this has different meanings
|
||||
// depending on direction
|
||||
if (flags.is_newest_to_oldest()) {
|
||||
// only finished if the folder is entirely normalized
|
||||
Trillian is_fully_expanded = yield is_fully_expanded_async();
|
||||
finished = (is_fully_expanded == Trillian.TRUE);
|
||||
} else {
|
||||
// for oldest-to-newest, finished if no unfulfilled items
|
||||
finished = (unfulfilled.size == 0);
|
||||
}
|
||||
}
|
||||
|
||||
// local-only operations stop here; also, since the local store is normalized from the top
|
||||
// of the vector on down, if enough items came back fulfilled, then done
|
||||
local_list_count = (list != null) ? list.size : 0;
|
||||
if (flags.is_local_only() || (unfulfilled.size == 0 && local_list_count >= count)) {
|
||||
if (finished) {
|
||||
if (cb != null)
|
||||
cb(null, null);
|
||||
|
||||
|
|
@ -62,10 +97,36 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai
|
|||
}
|
||||
|
||||
public override async ReplayOperation.Status replay_remote_async() throws Error {
|
||||
// To get this far, either the local store doesn't have all the contents of the items in
|
||||
// the request range, or it doesn't have any row for items in the range (i.e. the vector
|
||||
// is too short).
|
||||
if (local_list_count + unfulfilled.size < count) {
|
||||
bool expansion_required = false;
|
||||
Trillian is_fully_expanded = yield is_fully_expanded_async();
|
||||
if (is_fully_expanded == Trillian.FALSE) {
|
||||
if (flags.is_oldest_to_newest()) {
|
||||
if (initial_id != null) {
|
||||
// expand vector if not initial_id not discovered
|
||||
expansion_required = !initial_id_found;
|
||||
} else {
|
||||
// initial_id == null, expansion required if not fully already
|
||||
expansion_required = true;
|
||||
}
|
||||
} else {
|
||||
// newest-to-oldest
|
||||
if (count == int.MAX) {
|
||||
// if infinite count, expansion required if not already
|
||||
expansion_required = true;
|
||||
} else if (initial_id != null) {
|
||||
// finite count, expansion required if initial not found *or* not enough
|
||||
// items were pulled in
|
||||
expansion_required = !initial_id_found || (fulfilled_count + unfulfilled.size < count);
|
||||
} else {
|
||||
// initial_id == null
|
||||
// finite count, expansion required if not enough found
|
||||
expansion_required = (fulfilled_count + unfulfilled.size < count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the vector is too short, expand it now
|
||||
if (expansion_required) {
|
||||
Gee.List<Geary.Email>? expanded = yield expand_vector_async();
|
||||
if (expanded != null) {
|
||||
// take all the IDs from the expanded vector and call them unfulfilled; base class
|
||||
|
|
@ -85,6 +146,23 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai
|
|||
return yield base.replay_remote_async();
|
||||
}
|
||||
|
||||
private async Trillian is_fully_expanded_async() throws Error {
|
||||
int remote_count;
|
||||
owner.get_remote_counts(out remote_count, null);
|
||||
|
||||
// if unknown (unconnected), say so
|
||||
if (remote_count < 0)
|
||||
return Trillian.UNKNOWN;
|
||||
|
||||
// include marked for removed in the count in case this is being called while a removal
|
||||
// is in process, in which case don't want to expand vector this moment because the
|
||||
// vector is in flux
|
||||
int local_count_with_marked = yield owner.local_folder.get_email_count_async(
|
||||
ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable);
|
||||
|
||||
return Trillian.from_boolean(local_count_with_marked >= remote_count);
|
||||
}
|
||||
|
||||
private async Gee.List<Geary.Email>? expand_vector_async() throws Error {
|
||||
// watch out for situations where the entire folder is represented locally (i.e. no
|
||||
// expansion necessary)
|
||||
|
|
@ -97,18 +175,6 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai
|
|||
// vector is in flux
|
||||
int local_count = yield owner.local_folder.get_email_count_async(
|
||||
ImapDB.Folder.ListFlags.NONE, cancellable);
|
||||
int local_count_with_marked = yield owner.local_folder.get_email_count_async(
|
||||
ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable);
|
||||
|
||||
if (local_count_with_marked >= remote_count) {
|
||||
// watch for sync discrepencies ... this is not something that can be fixed up here
|
||||
if (local_count_with_marked > remote_count) {
|
||||
message("%s: not expanding vector remote_count=%d local_count=%d", to_string(),
|
||||
remote_count, local_count);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// determine low and high position for expansion ... default in most code paths for high
|
||||
// is the SequenceNumber just below the lowest known message, unless no local messages
|
||||
|
|
@ -169,6 +235,13 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai
|
|||
// low_pos must be defined by this point
|
||||
assert(low_pos != null);
|
||||
|
||||
if (high_pos != null && low_pos.value > high_pos.value) {
|
||||
debug("%s: Aborting vector expansion, low_pos=%s > high_pos=%s", owner.to_string(),
|
||||
low_pos.to_string(), high_pos.to_string());
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Imap.MessageSet msg_set;
|
||||
if (high_pos != null)
|
||||
msg_set = new Imap.MessageSet.range_by_first_last(low_pos, high_pos);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
|
|||
|
||||
private AccountInformation account_information;
|
||||
private Gee.HashSet<ClientSession> sessions = new Gee.HashSet<ClientSession>();
|
||||
private int pending_sessions = 0;
|
||||
private Nonblocking.Mutex sessions_mutex = new Nonblocking.Mutex();
|
||||
private Gee.HashSet<ClientSession> reserved_sessions = new Gee.HashSet<ClientSession>();
|
||||
private bool authentication_failed = false;
|
||||
|
|
@ -133,16 +134,8 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
|
|||
return;
|
||||
}
|
||||
|
||||
while (sessions.size < min_pool_size && !authentication_failed && is_open) {
|
||||
try {
|
||||
yield create_new_authorized_session(null);
|
||||
} catch (Error err) {
|
||||
debug("Unable to create authorized session to %s: %s",
|
||||
account_information.get_imap_endpoint().to_string(), err.message);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
while ((sessions.size + pending_sessions) < min_pool_size && !authentication_failed && is_open)
|
||||
schedule_new_authorized_session();
|
||||
|
||||
try {
|
||||
sessions_mutex.release(ref token);
|
||||
|
|
@ -151,6 +144,26 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
|
|||
}
|
||||
}
|
||||
|
||||
private void schedule_new_authorized_session() {
|
||||
pending_sessions++;
|
||||
|
||||
create_new_authorized_session.begin(null, on_created_new_authorized_session);
|
||||
}
|
||||
|
||||
private void on_created_new_authorized_session(Object? source, AsyncResult result) {
|
||||
pending_sessions--;
|
||||
|
||||
try {
|
||||
create_new_authorized_session.end(result);
|
||||
} catch (Error err) {
|
||||
debug("Unable to create authorized session to %s: %s",
|
||||
account_information.get_imap_endpoint().to_string(), err.message);
|
||||
|
||||
// try again
|
||||
adjust_session_pool.begin();
|
||||
}
|
||||
}
|
||||
|
||||
// This should only be called when sessions_mutex is locked.
|
||||
private async ClientSession create_new_authorized_session(Cancellable? cancellable) throws Error {
|
||||
if (authentication_failed)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
* recv-collapsed: #f5f5f5
|
||||
*
|
||||
* Background colors associated with sent emails:
|
||||
* sent-normal: #ffd
|
||||
* sent-quoted: #eeb
|
||||
* sent-collapsed: #f7f7c7
|
||||
* sent-normal: white
|
||||
* sent-quoted: #e8e8e8
|
||||
* sent-collapsed: #f5f5f5
|
||||
*/
|
||||
@media print {
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ hr {
|
|||
}
|
||||
|
||||
.email.sent {
|
||||
background-color: #ffd;/* sent-normal */
|
||||
background-color: white;/* sent-normal */
|
||||
}
|
||||
|
||||
.email .starred {
|
||||
|
|
@ -241,7 +241,7 @@ body:not(.nohide) .email.hide .header_container .avatar {
|
|||
}
|
||||
body:not(.nohide) .email.sent.hide,
|
||||
body:not(.nohide) .email.sent .email.hide {
|
||||
background-color: #f7f7c7;/* sent-collapsed */
|
||||
background-color: #f5f5f5;/* sent-collapsed */
|
||||
}
|
||||
body:not(.nohide) .email.hide .body,
|
||||
body:not(.nohide) .email.hide > .attachment_container,
|
||||
|
|
@ -317,7 +317,7 @@ body:not(.nohide) .email.hide .header_container .avatar {
|
|||
cursor: hand;
|
||||
}
|
||||
.email.sent .compressed_note > span {
|
||||
background-color: #f7f7c7;/* sent-collapsed */
|
||||
background-color: #f5f5f5;/* sent-collapsed */
|
||||
}
|
||||
body.nohide .email .compressed_note > span {
|
||||
display: none !important;
|
||||
|
|
@ -331,7 +331,7 @@ body.nohide .email .compressed_note > span {
|
|||
background-color: white;/* recv-normal */
|
||||
}
|
||||
.email.sent .email {
|
||||
background-color: #ffd;/* sent-normal */
|
||||
background-color: white;/* sent-normal */
|
||||
}
|
||||
.email .email .email_container .menu,
|
||||
.email .email .email_container .starred,
|
||||
|
|
@ -475,7 +475,7 @@ body.nohide .email .compressed_note > span {
|
|||
}
|
||||
|
||||
.email.sent .quote_container {
|
||||
background-color: #eeb;/* sent-quoted */
|
||||
background-color: #e8e8e8;/* sent-quoted */
|
||||
}
|
||||
|
||||
.quote_container > .shower,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@
|
|||
<!-- interface-requires gtk+ 3.0 -->
|
||||
<object class="GtkActionGroup" id="actions">
|
||||
<child>
|
||||
<object class="GtkAction" id="ok_action">
|
||||
<property name="stock_id">gtk-ok</property>
|
||||
</object>
|
||||
<object class="GtkAction" id="ok_action"/>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkBox" id="container">
|
||||
|
|
@ -17,7 +15,8 @@
|
|||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="yalign">0</property>
|
||||
<property name="stock">gtk-dialog-error</property>
|
||||
<property name="pixel_size">60</property>
|
||||
<property name="icon_name">dialog-error</property>
|
||||
<property name="icon-size">6</property>
|
||||
</object>
|
||||
<packing>
|
||||
|
|
@ -72,7 +71,8 @@
|
|||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="button2">
|
||||
<property name="label" translatable="yes">_Remove</property>
|
||||
<property name="label" translatable="yes">_OK</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="related_action">ok_action</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@
|
|||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="close">
|
||||
<property name="stock_id">gtk-close</property>
|
||||
</object>
|
||||
<object class="GtkAction" id="close"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="edit_account">
|
||||
|
|
@ -122,11 +120,13 @@
|
|||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="label" translatable="yes">_Close</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="related_action">close</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="yalign">0.54000002145767212</property>
|
||||
</object>
|
||||
<packing>
|
||||
|
|
|
|||
|
|
@ -3,57 +3,43 @@
|
|||
<!-- interface-requires gtk+ 3.0 -->
|
||||
<object class="GtkActionGroup" id="compose actions">
|
||||
<child>
|
||||
<object class="GtkAction" id="undo">
|
||||
<property name="stock_id">gtk-undo</property>
|
||||
</object>
|
||||
<object class="GtkAction" id="undo"/>
|
||||
<accelerator key="z" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="redo">
|
||||
<property name="stock_id">gtk-redo</property>
|
||||
</object>
|
||||
<object class="GtkAction" id="redo"/>
|
||||
<accelerator key="z" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="cut">
|
||||
<property name="stock_id">gtk-cut</property>
|
||||
</object>
|
||||
<object class="GtkAction" id="cut"/>
|
||||
<accelerator key="x" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="copy">
|
||||
<property name="stock_id">gtk-copy</property>
|
||||
</object>
|
||||
<object class="GtkAction" id="copy"/>
|
||||
<accelerator key="c" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="paste">
|
||||
<property name="stock_id">gtk-paste</property>
|
||||
</object>
|
||||
<object class="GtkAction" id="paste"/>
|
||||
<accelerator key="v" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="justifyleft">
|
||||
<property name="label" translatable="yes">_Left</property>
|
||||
<property name="stock_id">gtk-justify-left</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="justifyright">
|
||||
<property name="label" translatable="yes">_Right</property>
|
||||
<property name="stock_id">gtk-justify-right</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="justifycenter">
|
||||
<property name="label" translatable="yes">_Center</property>
|
||||
<property name="stock_id">gtk-justify-center</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="justifyfull">
|
||||
<property name="label" translatable="yes">_Justify</property>
|
||||
<property name="stock_id">gtk-justify-fill</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
|
@ -66,27 +52,26 @@
|
|||
<child>
|
||||
<object class="GtkAction" id="color">
|
||||
<property name="label" translatable="yes">C_olor</property>
|
||||
<property name="stock_id">gtk-select-color</property>
|
||||
</object>
|
||||
<accelerator key="r" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="menu">
|
||||
<property name="label" translatable="yes">Menu</property>
|
||||
<property name="stock_id">gtk-go-down</property>
|
||||
<property name="icon_name">go-down</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="indent">
|
||||
<property name="label" translatable="yes">Quote text</property>
|
||||
<property name="stock_id">gtk-indent</property>
|
||||
<property name="icon_name">format-indent-more</property>
|
||||
</object>
|
||||
<accelerator key="bracketright" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="outdent">
|
||||
<property name="label" translatable="yes">Unquote text</property>
|
||||
<property name="stock_id">gtk-unindent</property>
|
||||
<property name="icon_name">format-indent-less</property>
|
||||
</object>
|
||||
<accelerator key="bracketleft" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
|
|
@ -110,25 +95,25 @@
|
|||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleAction" id="bold">
|
||||
<property name="stock_id">gtk-bold</property>
|
||||
<property name="icon_name">format-text-bold</property>
|
||||
</object>
|
||||
<accelerator key="b" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleAction" id="italic">
|
||||
<property name="stock_id">gtk-italic</property>
|
||||
<property name="icon_name">format-text-italic</property>
|
||||
</object>
|
||||
<accelerator key="i" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleAction" id="underline">
|
||||
<property name="stock_id">gtk-underline</property>
|
||||
<property name="icon_name">format-text-underline</property>
|
||||
</object>
|
||||
<accelerator key="u" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleAction" id="strikethrough">
|
||||
<property name="stock_id">gtk-strikethrough</property>
|
||||
<property name="icon_name">format-text-strikethrough</property>
|
||||
</object>
|
||||
<accelerator key="k" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
|
|
@ -697,11 +682,11 @@
|
|||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="Discard">
|
||||
<property name="label">gtk-discard</property>
|
||||
<property name="label">_Discard</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="use_underline">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<object class="GtkImage" id="image2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">gtk-close</property>
|
||||
<property name="icon_name">window-close</property>
|
||||
<property name="icon-size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="image3">
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@
|
|||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="yalign">0</property>
|
||||
<property name="stock">gtk-dialog-authentication</property>
|
||||
<property name="pixel_size">60</property>
|
||||
<property name="icon_name">security-high</property>
|
||||
<property name="icon-size">6</property>
|
||||
</object>
|
||||
<packing>
|
||||
|
|
|
|||
|
|
@ -18,15 +18,13 @@
|
|||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="label">gtk-close</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="label">_Close</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="has_default">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="use_underline">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
|
@ -69,7 +67,6 @@
|
|||
<child>
|
||||
<object class="GtkCheckButton" id="autoselect">
|
||||
<property name="label" translatable="yes">_Automatically select next message</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
|
|
@ -77,7 +74,6 @@
|
|||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
|
|
@ -92,7 +88,6 @@
|
|||
<child>
|
||||
<object class="GtkCheckButton" id="display_preview">
|
||||
<property name="label" translatable="yes">_Display conversation preview</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
|
|
@ -100,7 +95,6 @@
|
|||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
|
|
@ -135,7 +129,6 @@
|
|||
<child>
|
||||
<object class="GtkCheckButton" id="spell_check">
|
||||
<property name="label" translatable="yes">Enable _spell checking</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
|
|
@ -143,7 +136,6 @@
|
|||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
|
|
@ -178,7 +170,6 @@
|
|||
<child>
|
||||
<object class="GtkCheckButton" id="play_sounds">
|
||||
<property name="label" translatable="yes">_Play notification sounds</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
|
|
@ -186,7 +177,6 @@
|
|||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
|
|
@ -201,7 +191,6 @@
|
|||
<child>
|
||||
<object class="GtkCheckButton" id="show_notifications">
|
||||
<property name="label" translatable="yes">Show _notifications for new mail</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
|
|
@ -209,7 +198,6 @@
|
|||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
|
|
@ -221,6 +209,9 @@
|
|||
<property name="height">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,10 @@
|
|||
<!-- interface-requires gtk+ 3.0 -->
|
||||
<object class="GtkActionGroup" id="actions">
|
||||
<child>
|
||||
<object class="GtkAction" id="cancel_action">
|
||||
<property name="stock_id">gtk-cancel</property>
|
||||
</object>
|
||||
<object class="GtkAction" id="cancel_action"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="remove_action">
|
||||
<property name="stock_id">gtk-remove</property>
|
||||
</object>
|
||||
<object class="GtkAction" id="remove_action"/>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkBox" id="container">
|
||||
|
|
@ -22,7 +18,8 @@
|
|||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="yalign">0</property>
|
||||
<property name="stock">gtk-dialog-warning</property>
|
||||
<property name="pixel_size">60</property>
|
||||
<property name="icon_name">dialog-warning</property>
|
||||
<property name="icon-size">6</property>
|
||||
</object>
|
||||
<packing>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue