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.
This commit is contained in:
Michael James Gratton 2017-11-08 18:13:51 +11:00
parent 304fa9dcc2
commit 80680f280e
9 changed files with 560 additions and 64 deletions

View file

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

View file

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

View file

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

View file

@ -0,0 +1,245 @@
/*
* Copyright 2017 Michael Gratton <mike@vee.net>
*
* 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;
}
}
}

View file

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

View file

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

View file

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

142
ui/main-window-info-bar.ui Normal file
View file

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<template class="MainWindowInfoBar" parent="GtkInfoBar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<signal name="hide" handler="on_hide" after="yes" swapped="no"/>
<signal name="response" handler="on_info_bar_response" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="title">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label">Title</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="description">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label">Description.</property>
<property name="ellipsize">end</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<style>
<class name="sigh"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</template>
<object class="GtkGrid" id="problem_details">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">18</property>
<property name="margin_right">18</property>
<property name="margin_top">18</property>
<property name="margin_bottom">18</property>
<property name="row_spacing">6</property>
<property name="column_spacing">12</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="halign">start</property>
<property name="valign">baseline</property>
<property name="margin_bottom">12</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">If the problem is serious or persists, please copy and send these details to the &lt;a href="https://wiki.gnome.org/Apps/Geary/Contact"&gt;mailing list&lt;/a&gt; or lodge a &lt;a href="https://wiki.gnome.org/Apps/Geary/ReportingABug"&gt;bug report&lt;/a&gt;.</property>
<property name="use_markup">True</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">baseline</property>
<property name="label" translatable="yes">Details:</property>
<property name="track_visited_links">False</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkTextView" id="detail_text">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="editable">False</property>
<property name="wrap_mode">word</property>
<property name="left_margin">6</property>
<property name="right_margin">6</property>
<property name="top_margin">6</property>
<property name="bottom_margin">6</property>
<property name="cursor_visible">False</property>
<property name="monospace">True</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,69 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
<requires lib="gtk+" version="3.14"/>
<requires lib="gtk+" version="3.20"/>
<template class="MainWindow" parent="GtkApplicationWindow">
<property name="visible">False</property>
<property name="show_menubar">False</property>
<property name="name">GearyMainWindow</property>
<property name="can_focus">False</property>
<property name="events">GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_FOCUS_CHANGE_MASK | GDK_STRUCTURE_MASK</property>
<signal name="delete_event" handler="on_delete_event"/>
<signal name="key_release_event" handler="on_key_release_event"/>
<signal name="focus_in_event" handler="on_focus_event"/>
<property name="show_menubar">False</property>
<signal name="delete-event" handler="on_delete_event" swapped="no"/>
<signal name="focus-in-event" handler="on_focus_event" swapped="no"/>
<signal name="key-release-event" handler="on_key_release_event" swapped="no"/>
<child>
<object class="GtkBox" id="main_layout">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">0</property>
<child>
<object class="GtkPaned" id="conversations_paned">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">horizontal</property>
<child>
<object class="GtkBox" id="search_bar_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">0</property>
<style>
<class name="sidebar"/>
</style>
<child>
<object class="GtkPaned" id="folder_paned">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">horizontal</property>
<style>
<class name="geary-sidebar-pane-separator"/>
</style>
<child>
<object class="GtkBox" id="folder_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">0</property>
<child>
<object class="GtkFrame" id="folder_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkScrolledWindow" id="folder_list_scrolled">
<property name="width_request">100</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hscrollbar_policy">never</property>
</object>
</child>
<style>
<class name="geary-folder-frame"/>
</style>
<child>
<object class="GtkScrolledWindow" id="folder_list_scrolled">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="width_request">100</property>
<property name="hscrollbar_policy">never</property>
<property name="vscrollbar_policy">automatic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
@ -77,28 +68,27 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">0</property>
<child>
<object class="GtkFrame" id="conversation_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkScrolledWindow" id="conversation_list_scrolled">
<property name="width_request">250</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<style>
<class name="geary-conversation-frame"/>
</style>
<child>
<object class="GtkScrolledWindow" id="conversation_list_scrolled">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="width_request">250</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
@ -107,13 +97,20 @@
<property name="shrink">False</property>
</packing>
</child>
<style>
<class name="geary-sidebar-pane-separator"/>
</style>
</object>
<packing>
<property name="pack_type">end</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<style>
<class name="sidebar"/>
</style>
</object>
<packing>
<property name="resize">False</property>
@ -122,9 +119,64 @@
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="info_bar_frame">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkGrid" id="info_bar_container">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<signal name="remove" handler="on_info_bar_container_remove" swapped="no"/>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
<style>
<class name="geary-info-bar-frame"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>