Merge branch 'master' into feature/drafts

This commit is contained in:
Eric Gregory 2013-08-08 13:07:51 -07:00
commit c8cbebc39c
48 changed files with 760 additions and 386 deletions

2
debian/control vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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("&#x91;", "<b>").replace("&#x92;", "</b>");
return matched;
}
@ -291,43 +199,5 @@ public class ContactEntryCompletion : Gtk.EntryCompletion {
return false;
}
private int sort_func(Gtk.TreeModel model, Gtk.TreeIter aiter, Gtk.TreeIter biter) {
// Order by importance, then by real name, then by email.
GLib.Value avalue, bvalue;
model.get_value(aiter, Column.CONTACT_OBJECT, out avalue);
model.get_value(biter, Column.CONTACT_OBJECT, out bvalue);
Geary.Contact? acontact = avalue.get_object() as Geary.Contact;
Geary.Contact? bcontact = bvalue.get_object() as Geary.Contact;
// Contacts can be null if the sort func is called between TreeModel.append and
// TreeModel.set.
if (acontact == bcontact)
return 0;
if (acontact == null && bcontact != null)
return -1;
if (acontact != null && bcontact == null)
return 1;
// First order by importance.
if (acontact.highest_importance > bcontact.highest_importance)
return -1;
if (acontact.highest_importance < bcontact.highest_importance)
return 1;
// Then order by real name.
string? anormalized_real_name = acontact.real_name == null ? null :
acontact.real_name.normalize().casefold();
string? bnormalized_real_name = bcontact.real_name == null ? null :
bcontact.real_name.normalize().casefold();
// strcmp correctly marks 'null' as first in lexigraphic order, so we don't need to
// special-case it.
int result = strcmp(anormalized_real_name, bnormalized_real_name);
if (result != 0)
return result;
// Finally, order by email.
return strcmp(acontact.normalized_email, bcontact.normalized_email);
}
}

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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