diff --git a/src/client/components/components-inspector-log-view.vala b/src/client/components/components-inspector-log-view.vala new file mode 100644 index 00000000..19ffbbe7 --- /dev/null +++ b/src/client/components/components-inspector-log-view.vala @@ -0,0 +1,257 @@ +/* + * Copyright 2019 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. + */ + +/** + * A view that displays the contents of the Engine's log. + */ +[GtkTemplate (ui = "/org/gnome/Geary/components-inspector-log-view.ui")] +public class Components.InspectorLogView : Gtk.Grid { + + + private const int COL_MESSAGE = 0; + + + /** Determines if the log record search user interface is shown. */ + public bool search_mode_enabled { + get { return this.search_bar.search_mode_enabled; } + set { this.search_bar.search_mode_enabled = value; } + } + + [GtkChild] + private Hdy.SearchBar search_bar { get; private set; } + + [GtkChild] + private Gtk.SearchEntry search_entry { get; private set; } + + [GtkChild] + private Gtk.ScrolledWindow logs_scroller; + + [GtkChild] + private Gtk.TreeView logs_view; + + [GtkChild] + private Gtk.CellRendererText log_renderer; + + 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 bool update_logs = true; + private Geary.Logging.Record? first_pending = null; + + private bool autoscroll = true; + + private Geary.AccountInformation? account_filter = null; + + + /** Emitted when the number of selected records changes. */ + public signal void record_selection_changed(); + + + public InspectorLogView(Configuration config, + Geary.AccountInformation? filter_by = null) { + GLib.Settings system = config.gnome_interface; + system.bind( + "monospace-font-name", + this.log_renderer, "font", + SettingsBindFlags.DEFAULT + ); + + this.search_bar.connect_entry(this.search_entry); + } + + /** Loads log records from the logging system into the view. */ + public void load() { + // Install the listener then start adding 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); + } + + /** {@inheritDoc} */ + public override void destroy() { + Geary.Logging.set_log_listener(null); + base.destroy(); + } + + /** Forwards a key press event to the search entry. */ + public bool handle_key_press(Gdk.EventKey event) { + return this.search_entry.key_press_event(event); + } + + /** Returns the number of currently selected log records. */ + public int count_selected_records() { + return this.logs_view.get_selection().count_selected_rows(); + } + + /** Enables and disables updating log records as new ones arrive. */ + public void enable_log_updates(bool 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; + } + } + + /** Saves all log records to the given output stream. */ + public void save(GLib.DataOutputStream out, + bool save_all, + GLib.Cancellable? cancellable) + throws GLib.Error { + Gtk.TreeModel model = this.logs_view.model; + if (save_all) { + // Save all rows selected + Gtk.TreeIter? iter; + bool valid = model.get_iter_first(out iter); + while (valid && !cancellable.is_cancelled()) { + save_record(model, iter, @out, cancellable); + valid = model.iter_next(ref iter); + } + } else { + // Save only selected + GLib.Error? inner_err = null; + this.logs_view.get_selection().selected_foreach( + (model, path, iter) => { + if (inner_err == null) { + try { + save_record(model, iter, @out, cancellable); + } catch (GLib.Error err) { + inner_err = err; + } + } + } + ); + if (inner_err != null) { + throw inner_err; + } + } + } + + private inline void save_record(Gtk.TreeModel model, + Gtk.TreeIter iter, + GLib.DataOutputStream @out, + GLib.Cancellable? cancellable) + throws GLib.Error { + GLib.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'); + } + } + + 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. + record.fill_well_known_loggables(); + return ( + record.domain != "GdkPixbuf" && + (record.account == null || + this.account_filter == null || + record.account.information == this.account_filter) + ); + } + + 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_logs_size_allocate() { + if (this.autoscroll) { + update_scrollbar(); + } + } + + [GtkCallback] + private void on_logs_search_changed() { + update_logs_filter(); + } + + [GtkCallback] + private void on_logs_selection_changed() { + record_selection_changed(); + } + + 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; + } + } + +} diff --git a/src/client/components/components-inspector-system-view.vala b/src/client/components/components-inspector-system-view.vala new file mode 100644 index 00000000..b4122e46 --- /dev/null +++ b/src/client/components/components-inspector-system-view.vala @@ -0,0 +1,83 @@ +/* + * Copyright 2019 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. + */ + +/** + * A view that displays system and library information. + */ +[GtkTemplate (ui = "/org/gnome/Geary/components-inspector-system-view.ui")] +public class Components.InspectorSystemView : Gtk.Grid { + + + + private class 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(); + } + + } + + + [GtkChild] + private Gtk.ListBox system_list; + + private string details; + + + public InspectorSystemView(GearyApplication application) { + StringBuilder details = new StringBuilder(); + foreach (GearyApplication.RuntimeDetail? detail + in application.get_runtime_information()) { + this.system_list.add( + new DetailRow("%s:".printf(detail.name), detail.value) + ); + details.append_printf("%s: %s\n", detail.name, detail.value); + } + this.details = details.str; + } + + public void save(GLib.DataOutputStream out, GLib.Cancellable? cancellable) + throws GLib.Error { + out.put_string(this.details, cancellable); + } + +} diff --git a/src/client/components/components-inspector.vala b/src/client/components/components-inspector.vala index 152bbdfc..b8102295 100644 --- a/src/client/components/components-inspector.vala +++ b/src/client/components/components-inspector.vala @@ -1,4 +1,3 @@ - /* * Copyright 2019 Michael Gratton * @@ -13,8 +12,6 @@ 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"; @@ -45,150 +42,69 @@ public class Components.Inspector : Gtk.ApplicationWindow { [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; + private InspectorLogView log_pane; + private InspectorSystemView system_pane; - public Inspector(GearyApplication app) { - Object(application: app); + public Inspector(GearyApplication application) { + Object(application: application); 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 + this.log_pane = new InspectorLogView(application.config, null); + this.log_pane.record_selection_changed.connect( + on_logs_selection_changed ); + /// Translators: Title for Inspector logs pane + this.stack.add_titled(this.log_pane, "log_pane", _("Logs")); - 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; + this.system_pane = new InspectorSystemView(application); + /// Translators: Title for Inspector system system information pane + this.stack.add_titled(this.system_pane, "system_pane", _("System")); - // Enable updates to get the log marker + // Enable updates to get the log marker, then load log records in 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(); + this.log_pane.load(); } public override bool key_press_event(Gdk.EventKey event) { bool ret = Gdk.EVENT_PROPAGATE; - if (this.search_bar.search_mode_enabled && + if (this.log_pane.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) { + this.log_pane.search_mode_enabled) { // Ensure and others are passed to the search // entry before getting used as an accelerator. - ret = this.search_entry.key_press_event(event); + ret = this.log_pane.handle_key_press(event); } if (ret == Gdk.EVENT_PROPAGATE) { ret = base.key_press_event(event); } + + if (ret == Gdk.EVENT_PROPAGATE && + !this.log_pane.search_mode_enabled) { + // Nothing has handled the event yet, and search is not + // active, so see if we want to activate it now. + ret = this.log_pane.handle_key_press(event); + if (ret == Gdk.EVENT_STOP) { + this.search_button.set_active(true); + } + } + return ret; } @@ -196,31 +112,11 @@ public class Components.Inspector : Gtk.ApplicationWindow { // Log a marker to indicate when it was started/stopped debug( "---- 8< ---- %s %s ---- 8< ----", - this.header_bar.title, + this.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"); + this.log_pane.enable_log_updates(enabled); } private async void save(string path, @@ -236,85 +132,48 @@ public class Components.Inspector : Gtk.ApplicationWindow { new GLib.BufferedOutputStream(dest_io.get_output_stream()) ); - out.put_string(this.details); + this.system_pane.save(@out, cancellable); 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); - } + this.log_pane.save(@out, true, cancellable); 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(); + bool logs_visible = this.stack.visible_child == this.log_pane; + uint logs_selected = this.log_pane.count_selected_records(); 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'); - } - } + GLib.MemoryOutputStream bytes = new GLib.MemoryOutputStream.resizable(); + GLib.DataOutputStream out = new GLib.DataOutputStream(bytes); + try { + if (this.stack.visible_child == this.log_pane) { + this.log_pane.save(@out, false, null); + } else if (this.stack.visible_child == this.system_pane) { + this.system_pane.save(@out, null); } - clipboard_value = rows.str; - } else if (this.stack.visible_child == this.detail_pane) { - clipboard_value = this.details; + + // Ensure the data is a valid string + out.put_byte(0, null); + } catch (GLib.Error err) { + warning( + "Error saving inspector data for clipboard: %s", + err.message + ); } + string clipboard_value = (string) bytes.get_data(); if (!Geary.String.is_empty(clipboard_value)) { get_clipboard(Gdk.SELECTION_CLIPBOARD).set_text(clipboard_value, -1); } @@ -348,14 +207,6 @@ public class Components.Inspector : Gtk.ApplicationWindow { } } - [GtkCallback] - private void on_logs_size_allocate() { - if (this.autoscroll) { - update_scrollbar(); - } - } - - [GtkCallback] private void on_logs_selection_changed() { update_ui(); } @@ -363,13 +214,12 @@ public class Components.Inspector : Gtk.ApplicationWindow { private void on_logs_search_toggled(GLib.SimpleAction action, GLib.Variant? param) { bool enabled = !((bool) action.state); - this.search_bar.set_search_mode(enabled); + this.log_pane.search_mode_enabled = 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, @@ -379,60 +229,8 @@ public class Components.Inspector : Gtk.ApplicationWindow { 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(); - } - -} diff --git a/src/client/meson.build b/src/client/meson.build index df1cacdc..95aab1e3 100644 --- a/src/client/meson.build +++ b/src/client/meson.build @@ -24,6 +24,8 @@ geary_client_vala_sources = files( 'components/client-web-view.vala', 'components/components-inspector.vala', + 'components/components-inspector-log-view.vala', + 'components/components-inspector-system-view.vala', 'components/components-placeholder-pane.vala', 'components/components-validator.vala', 'components/count-badge.vala', diff --git a/ui/components-inspector-log-view.ui b/ui/components-inspector-log-view.ui new file mode 100644 index 00000000..6777cc1d --- /dev/null +++ b/ui/components-inspector-log-view.ui @@ -0,0 +1,83 @@ + + + + + + + + + + + + + Inspector opened + + + + + diff --git a/ui/components-inspector-system-view.ui b/ui/components-inspector-system-view.ui new file mode 100644 index 00000000..8ef50dc4 --- /dev/null +++ b/ui/components-inspector-system-view.ui @@ -0,0 +1,61 @@ + + + + + + + diff --git a/ui/components-inspector.ui b/ui/components-inspector.ui index 9c0080ac..8669200e 100644 --- a/ui/components-inspector.ui +++ b/ui/components-inspector.ui @@ -3,17 +3,6 @@ - - - - - - - - Inspector opened - - - diff --git a/ui/org.gnome.Geary.gresource.xml b/ui/org.gnome.Geary.gresource.xml index 3fd3b76e..1065416e 100644 --- a/ui/org.gnome.Geary.gresource.xml +++ b/ui/org.gnome.Geary.gresource.xml @@ -11,6 +11,8 @@ client-web-view.js client-web-view-allow-remote-images.js components-inspector.ui + components-inspector-log-view.ui + components-inspector-system-view.ui components-placeholder-pane.ui composer-headerbar.ui composer-link-popover.ui