Revamp status bar

* Increase the size of the spinner; fix #7184
* In the event of an error sending a message, display text in the status
  bar indicating as such (and display a notification too); fix #7481
* Display a status bar message and spin the spinner when sending;
  fix #4629
* Re-add the sound played when a message is sent; fix #5429
This commit is contained in:
Charles Lindsay 2013-09-17 16:55:03 -07:00
parent c8b4f9e0e2
commit 542b2ff914
12 changed files with 251 additions and 38 deletions

View file

@ -351,6 +351,7 @@ client/ui/main-window.vala
client/ui/monitored-progress-bar.vala
client/ui/monitored-spinner.vala
client/ui/pill-toolbar.vala
client/ui/status-bar.vala
client/ui/stock.vala
client/util/util-date.vala

View file

@ -12,13 +12,13 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In
public FolderEntry(Geary.Folder folder) {
base(folder);
has_new = false;
folder.properties.notify[Geary.FolderProperties.PROP_NAME_EMAIL_UNREAD].connect(
on_email_unread_count_changed);
folder.properties.notify[Geary.FolderProperties.PROP_NAME_EMAIL_TOTAL].connect(on_counts_changed);
folder.properties.notify[Geary.FolderProperties.PROP_NAME_EMAIL_UNREAD].connect(on_counts_changed);
}
~FolderEntry() {
folder.properties.notify[Geary.FolderProperties.PROP_NAME_EMAIL_UNREAD].disconnect(
on_email_unread_count_changed);
folder.properties.notify[Geary.FolderProperties.PROP_NAME_EMAIL_TOTAL].disconnect(on_counts_changed);
folder.properties.notify[Geary.FolderProperties.PROP_NAME_EMAIL_UNREAD].disconnect(on_counts_changed);
}
public override string get_sidebar_name() {
@ -111,7 +111,7 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In
return true;
}
private void on_email_unread_count_changed() {
private void on_counts_changed() {
sidebar_count_changed(get_count());
sidebar_tooltip_changed(get_sidebar_tooltip());
}

View file

@ -396,11 +396,13 @@ public class GearyController : Geary.BaseObject {
private void open_account(Geary.Account account) {
account.report_problem.connect(on_report_problem);
account.email_removed.connect(on_account_email_removed);
connect_account_async.begin(account, cancellable_open_account);
}
private void close_account(Geary.Account account) {
account.report_problem.disconnect(on_report_problem);
account.email_removed.disconnect(on_account_email_removed);
disconnect_account_async.begin(account);
}
@ -581,11 +583,61 @@ public class GearyController : Geary.BaseObject {
close_account(account);
break;
case Geary.Account.Problem.EMAIL_DELIVERY_FAILURE:
handle_send_failure();
break;
default:
assert_not_reached();
}
}
private void handle_send_failure() {
bool activate_message = false;
try {
// Due to a timing hole where it's possible to delete a message
// from the outbox after the SMTP queue has picked it up and is
// in the process of sending it, we only want to display a message
// telling the user there's a problem if there are any other
// messages waiting to be sent on any account.
foreach (Geary.AccountInformation info in Geary.Engine.instance.get_accounts().values) {
Geary.Account account = Geary.Engine.instance.get_account_instance(info);
if (account.is_open()) {
Geary.Folder? outbox = account.get_special_folder(Geary.SpecialFolderType.OUTBOX);
if (outbox != null && outbox.properties.email_total > 0) {
activate_message = true;
break;
}
}
}
} catch (Error e) {
debug("Error determining whether any outbox has messages: %s", e.message);
activate_message = true;
}
if (activate_message) {
if (!main_window.status_bar.is_message_active(StatusBar.Message.OUTBOX_SEND_FAILURE))
main_window.status_bar.activate_message(StatusBar.Message.OUTBOX_SEND_FAILURE);
libnotify.set_error_notification(_("Error sending email"),
_("Geary encountered an error sending an email. If the problem persists, please manually delete the email from your Outbox folder."));
}
}
private void on_account_email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
if (folder.special_folder_type == Geary.SpecialFolderType.OUTBOX) {
main_window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SEND_FAILURE);
libnotify.clear_error_notification();
}
}
private void on_sending_started() {
main_window.status_bar.activate_message(StatusBar.Message.OUTBOX_SENDING);
}
private void on_sending_finished() {
main_window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SENDING);
}
// Removes an existing account.
public async void remove_account_async(Geary.AccountInformation account,
Cancellable? cancellable = null) {
@ -599,6 +651,8 @@ public class GearyController : Geary.BaseObject {
public async void connect_account_async(Geary.Account account, Cancellable? cancellable = null) {
account.folders_available_unavailable.connect(on_folders_available_unavailable);
account.sending_monitor.start.connect(on_sending_started);
account.sending_monitor.finish.connect(on_sending_finished);
bool retry = false;
do {
@ -712,6 +766,8 @@ public class GearyController : Geary.BaseObject {
cancel_folder();
account.folders_available_unavailable.disconnect(on_folders_available_unavailable);
account.sending_monitor.start.disconnect(on_sending_started);
account.sending_monitor.finish.disconnect(on_sending_finished);
if (main_window.conversation_list_store.account_owner_email == account.information.email)
main_window.conversation_list_store.account_owner_email = null;

View file

@ -13,6 +13,7 @@ public class Libnotify : Geary.BaseObject {
private NewMessagesMonitor monitor;
private Notify.Notification? current_notification = null;
private Notify.Notification? error_notification = null;
private Geary.Folder? folder = null;
private Geary.Email? email = null;
private unowned List<string> caps;
@ -75,7 +76,7 @@ public class Libnotify : Geary.BaseObject {
body, total);
}
issue_notification(folder.account.information.email, body, null);
issue_current_notification(folder.account.information.email, body, null);
}
private async void notify_one_message_async(Geary.Folder folder, Geary.Email email,
@ -128,10 +129,10 @@ public class Libnotify : Geary.BaseObject {
ins = null;
}
issue_notification(primary.get_short_address(), body, avatar);
issue_current_notification(primary.get_short_address(), body, avatar);
}
private void issue_notification(string summary, string body, Gdk.Pixbuf? icon) {
private void issue_current_notification(string summary, string body, Gdk.Pixbuf? icon) {
// only one outstanding notification at a time
if (current_notification != null) {
try {
@ -143,32 +144,42 @@ public class Libnotify : Geary.BaseObject {
current_notification = null;
}
current_notification = issue_notification("email.arrived", summary, body, icon, "message-new_email");
}
private Notify.Notification issue_notification(string category, string summary,
string body, Gdk.Pixbuf? icon, string? sound) {
// Avoid constructor due to ABI change
current_notification = (Notify.Notification) GLib.Object.new(
Notify.Notification notification = (Notify.Notification) GLib.Object.new(
typeof (Notify.Notification),
"icon-name", "geary",
"summary", GLib.Environment.get_application_name());
current_notification.set_hint_string("desktop-entry", "geary");
notification.set_hint_string("desktop-entry", "geary");
if (caps.find_custom("actions", GLib.strcmp) != null)
current_notification.add_action("default", _("Open"), on_default_action);
notification.add_action("default", _("Open"), on_default_action);
current_notification.set_category("email.arrived");
current_notification.set("summary", summary);
current_notification.set("body", body);
notification.set_category(category);
notification.set("summary", summary);
notification.set("body", body);
if (icon != null)
current_notification.set_image_from_pixbuf(icon);
notification.set_image_from_pixbuf(icon);
if (caps.find("sound") != null)
current_notification.set_hint_string("sound-name", "message-new-email");
else
play_sound("message-new-email");
if (sound != null) {
if (caps.find("sound") != null)
notification.set_hint_string("sound-name", sound);
else
play_sound(sound);
}
try {
current_notification.show();
notification.show();
} catch (Error err) {
message("Unable to show notification: %s", err.message);
}
return notification;
}
public static void play_sound(string sound) {
@ -178,5 +189,28 @@ public class Libnotify : Geary.BaseObject {
init_sound();
sound_context.play(0, Canberra.PROP_EVENT_ID, sound);
}
public void set_error_notification(string summary, string body) {
// Only one error at a time, guys. (This means subsequent errors will
// be dropped. Since this is only used for one thing now, that's ok,
// but it means in the future, a more robust system will be needed.)
if (error_notification != null)
return;
error_notification = issue_notification("email", summary, body,
IconFactory.instance.application_icon, null);
}
public void clear_error_notification() {
if (error_notification != null) {
try {
error_notification.close();
} catch (Error err) {
debug("Unable to close libnotify error notification: %s", err.message);
}
error_notification = null;
}
}
}

View file

@ -7,12 +7,14 @@
public class MainWindow : Gtk.Window {
private const int MESSAGE_LIST_WIDTH = 250;
private const int FOLDER_LIST_WIDTH = 100;
private const int STATUS_BAR_HEIGHT = 18;
public FolderList.Tree folder_list { get; private set; default = new FolderList.Tree(); }
public ConversationListStore conversation_list_store { get; private set; default = new ConversationListStore(); }
public MainToolbar main_toolbar { get; private set; }
public ConversationListView conversation_list_view { get; private set; }
public ConversationViewer conversation_viewer { get; private set; default = new ConversationViewer(); }
public StatusBar status_bar { get; private set; default = new StatusBar(); }
public int window_width { get; set; }
public int window_height { get; set; }
@ -118,7 +120,9 @@ public class MainWindow : Gtk.Window {
// Three-pane display.
Gtk.Box status_bar_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
Gtk.Statusbar status_bar = new Gtk.Statusbar();
status_bar.set_size_request(-1, STATUS_BAR_HEIGHT);
status_bar.set_border_width(2);
spinner.set_size_request(STATUS_BAR_HEIGHT - 2, -1);
status_bar.add(spinner);
status_bar_box.pack_start(folder_frame);
status_bar_box.pack_start(status_bar, false, false, 0);
@ -179,16 +183,18 @@ public class MainWindow : Gtk.Window {
private void on_account_available(Geary.AccountInformation account) {
try {
progress_monitor.add(Geary.Engine.instance.get_account_instance(account).opening_monitor);
progress_monitor.add(Geary.Engine.instance.get_account_instance(account).sending_monitor);
} catch (Error e) {
debug("Could not access account opening progress monitor: %s", e.message);
debug("Could not access account progress monitors: %s", e.message);
}
}
private void on_account_unavailable(Geary.AccountInformation account) {
try {
progress_monitor.remove(Geary.Engine.instance.get_account_instance(account).opening_monitor);
progress_monitor.remove(Geary.Engine.instance.get_account_instance(account).sending_monitor);
} catch (Error e) {
debug("Could not access account opening progress monitor: %s", e.message);
debug("Could not access account progress monitors: %s", e.message);
}
}
}

View file

@ -0,0 +1,100 @@
/* 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.
*/
/**
* A wrapper around Gtk.Statusbar that predefines messages and context areas so
* you don't have to keep track of them elsewhere. You can activate and
* deactivate messages, instead of worrying about context areas and stacks.
* Internally, activations are reference counted, and every new activation
* pushes the message to the top of its context area's stack. Only when
* the number of deactivations equals the number of activations is the message
* removed from the stack entirely.
*/
public class StatusBar : Gtk.Statusbar {
public enum Message {
OUTBOX_SENDING,
OUTBOX_SEND_FAILURE;
internal string get_text() {
switch (this) {
case Message.OUTBOX_SENDING:
/// Displayed in the space-limited status bar while a message is in the process of being sent.
return _("Sending...");
case Message.OUTBOX_SEND_FAILURE:
/// Displayed in the space-limited status bar when a message fails to be sent due to error.
return _("Error sending email");
default:
assert_not_reached();
}
}
internal Context get_context() {
switch (this) {
case Message.OUTBOX_SENDING:
return Context.OUTBOX;
case Message.OUTBOX_SEND_FAILURE:
return Context.OUTBOX;
default:
assert_not_reached();
}
}
}
internal enum Context {
OUTBOX,
}
private Gee.HashMap<Context, uint> context_ids = new Gee.HashMap<Context, uint>();
private Gee.HashMap<Message, uint> message_ids = new Gee.HashMap<Message, uint>();
private Gee.HashMap<Message, int> message_counts = new Gee.HashMap<Message, int>();
public StatusBar() {
set_context_id(Context.OUTBOX);
}
private void set_context_id(Context context) {
context_ids.set(context, get_context_id(context.to_string()));
}
private int get_count(Message message) {
return (message_counts.has_key(message) ? message_counts.get(message) : 0);
}
private void push_message(Message message) {
message_ids.set(message, push(context_ids.get(message.get_context()), message.get_text()));
}
private void remove_message(Message message) {
remove(context_ids.get(message.get_context()), message_ids.get(message));
message_ids.unset(message);
}
/**
* Return whether the message has been activated more times than it has
* been deactivated.
*/
public bool is_message_active(Message message) {
return message_ids.has_key(message);
}
public void activate_message(Message message) {
if (is_message_active(message))
remove_message(message);
push_message(message);
message_counts.set(message, get_count(message) + 1);
}
public void deactivate_message(Message message) {
if (!is_message_active(message))
return;
int count = get_count(message);
if (count == 1)
remove_message(message);
message_counts.set(message, count - 1);
}
}

View file

@ -9,6 +9,7 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account {
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; }
public Geary.ProgressMonitor sending_monitor { get; protected set; }
private string name;

View file

@ -10,7 +10,8 @@ public interface Geary.Account : BaseObject {
SEND_EMAIL_LOGIN_FAILED,
HOST_UNREACHABLE,
NETWORK_UNAVAILABLE,
DATABASE_FAILURE
DATABASE_FAILURE,
EMAIL_DELIVERY_FAILURE,
}
public abstract Geary.AccountInformation information { get; protected set; }
@ -18,6 +19,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 abstract Geary.ProgressMonitor sending_monitor { get; protected set; }
public signal void opened();

View file

@ -27,6 +27,8 @@ private class Geary.ImapDB.Account : BaseObject {
}
}
public signal void email_sent(Geary.RFC822.Message rfc822);
// Only available when the Account is opened
public SmtpOutboxFolder? outbox { get; private set; default = null; }
public SearchFolder? search_folder { get; private set; default = null; }
@ -35,6 +37,8 @@ private class Geary.ImapDB.Account : BaseObject {
default = new IntervalProgressMonitor(ProgressType.SEARCH_INDEX, 0, 0); }
public SimpleProgressMonitor upgrade_monitor { get; private set; default = new SimpleProgressMonitor(
ProgressType.DB_UPGRADE); }
public SimpleProgressMonitor sending_monitor { get; private set;
default = new SimpleProgressMonitor(ProgressType.ACTIVITY); }
private string name;
private AccountInformation account_information;
@ -134,7 +138,8 @@ private class Geary.ImapDB.Account : BaseObject {
initialize_contacts(cancellable);
// ImapDB.Account holds the Outbox, which is tied to the database it maintains
outbox = new SmtpOutboxFolder(db, account);
outbox = new SmtpOutboxFolder(db, account, sending_monitor);
outbox.email_sent.connect(on_outbox_email_sent);
// Search folder
search_folder = ((ImapEngine.GenericAccount) account).new_search_folder();
@ -154,10 +159,15 @@ private class Geary.ImapDB.Account : BaseObject {
background_cancellable.cancel();
background_cancellable = null;
outbox.email_sent.disconnect(on_outbox_email_sent);
outbox = null;
search_folder = null;
}
private void on_outbox_email_sent(Geary.RFC822.Message rfc822) {
email_sent(rfc822);
}
public async void clone_folder_async(Geary.Imap.Folder imap_folder, Cancellable? cancellable = null)
throws Error {
check_open();

View file

@ -56,18 +56,21 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
private ImapDB.Database db;
private weak Account _account;
private Geary.ProgressMonitor sending_monitor;
private Geary.Smtp.ClientSession smtp;
private Nonblocking.Mailbox<OutboxRow> outbox_queue = new Nonblocking.Mailbox<OutboxRow>();
private SmtpOutboxFolderProperties _properties = new SmtpOutboxFolderProperties(0, 0);
private int64 next_ordering = 0;
public signal void report_problem(Geary.Account.Problem problem, Error? err);
public signal void email_sent(Geary.RFC822.Message rfc822);
// Requires the Database from the get-go because it runs a background task that access it
// whether open or not
public SmtpOutboxFolder(ImapDB.Database db, Account account) {
public SmtpOutboxFolder(ImapDB.Database db, Account account, Geary.ProgressMonitor sending_monitor) {
this.db = db;
_account = account;
this.sending_monitor = sending_monitor;
smtp = new Geary.Smtp.ClientSession(_account.information.get_smtp_endpoint());
@ -219,6 +222,8 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
if (report)
report_problem(Geary.Account.Problem.SEND_EMAIL_LOGIN_FAILED, send_err);
} else {
report_problem(Geary.Account.Problem.EMAIL_DELIVERY_FAILURE, send_err);
}
if (should_nap) {
@ -243,14 +248,6 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
debug("Outbox postman: Unable to remove row from database: %s", rm_err.message);
}
// update properties
try {
_properties.set_total(yield get_email_count_async(null));
} catch (Error err) {
debug("Outbox postman: Unable to fetch updated email count for properties: %s",
err.message);
}
// If we got this far the send was successful, so reset the send retry interval.
send_retry_seconds = MIN_SEND_RETRY_INTERVAL_SEC;
}
@ -503,6 +500,8 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
if (removed.size == 0)
return false;
_properties.set_total(final_count);
notify_email_removed(removed);
notify_email_count_changed(final_count, CountChangeReason.REMOVED);
@ -531,8 +530,9 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
private async void send_email_async(Geary.RFC822.Message rfc822, Cancellable? cancellable)
throws Error {
Error? smtp_err = null;
sending_monitor.notify_start();
Error? smtp_err = null;
try {
yield smtp.login_async(_account.information.smtp_credentials, cancellable);
} catch (Error login_err) {
@ -557,8 +557,12 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
debug("Unable to disconnect from SMTP server %s: %s", smtp.to_string(), err.message);
}
sending_monitor.notify_finish();
if (smtp_err != null)
throw smtp_err;
email_sent(rfc822);
}
private async bool ordering_exists_async(int64 ordering, Cancellable? cancellable) throws Error {

View file

@ -29,11 +29,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
this.local = local;
this.remote.login_failed.connect(on_login_failed);
this.remote.email_sent.connect(on_email_sent);
this.local.email_sent.connect(on_email_sent);
search_upgrade_monitor = local.search_index_monitor;
db_upgrade_monitor = local.upgrade_monitor;
opening_monitor = new Geary.SimpleProgressMonitor(Geary.ProgressType.ACTIVITY);
sending_monitor = local.sending_monitor;
if (outbox_path == null) {
outbox_path = new SmtpOutboxFolderRoot();

View file

@ -29,8 +29,6 @@ private class Geary.Imap.Account : BaseObject {
private Gee.List<MailboxInformation>? list_collector = null;
private Gee.List<StatusData>? status_collector = null;
public signal void email_sent(Geary.RFC822.Message rfc822);
public signal void login_failed(Geary.Credentials cred);
public Account(Geary.AccountInformation account_information) {