Merge branch 'wip/geary-inspector' into 'mainline'

Add an Inspector to Geary

See merge request GNOME/geary!199
This commit is contained in:
Michael Gratton 2019-04-08 09:26:05 +00:00
commit 6234405e4f
17 changed files with 1026 additions and 116 deletions

View file

@ -16,30 +16,30 @@ variables:
INSTALL_CMD: ninja -v -C $BUILD_DIR install
# Fedora packages
FEDORA_DEPS: vala
meson desktop-file-utils libcanberra-devel
folks-devel libgee-devel glib2-devel gmime-devel
gtk3-devel libnotify-devel sqlite-devel
webkitgtk4-devel libsecret-devel libxml2-devel
vala-tools gcr-devel enchant2-devel libunwind-devel
iso-codes-devel gnome-online-accounts-devel itstool
json-glib-devel
FEDORA_DEPS: meson vala
desktop-file-utils enchant2-devel folks-devel gcr-devel
glib2-devel gmime-devel gnome-online-accounts-devel
gtk3-devel iso-codes-devel json-glib-devel itstool
libcanberra-devel libgee-devel libhandy-devel
libnotify-devel libsecret-devel libunwind-devel
libxml2-devel sqlite-devel webkitgtk4-devel
FEDORA_TEST_DEPS: Xvfb tar xz
# Ubuntu packages
UBUNTU_DEPS: valac build-essential
meson desktop-file-utils libcanberra-dev
libfolks-dev libgee-0.8-dev libglib2.0-dev
libgmime-2.6-dev libgtk-3-dev libsecret-1-dev
libxml2-dev libnotify-dev libsqlite3-dev
libwebkit2gtk-4.0-dev libgcr-3-dev libenchant-dev
libunwind-dev iso-codes libgoa-1.0-dev itstool gettext
libmessaging-menu-dev libunity-dev libjson-glib-dev
UBUNTU_DEPS: meson build-essential valac
desktop-file-utils gettext iso-codes itstool
libcanberra-dev libenchant-dev libfolks-dev
libgcr-3-dev libgee-0.8-dev libglib2.0-dev
libgmime-2.6-dev libgoa-1.0-dev libgtk-3-dev
libhandy-0.0-dev libjson-glib-dev libmessaging-menu-dev
libnotify-dev libsecret-1-dev libsqlite3-dev
libunity-dev libunwind-dev libwebkit2gtk-4.0-dev
libxml2-dev
UBUNTU_TEST_DEPS: xauth xvfb
fedora:
stage: build
image: fedora:latest
image: fedora:rawhide
before_script:
- dnf update -y --nogpgcheck
- dnf install -y --nogpgcheck $FEDORA_DEPS $FEDORA_TEST_DEPS
@ -51,7 +51,7 @@ fedora:
ubuntu:
stage: build
image: ubuntu:rolling
image: ubuntu:devel
before_script:
- apt-get update
- apt-get install -q -y --no-install-recommends $UBUNTU_DEPS $UBUNTU_TEST_DEPS

39
INSTALL
View file

@ -38,40 +38,31 @@ distribution's package repositories:
Installing dependencies on Fedora
---------------------------------
Fedora 25 and later ships with the correct versions of the required
libraries. Install them by running this command:
Install them by running this command:
sudo yum install vala meson desktop-file-utils iso-codes-devel \
libcanberra-devel folks-devel libgee-devel glib2-devel \
gmime-devel gtk3-devel libnotify-devel sqlite-devel \
webkitgtk4-devel libsecret-devel libxml2-devel vala-tools \
gcr-devel enchant2-devel libunwind-devel json-glib-devel \
gnome-online-accounts-devel itstool
sudo yum install meson vala \
desktop-file-utils enchant2-devel folks-devel gcr-devel \
glib2-devel gmime-devel gnome-online-accounts-devel gtk3-devel \
iso-codes-devel json-glib-devel libcanberra-devel \
libgee-devel libhandy-devel libnotify-devel libsecret-devel \
libunwind-devel libxml2-devel sqlite-devel webkitgtk4-devel
Installing dependencies on Ubuntu/Debian
----------------------------------------
Ubuntu 17.10 (Artful) and later ships with the correct versions of the
required libraries.
Ubuntu 16.04 LTS (Xenial) does not meet the minimum requirements,
users of that are encourage to use Geary 0.12 LTS instead.
Debian 9 (Stretch) and later ships with the correct versions of the
required libraries.
Install them by running this command:
sudo apt-get install valac meson desktop-file-utils iso-codes \
libcanberra-dev libfolks-dev libgee-0.8-dev libglib2.0-dev \
libgmime-2.6-dev libgtk-3-dev libsecret-1-dev libxml2-dev \
libnotify-dev libsqlite3-dev libwebkit2gtk-4.0-dev \
libgcr-3-dev libenchant-dev libunwind-dev libgoa-1.0-dev \
libjson-glib-dev itstool gettext
sudo apt-get install meson build-essential valac \
desktop-file-utils iso-codes gettext libcanberra-dev \
libenchant-dev libfolks-dev libgcr-3-dev libgee-0.8-dev \
libglib2.0-dev libgmime-2.6-dev libgoa-1.0-dev libgtk-3-dev \
libjson-glib-dev libhandy-dev libnotify-dev libsecret-1-dev \
libsqlite3-dev libunwind-dev libwebkit2gtk-4.0-dev \
libxml2-dev
And for Ubuntu Unity integration:
sudo apt-get install libunity-dev libmessaging-menu-dev
sudo apt-get install libmessaging-menu-dev libunity-dev
Running
-------

