From 80680f280ea122cfd1c4f569d107c497c392e41e Mon Sep 17 00:00:00 2001 From: Michael James Gratton Date: Wed, 8 Nov 2017 18:13:51 +1100 Subject: [PATCH 01/13] Add initial support for using Gtk.InfoBars to display errors. * src/client/components/main-window-info-bar.vala (MainWindowInfoBar): New class and UI file for displaying an info bar in the main window, as well as providing a way to show technical informartion if needed. * src/client/components/main-window.vala (MainWindow): Add a Gtk.Frame and container to hold inforbar instances. Show it when showing an infobar, and hide it when there are none. Add show_infobar() method to provide a cromulent way of adding info bars. * src/client/application/geary-controller.vala (BaseObject): Rather than bailing out on an account when an error occurs, display an info bar instead. * ui/geary.css: Style the info bar frame to only show a border between main content and the info bars. --- po/POTFILES.in | 2 + src/CMakeLists.txt | 1 + src/client/application/geary-controller.vala | 74 ++++-- .../components/main-window-info-bar.vala | 245 ++++++++++++++++++ src/client/components/main-window.vala | 21 ++ ui/CMakeLists.txt | 1 + ui/geary.css | 10 + ui/main-window-info-bar.ui | 142 ++++++++++ ui/main-window.ui | 128 ++++++--- 9 files changed, 560 insertions(+), 64 deletions(-) create mode 100644 src/client/components/main-window-info-bar.vala create mode 100644 ui/main-window-info-bar.ui diff --git a/po/POTFILES.in b/po/POTFILES.in index 1f25f476..1969cbaf 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -31,6 +31,7 @@ src/client/components/folder-popover.vala src/client/components/icon-factory.vala src/client/components/main-toolbar.vala src/client/components/main-window.vala +src/client/components/main-window-info-bar.vala src/client/components/monitored-progress-bar.vala src/client/components/monitored-spinner.vala src/client/components/search-bar.vala @@ -411,6 +412,7 @@ ui/login.glade ui/main-toolbar.ui ui/main-toolbar-menus.ui ui/main-window.ui +ui/main-window-info-bar.ui ui/password-dialog.glade ui/preferences-dialog.ui ui/remove_confirm.glade diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 218f5255..efe24031 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -342,6 +342,7 @@ client/components/folder-popover.vala client/components/icon-factory.vala client/components/main-toolbar.vala client/components/main-window.vala +client/components/main-window-info-bar.vala client/components/monitored-progress-bar.vala client/components/monitored-spinner.vala client/components/search-bar.vala diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala index 451218f9..910af28a 100644 --- a/src/client/application/geary-controller.vala +++ b/src/client/application/geary-controller.vala @@ -872,44 +872,66 @@ public class GearyController : Geary.BaseObject { debug("Error updating stored passwords: %s", e.message); } } - + private void on_report_problem(Geary.Account account, Geary.Account.Problem problem, Error? err) { debug("Reported problem: %s Error: %s", problem.to_string(), err != null ? err.message : "(N/A)"); - + switch (problem) { - case Geary.Account.Problem.DATABASE_FAILURE: - case Geary.Account.Problem.HOST_UNREACHABLE: - case Geary.Account.Problem.NETWORK_UNAVAILABLE: - // TODO - break; - - case Geary.Account.Problem.RECV_EMAIL_LOGIN_FAILED: - case Geary.Account.Problem.SEND_EMAIL_LOGIN_FAILED: - // At this point, we've prompted them for the password and - // they've hit cancel, so there's not much for us to do here. - close_account(account); + case Geary.Account.Problem.CONNECTION_FAILURE: + ErrorDialog dialog = new ErrorDialog( + main_window, + _("Error connecting to the server"), + _("Geary encountered an error while connecting to the server. Please try again in a few moments.") + ); + dialog.run(); break; - case Geary.Account.Problem.SEND_EMAIL_DELIVERY_FAILURE: - handle_outbox_failure(StatusBar.Message.OUTBOX_SEND_FAILURE); + case Geary.Account.Problem.DATABASE_FAILURE: + case Geary.Account.Problem.HOST_UNREACHABLE: + case Geary.Account.Problem.NETWORK_UNAVAILABLE: + case Geary.Account.Problem.RECV_EMAIL_LOGIN_FAILED: + case Geary.Account.Problem.SEND_EMAIL_ERROR: + case Geary.Account.Problem.SEND_EMAIL_LOGIN_FAILED: + MainWindowInfoBar info_bar = new MainWindowInfoBar.for_problem( + problem, account, err + ); + info_bar.retry.connect(on_retry_problem); + this.main_window.show_infobar(info_bar); break; - case Geary.Account.Problem.SEND_EMAIL_SAVE_FAILED: - handle_outbox_failure(StatusBar.Message.OUTBOX_SAVE_SENT_MAIL_FAILED); + case Geary.Account.Problem.SEND_EMAIL_DELIVERY_FAILURE: + handle_outbox_failure(StatusBar.Message.OUTBOX_SEND_FAILURE); break; - case Geary.Account.Problem.CONNECTION_FAILURE: - ErrorDialog dialog = new ErrorDialog(main_window, - _("Error connecting to the server"), - _("Geary encountered an error while connecting to the server. Please try again in a few moments.")); - dialog.run(); + case Geary.Account.Problem.SEND_EMAIL_SAVE_FAILED: + handle_outbox_failure(StatusBar.Message.OUTBOX_SAVE_SENT_MAIL_FAILED); break; - default: - assert_not_reached(); + default: + assert_not_reached(); } } - + + private void on_retry_problem(MainWindowInfoBar info_bar) { + switch (info_bar.problem) { + case Geary.Account.Problem.RECV_EMAIL_LOGIN_FAILED: + break; + + case Geary.Account.Problem.SEND_EMAIL_ERROR: + break; + + case Geary.Account.Problem.SEND_EMAIL_LOGIN_FAILED: + break; + + default: + debug("Un-handled problem retry for %s: %s".printf( + info_bar.account.information.id, + info_bar.problem.to_string() + )); + break; + } + } + private void handle_outbox_failure(StatusBar.Message message) { bool activate_message = false; try { @@ -2817,5 +2839,5 @@ public class GearyController : Geary.BaseObject { }); } } -} +} diff --git a/src/client/components/main-window-info-bar.vala b/src/client/components/main-window-info-bar.vala new file mode 100644 index 00000000..a5a66919 --- /dev/null +++ b/src/client/components/main-window-info-bar.vala @@ -0,0 +1,245 @@ +/* + * Copyright 2017 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * Displays application-wide or important account-related messages. + */ +[GtkTemplate (ui = "/org/gnome/Geary/main-window-info-bar.ui")] +public class MainWindowInfoBar : Gtk.InfoBar { + + + private enum ResponseType { COPY, DETAILS, RETRY; } + + + /** If reporting a problem returns, the specific problem else null. */ + public Geary.Account.Problem? problem { get; private set; default = null; } + + /** If reporting a problem for an account, returns the account else null. */ + public Geary.Account? account { get; private set; default = null; } + + /** If reporting a problem, returns the error thrown, if any. */ + public Error error { get; private set; default = null; } + + + /** Emitted when the user clicks the Retry button, if any. */ + public signal void retry(); + + + [GtkChild] + private Gtk.Label title; + + [GtkChild] + private Gtk.Label description; + + [GtkChild] + private Gtk.Grid problem_details; + + [GtkChild] + private Gtk.TextView detail_text; + + + public MainWindowInfoBar.for_problem(Geary.Account.Problem problem, + Geary.Account account, + GLib.Error? error) { + string name = account.information.display_name; + Gtk.MessageType type = Gtk.MessageType.WARNING; + string title = ""; + string descr = ""; + string? retry = null; + bool show_close = false; + switch (problem) { + case Geary.Account.Problem.DATABASE_FAILURE: + type = Gtk.MessageType.ERROR; + title = _("A database problem has occurred"); + descr = _("Messages for %s must be downloaded again.").printf(name); + show_close = true; + break; + + case Geary.Account.Problem.HOST_UNREACHABLE: + // XXX should really be displaying the server name here + title = _("Could not contact server"); + descr = _("Please check %s server names are correct and are working.").printf(name); + show_close = true; + break; + + case Geary.Account.Problem.NETWORK_UNAVAILABLE: + title = _("Not connected to the Internet"); + descr = _("Please check your connection to the Internet."); + show_close = true; + break; + + case Geary.Account.Problem.RECV_EMAIL_LOGIN_FAILED: + title = _("Incoming mail password required"); + descr = _("Messages cannot be received for %s without the correct password.").printf(name); + retry = _("Retry receiving email, you will be prompted for a password"); + break; + + case Geary.Account.Problem.SEND_EMAIL_ERROR: + type = Gtk.MessageType.ERROR; + title = _("A problem occurred sending mail"); + descr = _("A message was unable to be sent for %s, try again in a moment").printf(name); + retry = _("Retry sending queued messages"); + break; + + case Geary.Account.Problem.SEND_EMAIL_LOGIN_FAILED: + title = _("Outgoing mail password required"); + descr = _("Messages cannot be sent for %s without the correct password.").printf(name); + retry = _("Retry sending queued messages, you will be prompted for a password"); + break; + + default: + debug("Un-handled problem type for %s: %s".printf( + account.information.id, problem.to_string() + )); + break; + } + + this(type, title, descr, show_close); + this.problem = problem; + this.account = account; + this.error = error; + + if (this.error != null) { + Gtk.Button details = add_button(_("_Details"), ResponseType.DETAILS); + details.tooltip_text = _("View technical details about the error"); + } + + if (retry != null) { + Gtk.Button retry_btn = add_button(_("_Retry"), ResponseType.RETRY); + retry_btn.tooltip_text = retry; + } + } + + protected MainWindowInfoBar(Gtk.MessageType type, + string title, + string description, + bool show_close) { + this.message_type = type; + this.title.label = title; + this.description.label = description; + this.show_close_button = show_close; + } + + private string format_details() { + string type = ""; + if (this.error != null) { + const string QUARK_SUFFIX = "-quark"; + string ugly_domain = this.error.domain.to_string(); + if (ugly_domain.has_suffix(QUARK_SUFFIX)) { + ugly_domain = ugly_domain.substring( + 0, ugly_domain.length - QUARK_SUFFIX.length + ); + } + StringBuilder nice_domain = new StringBuilder(); + foreach (string part in ugly_domain.split("_")) { + nice_domain.append(part.up(1)); + nice_domain.append(part.substring(1)); + } + + type = "%s %i".printf(nice_domain.str, this.error.code); + } + + return """Geary version: %s +GTK+ version: %u.%u.%u +Desktop: %s +Error type: %s +Message: %s +""".printf( + GearyApplication.VERSION, + Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version(), + Environment.get_variable("XDG_CURRENT_DESKTOP") ?? "Unknown", + type, + (this.error != null) ? error.message : "" + ); + } + + private void show_details() { + this.detail_text.buffer.text = format_details(); + + // Would love to construct the dialog in Builder, but we to + // construct the dialog manually since we can't adjust the + // Headerbar setting afterwards. If the user re-clicks on the + // Details button to re-show it, a whole bunch of GTK + // criticals are spewed and the dialog appears b0rked, so just + // do it from scratch ever time anyway. + bool use_header = Gtk.Settings.get_default().gtk_dialogs_use_header; + Gtk.DialogFlags flags = Gtk.DialogFlags.MODAL; + if (use_header) { + flags |= Gtk.DialogFlags.USE_HEADER_BAR; + } + Gtk.Dialog dialog = new Gtk.Dialog.with_buttons( + _("Details"), // same as the button + get_toplevel() as Gtk.Window, + flags + ); + dialog.set_default_size(600, -1); + dialog.get_content_area().add(this.problem_details); + + Gtk.HeaderBar? header_bar = dialog.get_header_bar() as Gtk.HeaderBar; + use_header = (header_bar != null); + if (use_header) { + header_bar.show_close_button = true; + } else { + dialog.add_button(_("_Close"), Gtk.ResponseType.CLOSE); + } + + Gtk.Widget copy = dialog.add_button( + _("Copy to Clipboard"), ResponseType.COPY + ); + copy.tooltip_text = + _("Copy technical details to clipboard for pasting into an email or bug report"); + + + dialog.set_default_response(ResponseType.COPY); + dialog.response.connect(on_details_response); + dialog.show(); + copy.grab_focus(); + } + + private void copy_details() { + get_clipboard(Gdk.SELECTION_CLIPBOARD).set_text(format_details(), -1); + } + + [GtkCallback] + private void on_info_bar_response(int response) { + switch(response) { + case ResponseType.DETAILS: + show_details(); + break; + + case ResponseType.RETRY: + retry(); + this.hide(); + break; + + default: + this.hide(); + break; + } + } + + [GtkCallback] + private void on_hide() { + this.parent.remove(this); + } + + private void on_details_response(Gtk.Dialog dialog, int response) { + switch(response) { + case ResponseType.COPY: + copy_details(); + break; + + default: + // fml + dialog.get_content_area().remove(this.problem_details); + dialog.hide(); + break; + } + } + + +} diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala index 13fedf7c..4161fa3a 100644 --- a/src/client/components/main-window.vala +++ b/src/client/components/main-window.vala @@ -50,6 +50,12 @@ public class MainWindow : Gtk.ApplicationWindow { [GtkChild] private Gtk.ScrolledWindow conversation_list_scrolled; + // This is a frame so users can use F6/Shift-F6 to get to it + [GtkChild] + private Gtk.Frame info_bar_frame; + + [GtkChild] + private Gtk.Grid info_bar_container; /** Fired when the shift key is pressed or released. */ public signal void on_shift_key(bool pressed); @@ -72,6 +78,11 @@ public class MainWindow : Gtk.ApplicationWindow { on_change_orientation(); } + public void show_infobar(MainWindowInfoBar info_bar) { + this.info_bar_container.add(info_bar); + this.info_bar_frame.show(); + } + private void load_config(Configuration config) { // This code both loads AND saves the pane positions with live updating. This is more // resilient against crashes because the value in dconf changes *immediately*, and @@ -422,4 +433,14 @@ public class MainWindow : Gtk.ApplicationWindow { } return Gdk.EVENT_STOP; } + + [GtkCallback] + private void on_info_bar_container_remove() { + // Ensure the info bar frame is hidden when the last info bar + // is removed from the container. + if (this.info_bar_container.get_children().length() == 0) { + this.info_bar_frame.hide(); + } + } + } diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index 05a74e74..4513df64 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -30,6 +30,7 @@ set(RESOURCE_LIST STRIPBLANKS "main-toolbar.ui" STRIPBLANKS "main-toolbar-menus.ui" STRIPBLANKS "main-window.ui" + STRIPBLANKS "main-window-info-bar.ui" STRIPBLANKS "password-dialog.glade" STRIPBLANKS "preferences-dialog.ui" STRIPBLANKS "remove_confirm.glade" diff --git a/ui/geary.css b/ui/geary.css index cfeb72d3..d92ca396 100644 --- a/ui/geary.css +++ b/ui/geary.css @@ -6,6 +6,8 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ +/* MainWindow */ + .geary-folder-frame > border { border-left-width: 0; border-top-width: 0; @@ -39,6 +41,14 @@ border-top-left-radius: 0px; } +/* MainWindowInfoBarSet */ + +.geary-info-bar-frame > border { + border-top-width: 0; + border-left-width: 0; + border-right-width: 0; +} + /* FolderPopover */ row.geary-folder-popover-list-row { diff --git a/ui/main-window-info-bar.ui b/ui/main-window-info-bar.ui new file mode 100644 index 00000000..465f4410 --- /dev/null +++ b/ui/main-window-info-bar.ui @@ -0,0 +1,142 @@ + + + + + + + True + False + 18 + 18 + 18 + 18 + 6 + 12 + + + True + True + start + baseline + 12 + True + If the problem is serious or persists, please copy and send these details to the <a href="https://wiki.gnome.org/Apps/Geary/Contact">mailing list</a> or lodge a <a href="https://wiki.gnome.org/Apps/Geary/ReportingABug">bug report</a>. + True + True + 0 + + + 0 + 0 + + + + + True + False + start + baseline + Details: + False + + + 0 + 1 + + + + + True + True + True + True + False + word + 6 + 6 + 6 + 6 + False + True + + + 0 + 2 + + + + diff --git a/ui/main-window.ui b/ui/main-window.ui index 1ed5894b..e09e308a 100644 --- a/ui/main-window.ui +++ b/ui/main-window.ui @@ -1,69 +1,60 @@ + - +