View file

@ -66,6 +66,7 @@ iso_codes = dependency('iso-codes')
javascriptcoregtk = dependency('javascriptcoregtk-4.0', version: '>=' + target_webkit)
json_glib = dependency('json-glib-1.0', version: '>= 1.0')
libcanberra = dependency('libcanberra', version: '>= 0.28')
libhandy = dependency('libhandy-0.0', version: '>= 0.0.9')
libmath = cc.find_library('m')
libnotify = dependency('libnotify', version: '>= 0.7.5')
libsecret = dependency('libsecret-1', version: '>= 0.11')

View file

@ -181,6 +181,20 @@
}
]
},
{
"name": "libhandy",
"buildsystem": "meson",
"builddir": true,
"config-opts": [
"-Dglade_catalog=disabled"
],
"sources": [
{
"type": "git",
"url": "https://source.puri.sm/Librem5/libhandy.git"
}
]
},
{
"name": "libunwind",
"sources": [

View file

@ -28,6 +28,7 @@ src/client/application/goa-mediator.vala
src/client/application/main.vala
src/client/application/secret-mediator.vala
src/client/components/client-web-view.vala
src/client/components/components-inspector.vala
src/client/components/components-placeholder-pane.vala
src/client/components/components-validator.vala
src/client/components/count-badge.vala
@ -415,6 +416,7 @@ ui/composer-headerbar.ui
ui/composer-link-popover.ui
ui/composer-menus.ui
ui/composer-widget.ui
ui/components-inspector.ui
ui/components-placeholder-pane.ui
ui/conversation-email.ui
ui/conversation-email-attachment-view.ui

View file

@ -27,6 +27,7 @@ internal class Accounts.EditorRow<PaneType> : Gtk.ListBoxRow {
public EditorRow() {
get_style_context().add_class("geary-settings");
get_style_context().add_class("geary-labelled-row");
this.layout.orientation = Gtk.Orientation.HORIZONTAL;
this.layout.show();

View file

@ -55,6 +55,7 @@ public class GearyApplication : Gtk.Application {
private const string ACTION_ABOUT = "about";
private const string ACTION_ACCOUNTS = "accounts";
private const string ACTION_COMPOSE = "compose";
private const string ACTION_INSPECT = "inspect";
private const string ACTION_HELP = "help";
private const string ACTION_MAILTO = "mailto";
private const string ACTION_PREFERENCES = "preferences";
@ -64,6 +65,7 @@ public class GearyApplication : Gtk.Application {
{ACTION_ABOUT, on_activate_about},
{ACTION_ACCOUNTS, on_activate_accounts},
{ACTION_COMPOSE, on_activate_compose},
{ACTION_INSPECT, on_activate_inspect},
{ACTION_HELP, on_activate_help},
{ACTION_MAILTO, on_activate_mailto, "s"},
{ACTION_PREFERENCES, on_activate_preferences},
@ -74,6 +76,14 @@ public class GearyApplication : Gtk.Application {
private const int64 FORCE_SHUTDOWN_USEC = 5 * USEC_PER_SEC;
/** Object returned by {@link get_runtime_information}. */
public struct RuntimeDetail {
public string name;
public string value;
}
[Version (deprecated = true)]
public static GearyApplication instance {
get { return _instance; }
@ -128,8 +138,93 @@ public class GearyApplication : Gtk.Application {
private bool exiting_fired = false;
private int exitcode = 0;
private bool is_destroyed = false;
private Components.Inspector? inspector = null;
/**
* Returns name/value pairs of application information.
*
* This includes Geary library version information, the current
* desktop, and so on.
*/
public Gee.Collection<RuntimeDetail?> get_runtime_information() {
Gee.LinkedList<RuntimeDetail?> info =
new Gee.LinkedList<RuntimeDetail?>();
/// Application runtime information label
info.add({ _("Geary version"), VERSION });
/// Application runtime information label
info.add({ _("GTK version"),
"%u.%u.%u".printf(
Gtk.get_major_version(),
Gtk.get_minor_version(),
Gtk.get_micro_version()
)});
/// Applciation runtime information label
info.add({ _("GLib version"),
"%u.%u.%u".printf(
GLib.Version.major,
GLib.Version.minor,
GLib.Version.micro
)});
/// Application runtime information label
info.add({ _("WebKitGTK version"),
"%u.%u.%u".printf(
WebKit.get_major_version(),
WebKit.get_minor_version(),
WebKit.get_micro_version()
)});
/// Application runtime information label
info.add({ _("Desktop environment"),
Environment.get_variable("XDG_CURRENT_DESKTOP") ??
_("Unknown")
});
// Distro name and version using LSB util
GLib.SubprocessLauncher launcher = new GLib.SubprocessLauncher(
GLib.SubprocessFlags.STDOUT_PIPE |
GLib.SubprocessFlags.STDERR_SILENCE
);
// Reset lang vars so we can guess the strings below
launcher.setenv("LANGUAGE", "C", true);
launcher.setenv("LANG", "C", true);
launcher.setenv("LC_ALL", "C", true);
string lsb_output = "";
try {
GLib.Subprocess lsb_release = launcher.spawnv(
{ "lsb_release", "-ir" }
);
lsb_release.communicate_utf8(null, null, out lsb_output, null);
} catch (GLib.Error err) {
warning("Failed to exec lsb_release: %s", err.message);
}
if (lsb_output != "") {
foreach (string line in lsb_output.split("\n")) {
string[] parts = line.split(":", 2);
if (parts.length > 1) {
if (parts[0].has_prefix("Distributor ID")) {
/// Application runtime information label
info.add(
{ _("Distribution name"), parts[1].strip() }
);
} else if (parts[0].has_prefix("Release")) {
/// Application runtime information label
info.add(
{ _("Distribution release"), parts[1].strip() }
);
}
}
}
}
/// Application runtime information label
info.add({ _("Installation prefix"), INSTALL_PREFIX });
return info;
}
/**
* Signal that is activated when 'exit' is called, but before the application actually exits.
*
@ -250,6 +345,7 @@ public class GearyApplication : Gtk.Application {
// Application accels
add_app_accelerators(ACTION_COMPOSE, { "<Ctrl>N" });
add_app_accelerators(ACTION_HELP, { "F1" });
add_app_accelerators(ACTION_INSPECT, { "<Alt><Shift>I" });
add_app_accelerators(ACTION_QUIT, { "<Ctrl>Q" });
// Common window accels
@ -260,6 +356,7 @@ public class GearyApplication : Gtk.Application {
add_window_accelerators(ACTION_UNDO, { "<Ctrl>Z" });
ComposerWidget.add_window_accelerators(this);
Components.Inspector.add_window_accelerators(this);
yield controller.open_async(null);
@ -469,6 +566,18 @@ public class GearyApplication : Gtk.Application {
}
}
private void on_activate_inspect() {
if (this.inspector == null) {
this.inspector = new Components.Inspector(this);
this.inspector.destroy.connect(() => {
this.inspector = null;
});
this.inspector.show();
} else {
this.inspector.present();
}
}
private void on_activate_mailto(SimpleAction action, Variant? param) {
if (this.controller != null && param != null) {
this.controller.compose_mailto(param.get_string());

View file

@ -0,0 +1,438 @@
/*
* Copyright 2019 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.
*/
/**
* A window that displays debugging and development information.
*/
[GtkTemplate (ui = "/org/gnome/Geary/components-inspector.ui")]
public class Components.Inspector : Gtk.ApplicationWindow {
private const int COL_MESSAGE = 0;
private const string ACTION_CLOSE = "inspector-close";
private const string ACTION_PLAY_TOGGLE = "toggle-play";
private const string ACTION_SEARCH_TOGGLE = "toggle-search";
private const string ACTION_SEARCH_ACTIVATE = "activate-search";
private const ActionEntry[] action_entries = {
{GearyApplication.ACTION_CLOSE, on_close },
{GearyApplication.ACTION_COPY, on_copy_clicked },
{ACTION_CLOSE, on_close },
{ACTION_PLAY_TOGGLE, on_logs_play_toggled, null, "true" },
{ACTION_SEARCH_TOGGLE, on_logs_search_toggled, null, "false" },
{ACTION_SEARCH_ACTIVATE, on_logs_search_activated },
};
public static void add_window_accelerators(GearyApplication app) {
app.add_window_accelerators(ACTION_CLOSE, { "Escape" } );
app.add_window_accelerators(ACTION_PLAY_TOGGLE, { "space" } );
app.add_window_accelerators(ACTION_SEARCH_ACTIVATE, { "<Ctrl>F" } );
}
[GtkChild]
private Gtk.HeaderBar header_bar;
[GtkChild]
private Gtk.Stack stack;
[GtkChild]
private Gtk.Button copy_button;
[GtkChild]
private Gtk.Widget logs_pane;
[GtkChild]
private Gtk.ToggleButton play_button;
[GtkChild]
private Gtk.ToggleButton search_button;
[GtkChild]
private Hdy.SearchBar search_bar;
[GtkChild]
private Gtk.SearchEntry search_entry;
[GtkChild]
private Gtk.ScrolledWindow logs_scroller;
[GtkChild]
private Gtk.TreeView logs_view;
[GtkChild]
private Gtk.CellRendererText log_renderer;
[GtkChild]
private Gtk.Widget detail_pane;
[GtkChild]
private Gtk.ListBox detail_list;
private Gtk.ListStore logs_store = new Gtk.ListStore.newv({
typeof(string)
});
private Gtk.TreeModelFilter logs_filter;
private string[] logs_filter_terms = new string[0];
private string details;
private bool update_logs = true;
private Geary.Logging.Record? first_pending = null;
private bool autoscroll = true;
public Inspector(GearyApplication app) {
Object(application: app);
this.title = this.header_bar.title = _("Inspector");
add_action_entries(Inspector.action_entries, this);
this.search_bar.connect_entry(this.search_entry);
GLib.Settings system = app.config.gnome_interface;
system.bind(
"monospace-font-name",
this.log_renderer, "font",
SettingsBindFlags.DEFAULT
);
StringBuilder details = new StringBuilder();
foreach (GearyApplication.RuntimeDetail? detail
in app.get_runtime_information()) {
this.detail_list.add(
new DetailRow("%s:".printf(detail.name), detail.value)
);
details.append_printf("%s: %s\n", detail.name, detail.value);
}
this.details = details.str;
// Enable updates to get the log marker
enable_log_updates(true);
// Install the listener then starting add the backlog
// (ba-doom-tish) so to avoid the race.
Geary.Logging.set_log_listener(this.on_log_record);
Gtk.ListStore logs_store = this.logs_store;
Geary.Logging.Record? logs = Geary.Logging.get_logs();
int index = 0;
while (logs != null) {
if (should_append(logs)) {
string message = logs.format();
Gtk.TreeIter iter;
logs_store.insert(out iter, index++);
logs_store.set_value(iter, COL_MESSAGE, message);
}
logs = logs.next;
}
this.logs_filter = new Gtk.TreeModelFilter(logs_store, null);
this.logs_filter.set_visible_func((model, iter) => {
bool ret = true;
if (this.logs_filter_terms.length > 0) {
ret = true;
Value value;
model.get_value(iter, COL_MESSAGE, out value);
string? message = (string) value;
if (message != null) {
message = message.casefold();
foreach (string term in this.logs_filter_terms) {
if (!message.contains(term)) {
ret = false;
break;
}
}
}
}
return ret;
});
this.logs_view.set_model(this.logs_filter);
}
public override void destroy() {
Geary.Logging.set_log_listener(null);
base.destroy();
}
public override bool key_press_event(Gdk.EventKey event) {
bool ret = Gdk.EVENT_PROPAGATE;
if (this.search_bar.search_mode_enabled &&
event.keyval == Gdk.Key.Escape) {
// Manually deactivate search so the button stays in sync
this.search_button.set_active(false);
ret = Gdk.EVENT_STOP;
}
if (ret == Gdk.EVENT_PROPAGATE) {
ret = this.search_bar.handle_event(event);
}
if (ret == Gdk.EVENT_PROPAGATE &&
this.search_bar.search_mode_enabled) {
// Ensure <Space> and others are passed to the search
// entry before getting used as an accelerator.
ret = this.search_entry.key_press_event(event);
}
if (ret == Gdk.EVENT_PROPAGATE) {
ret = base.key_press_event(event);
}
return ret;
}
private void enable_log_updates(bool enabled) {
// Log a marker to indicate when it was started/stopped
debug(
"---- 8< ---- %s %s ---- 8< ----",
this.header_bar.title,
enabled ? "" : ""
);
this.update_logs = enabled;
// Disable autoscroll when not updating as well to stop the
// tree view jumping to the bottom when changing the filter.
this.autoscroll = enabled;
if (enabled) {
Geary.Logging.Record? logs = this.first_pending;
while (logs != null) {
append_record(logs);
logs = logs.next;
}
this.first_pending = null;
}
}
private inline bool should_append(Geary.Logging.Record record) {
// Blacklist GdkPixbuf since it spams us e.g. when window
// focus changes, including between MainWindow and the
// Inspector, which is very annoying.
return (record.domain != "GdkPixbuf");
}
private async void save(string path,
GLib.Cancellable? cancellable)
throws GLib.Error {
GLib.File dest = GLib.File.new_for_path(path);
GLib.FileIOStream dest_io = yield dest.create_readwrite_async(
GLib.FileCreateFlags.NONE,
GLib.Priority.DEFAULT,
cancellable
);
GLib.DataOutputStream out = new GLib.DataOutputStream(
new GLib.BufferedOutputStream(dest_io.get_output_stream())
);
out.put_string(this.details);
out.put_byte('\n');
out.put_byte('\n');
Gtk.TreeModel model = this.logs_view.model;
Gtk.TreeIter? iter;
bool valid = model.get_iter_first(out iter);
while (valid && !cancellable.is_cancelled()) {
Value value;
model.get_value(iter, COL_MESSAGE, out value);
string? message = (string) value;
if (message != null) {
out.put_string(message);
out.put_byte('\n');
}
valid = model.iter_next(ref iter);
}
yield out.close_async();
yield dest_io.close_async();
}
private void update_ui() {
bool logs_visible = this.stack.visible_child == this.logs_pane;
uint logs_selected = this.logs_view.get_selection().count_selected_rows();
this.copy_button.set_sensitive(!logs_visible || logs_selected > 0);
this.play_button.set_visible(logs_visible);
this.search_button.set_visible(logs_visible);
}
private void update_scrollbar() {
Gtk.Adjustment adj = this.logs_scroller.get_vadjustment();
adj.set_value(adj.upper - adj.page_size);
}
private void update_logs_filter() {
string cleaned =
Geary.String.reduce_whitespace(this.search_entry.text).casefold();
this.logs_filter_terms = cleaned.split(" ");
this.logs_filter.refilter();
}
private void append_record(Geary.Logging.Record record) {
if (should_append(record)) {
Gtk.TreeIter inserted_iter;
this.logs_store.append(out inserted_iter);
this.logs_store.set_value(inserted_iter, COL_MESSAGE, record.format());
}
}
[GtkCallback]
private void on_visible_child_changed() {
update_ui();
}
private void on_copy_clicked() {
string clipboard_value = "";
if (this.stack.visible_child == this.logs_pane) {
StringBuilder rows = new StringBuilder();
Gtk.TreeModel model = this.logs_view.model;
foreach (Gtk.TreePath path in
this.logs_view.get_selection().get_selected_rows(null)) {
Gtk.TreeIter iter;
if (model.get_iter(out iter, path)) {
Value value;
model.get_value(iter, COL_MESSAGE, out value);
string? message = (string) value;
if (message != null) {
rows.append(message);
rows.append_c('\n');
}
}
}
clipboard_value = rows.str;
} else if (this.stack.visible_child == this.detail_pane) {
clipboard_value = this.details;
}
if (!Geary.String.is_empty(clipboard_value)) {
get_clipboard(Gdk.SELECTION_CLIPBOARD).set_text(clipboard_value, -1);
}
}
[GtkCallback]
private void on_save_as_clicked() {
Gtk.FileChooserNative chooser = new Gtk.FileChooserNative(
_("Save As"),
this,
Gtk.FileChooserAction.SAVE,
_("Save As"),
_("Cancel")
);
chooser.set_current_name(
new GLib.DateTime.now_local().format("Geary Inspector - %F %T.txt")
);
if (chooser.run() == Gtk.ResponseType.ACCEPT) {
this.save.begin(
chooser.get_filename(),
null,
(obj, res) => {
try {
this.save.end(res);
} catch (GLib.Error err) {
warning("Failed to save inspector data: %s", err.message);
}
}
);
}
}
[GtkCallback]
private void on_logs_size_allocate() {
if (this.autoscroll) {
update_scrollbar();
}
}
[GtkCallback]
private void on_logs_selection_changed() {
update_ui();
}
private void on_logs_search_toggled(GLib.SimpleAction action,
GLib.Variant? param) {
bool enabled = !((bool) action.state);
this.search_bar.set_search_mode(enabled);
action.set_state(enabled);
}
private void on_logs_search_activated() {
this.search_button.set_active(true);
this.search_entry.grab_focus();
}
private void on_logs_play_toggled(GLib.SimpleAction action,
GLib.Variant? param) {
bool enabled = !((bool) action.state);
enable_log_updates(enabled);
action.set_state(enabled);
}
[GtkCallback]
private void on_logs_search_changed() {
update_logs_filter();
}
private void on_log_record(Geary.Logging.Record record) {
if (this.update_logs) {
GLib.MainContext.default().invoke(() => {
append_record(record);
return GLib.Source.REMOVE;
});
} else if (this.first_pending == null) {
this.first_pending = record;
}
}
private void on_close() {
destroy();
}
}
private class Components.DetailRow : Gtk.ListBoxRow {
private Gtk.Grid layout { get; private set; default = new Gtk.Grid(); }
private Gtk.Label label { get; private set; default = new Gtk.Label(""); }
private Gtk.Label value { get; private set; default = new Gtk.Label(""); }
public DetailRow(string label, string value) {
get_style_context().add_class("geary-labelled-row");
this.label.halign = Gtk.Align.START;
this.label.valign = Gtk.Align.CENTER;
this.label.set_text(label);
this.label.show();
this.value.halign = Gtk.Align.END;
this.value.hexpand = true;
this.value.valign = Gtk.Align.CENTER;
this.value.xalign = 1.0f;
this.value.set_text(value);
this.value.show();
this.layout.orientation = Gtk.Orientation.HORIZONTAL;
this.layout.add(this.label);
this.layout.add(this.value);
this.layout.show();
add(this.layout);
this.activatable = false;
show();
}
}

View file

@ -135,8 +135,6 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
{ACTION_SHOW_EXTENDED, on_toggle_action, null, "false", on_show_extended_toggled },
};
public static Gee.MultiMap<string, string> action_accelerators = new Gee.HashMultiMap<string, string>();
public static void add_window_accelerators(GearyApplication application) {
application.add_window_accelerators(ACTION_CLOSE, { "Escape" } );
application.add_window_accelerators(ACTION_CUT, { "<Ctrl>x" } );

View file

@ -24,7 +24,10 @@ public class Dialogs.ProblemDetailsDialog : Gtk.Dialog {
Geary.ErrorContext error,
Geary.AccountInformation? account,
Geary.ServiceInformation? service) {
Object(use_header_bar: 1);
Object(
transient_for: parent,
use_header_bar: 1
);
set_default_size(600, -1);
this.error = error;
@ -50,18 +53,18 @@ public class Dialogs.ProblemDetailsDialog : Gtk.Dialog {
private string format_details() {
StringBuilder details = new StringBuilder();
details.append_printf(
"Geary version: %s\n",
GearyApplication.VERSION
);
details.append_printf(
"GTK version: %u.%u.%u\n",
Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version()
);
details.append_printf(
"Desktop: %s\n",
Environment.get_variable("XDG_CURRENT_DESKTOP") ?? "Unknown"
);
Gtk.ApplicationWindow? parent =
this.get_toplevel() as Gtk.ApplicationWindow;
GearyApplication? app = (parent != null)
? parent.application as GearyApplication
: null;
if (app != null) {
foreach (GearyApplication.RuntimeDetail? detail
in app.get_runtime_information()) {
details.append_printf("%s: %s", detail.name, detail.value);
}
}
if (this.account != null) {
details.append_printf(
"Account id: %s\n",

View file

@ -24,6 +24,7 @@ geary_client_vala_sources = files(
'accounts/accounts-manager.vala',
'components/client-web-view.vala',
'components/components-inspector.vala',
'components/components-placeholder-pane.vala',
'components/components-validator.vala',
'components/count-badge.vala',
@ -124,6 +125,7 @@ geary_client_dependencies = [
gtk,
json_glib,
libcanberra,
libhandy,
libnotify,
libsecret,
libsoup,

View file

@ -13,8 +13,19 @@
namespace Geary.Logging {
/** Specifies the default number of log records retained. */
public const uint DEFAULT_MAX_LOG_BUFFER_LENGTH = 4096;
private const string DOMAIN = "Geary";
/**
* Denotes a type of log message.
*
* Logging for each type of log message may be dynamically enabled or
* disabled at run time by {@link enable_flags} and {@link
* disable_flags}.
*/
[Flags]
public enum Flag {
NONE = 0,
@ -36,21 +47,84 @@ public enum Flag {
}
}
/**
* A record of a single message sent to the logging system.
*
* A record is created for each message logged, and stored in a
* limited-length, singly-linked buffer. Applications can retrieve
* this by calling {@link get_logs} and then {get_next}, and can be
* notified of new records via {@link set_log_listener}.
*/
public class Record {
/** Returns the GLib domain of the log message. */
public string domain { get; private set; }
/** Returns the next log record in the buffer, if any. */
public Record? next { get; internal set; default = null; }
private LogLevelFlags flags;
private int64 timestamp;
private double elapsed;
private string message;
internal Record(string domain,
LogLevelFlags flags,
int64 timestamp,
double elapsed,
string message) {
this.domain = domain;
this.flags = flags;
this.timestamp = timestamp;
this.elapsed = elapsed;
this.message = message;
}
/** Returns a formatted string representation of this record. */
public string format() {
GLib.DateTime time = new GLib.DateTime.from_unix_utc(
this.timestamp / 1000 / 1000
).to_local();
return "%s %02d:%02d:%02d %lf %s: %s".printf(
to_prefix(this.flags),
time.get_hour(), time.get_minute(), time.get_second(),
this.elapsed,
this.domain ?? "default",
this.message
);
}
}
/** Specifies the function signature for {@link set_log_listener}. */
public delegate void LogRecord(Record record);
private int init_count = 0;
private Flag logging_flags = Flag.NONE;
private unowned FileStream? stream = null;
private Timer? entry_timer = null;
private Record? first_record = null;
private Record? last_record = null;
private uint log_length = 0;
private uint max_log_length = 0;
private LogRecord? listener = null;
/**
* Must be called before ''any'' call to the Logging namespace.
*
* This will be initialized by the Engine when it's opened, but applications may want to set up
* logging before that, in which case, call this directly.
* This will be initialized by the Engine when it's opened, but
* applications may want to set up logging before that, in which case,
* call this directly.
*/
public void init() {
if (init_count++ != 0)
return;
entry_timer = new Timer();
max_log_length = DEFAULT_MAX_LOG_BUFFER_LENGTH;
}
/**
@ -75,6 +149,12 @@ public void disable_flags(Flag flags) {
logging_flags &= ~flags;
}
/** Sets a function to be called when a new log record is created. */
public void set_log_listener(LogRecord? new_listener) {
listener = new_listener;
}
/**
* Returns the current logging flags.
*/
@ -120,6 +200,11 @@ public inline void debug(Flag flags, string fmt, ...) {
}
}
/** Returns the oldest log record in the logging system's buffer. */
public Record? get_logs() {
return first_record;
}
/**
* Registers a FileStream to receive all log output from the Engine, be it via the specialized
* Logging calls (which use the topic-based {@link Flag} or GLib's standard issue
@ -135,6 +220,37 @@ public void log_to(FileStream? stream) {
public void default_handler(string? domain,
LogLevelFlags log_levels,
string message) {
Record record = new Record(
domain,
log_levels,
GLib.get_real_time(),
entry_timer.elapsed(),
message
);
entry_timer.start();
// Update the record linked list
if (first_record == null) {
first_record = record;
last_record = record;
} else {
last_record.next = record;
last_record = record;
}
log_length++;
while (log_length > max_log_length) {
first_record = first_record.next;
log_length--;
}
if (first_record == null) {
last_record = null;
}
if (listener != null) {
listener(record);
}
// Print to the output stream if needed
unowned FileStream? out = stream;
if (out != null ||
((LogLevelFlags.LEVEL_WARNING & log_levels) > 0) ||
@ -145,17 +261,8 @@ public void default_handler(string? domain,
out = GLib.stderr;
}
GLib.Time tm = GLib.Time.local(time_t());
out.printf(
"%s %02d:%02d:%02d %lf %s: %s\n",
to_prefix(log_levels),
tm.hour, tm.minute, tm.second,
entry_timer.elapsed(),
domain ?? "default",
message
);
entry_timer.start();
out.puts(record.format());
out.putc('\n');
}
}

View file

@ -95,9 +95,9 @@ private class Geary.ImapDB.Attachment : Geary.Attachment {
save_file(part, attachments_dir, cancellable);
update_db(cx, cancellable);
} catch (Error err) {
// Don't honour the cancellable here, we need to delete
// it.
this.delete(cx, cancellable);
// Don't honour the cancellable here, it needs to be
// deleted
this.delete(cx, null);
throw err;
}
}
@ -161,10 +161,8 @@ private class Geary.ImapDB.Attachment : Geary.Attachment {
// create directory, but don't throw exception if already exists
try {
target.get_parent().make_directory_with_parents(cancellable);
} catch (IOError ioe) {
// fall through if already exists
if (!(ioe is IOError.EXISTS))
throw ioe;
} catch (IOError.EXISTS err) {
// All good
}
// Delete any existing file now since we might not be creating

View file

@ -37,9 +37,10 @@ geary_c_options = [
# Select libunwind's optimised, local-only backtrace unwiding. See
# libunwind(3).
'-DUNW_LOCAL_ONLY',
# Neither GOA nor GCK want to hang out unless you are cool enough
'-DGOA_API_IS_SUBJECT_TO_CHANGE',
# None of these kids want to hang out unless you are cool enough
'-DGCK_API_SUBJECT_TO_CHANGE',
'-DGOA_API_IS_SUBJECT_TO_CHANGE',
'-DHANDY_USE_UNSTABLE_API',
]
subdir('sqlite3-unicodesn')

242
ui/components-inspector.ui Normal file
View file

@ -0,0 +1,242 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<requires lib="libhandy" version="0.0"/>
<object class="GtkListStore" id="logs_store">
<columns>
<!-- column-name log -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">Inspector opened</col>
</row>
</data>
</object>
<template class="ComponentsInspector" parent="GtkApplicationWindow">
<property name="can_focus">False</property>
<property name="default_width">750</property>
<property name="default_height">500</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkToggleButton" id="play_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes" comments="Tooltip for inspector button">Togggle appending new log entries</property>
<property name="action_name">win.toggle-play</property>
<property name="active">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">media-playback-start-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkToggleButton" id="search_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes" comments="Tooltip for inspector button">Search fo matching log entries</property>
<property name="action_name">win.toggle-search</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-find-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child type="title">
<object class="GtkStackSwitcher">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stack">stack</property>
</object>
</child>
<child>
<object class="GtkButton" id="save_as_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes" comments="Tooltip for inspector button">Save logs entries and details</property>
<signal name="clicked" handler="on_save_as_clicked" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-save-as-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="copy_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes" comments="Tooltip for inspector button">Copy selected log entries</property>
<property name="action_name">win.copy</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-copy-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="notify::visible-child" handler="on_visible_child_changed" swapped="no"/>
<child>
<object class="GtkGrid" id="logs_pane">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="HdySearchBar" id="search_bar">
<property name="name">search_bar</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">edit-find-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
<signal name="search-changed" handler="on_logs_search_changed" swapped="no"/>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="logs_scroller">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="logs_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">logs_store</property>
<property name="headers_visible">False</property>
<property name="enable_search">False</property>
<property name="show_expanders">False</property>
<signal name="size-allocate" handler="on_logs_size_allocate" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection">
<property name="mode">multiple</property>
<signal name="changed" handler="on_logs_selection_changed" swapped="no"/>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="log_column">
<property name="title" translatable="yes">column</property>
<child>
<object class="GtkCellRendererText" id="log_renderer"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">logs_pane</property>
<property name="title" translatable="yes" comments="Inspector stack title">Logs</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="detail_pane">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<property name="max_content_width">600</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="HdyColumn">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">16</property>
<property name="margin_right">16</property>
<property name="margin_top">32</property>
<property name="margin_bottom">32</property>
<property name="maximum_width">500</property>
<property name="linear_growth_width">1</property>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<child>
<object class="GtkListBox" id="detail_list">
<property name="name">detail_list</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">none</property>
<property name="activate_on_single_click">False</property>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">general_pane</property>
<property name="title" translatable="yes" comments="Inspector stack title">General</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</template>
</interface>

View file

@ -207,6 +207,45 @@ grid.geary-message-summary {
font-weight: bold;
}
/* GtkListboxRows with padded labels */
row.geary-labelled-row {
padding: 0px;
}
row.geary-labelled-row > grid > * {
margin: 18px 6px;
}
row.geary-labelled-row > grid > *:first-child:dir(ltr),
row.geary-labelled-row > grid > *:last-child:dir(rtl) {
margin-left: 18px;
}
row.geary-labelled-row > grid > *:last-child:dir(ltr),
row.geary-labelled-row > grid > *:first-child:dir(rtl) {
margin-right: 18px;
}
/* Images should have some padding to offset them from adjacent
widgets, but care ust be taken since images are also used as children
of other widgets like entries, comboboxes and switches, and these
shouldn't be be touched. */
row.geary-labelled-row widget > image,
row.geary-labelled-row grid > image {
padding: 0px 6px;
}
row.geary-labelled-row > grid > combobox,
row.geary-labelled-row > grid > entry,
row.geary-labelled-row:not(.geary-add-row) > grid > image,
row.geary-labelled-row > grid > switch {
/* These use more space than labels, so set their valign to center
when adding them and free up some space around them here to keep a
consistent row height. */
margin: 0 12px;
}
/* Accounts.Editor */
grid.geary-accounts-editor-pane-content {
@ -230,10 +269,6 @@ label.geary-settings-heading {
margin-bottom: 12px;
}
row.geary-settings {
padding: 0px;
}
row.geary-settings entry {
border-width: 0px;
background-color: transparent;
@ -249,20 +284,6 @@ row.geary-settings.geary-drag-icon {
border: 1px solid @borders;
}
row.geary-settings > grid > * {
margin: 18px 6px;
}
row.geary-settings > grid > *:first-child:dir(ltr),
row.geary-settings > grid > *:last-child:dir(rtl) {
margin-left: 18px;
}
row.geary-settings > grid > *:last-child:dir(ltr),
row.geary-settings > grid > *:first-child:dir(rtl) {
margin-right: 18px;
}
/* dir pseudo-class used here for required additional specificity */
row.geary-settings > grid > grid.geary-drag-handle:dir(ltr),
row.geary-settings > grid > grid.geary-drag-handle:dir(rtl) {
@ -283,25 +304,6 @@ frame.geary-settings.geary-signature {
min-height: 5em;
}
/* Images should have some padding to offset them from adjacent
widgets, but care ust be taken since images are also used as children
of other widgets like entries, comboboxes and switches, and these
shouldn't be be touched. */
row.geary-settings widget > image,
row.geary-settings grid > image {
padding: 0px 6px;
}
row.geary-settings > grid > combobox,
row.geary-settings > grid > entry,
row.geary-settings:not(.geary-add-row) > grid > image,
row.geary-settings > grid > switch {
/* These use more space than labels, so set their valign to center
when adding them and free up some space around them here to keep a
consistent row height. */
margin: 0 12px;
}
buttonbox.geary-settings {
margin-top: 36px;
}

View file

@ -10,6 +10,7 @@
<file compressed="true" preprocess="xml-stripblanks">certificate_warning_dialog.glade</file>
<file compressed="true">client-web-view.js</file>
<file compressed="true">client-web-view-allow-remote-images.js</file>
<file compressed="true" preprocess="xml-stripblanks">components-inspector.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components-placeholder-pane.ui</file>
<file compressed="true" preprocess="xml-stripblanks">composer-headerbar.ui</file>
<file compressed="true" preprocess="xml-stripblanks">composer-link-popover.ui</file>