diff --git a/demo/GraniteDemo.vala b/demo/GraniteDemo.vala index 9b1204abd..60212f195 100644 --- a/demo/GraniteDemo.vala +++ b/demo/GraniteDemo.vala @@ -27,6 +27,7 @@ public class Granite.Demo : Gtk.Application { var controls_view = new ControlsView (); var maps_view = new MapsView (); var overlaybar_view = new OverlayBarView (); + var terminal_output_view = new TerminalOutputView (); var toast_view = new ToastView (); var settings_uris_view = new SettingsUrisView (); var style_manager_view = new StyleManagerView (); @@ -53,6 +54,7 @@ public class Granite.Demo : Gtk.Application { main_stack.add_titled (video_view, "video", video_view.title); main_stack.add_titled (overlaybar_view, "overlaybar", "OverlayBar"); main_stack.add_titled (settings_uris_view, "settings_uris", "Settings URIs"); + main_stack.add_titled (terminal_output_view, "terminal_output_view", "Terminal View"); main_stack.add_titled (toast_view, "toasts", "Toast"); main_stack.add_titled (utils_view, "utils", "Utils"); main_stack.add_titled (dialogs_view, "dialogs", "Dialogs"); diff --git a/demo/Views/CSSView.vala b/demo/Views/CSSView.vala index 6e70df169..6ee453a60 100644 --- a/demo/Views/CSSView.vala +++ b/demo/Views/CSSView.vala @@ -84,21 +84,6 @@ public class CSSView : DemoPage { card_box.append (card); card_box.append (card_checkered); - var terminal_label = new Granite.HeaderLabel ("\"terminal\" style class"); - - var terminal = new Gtk.Label ("[ 73%] Linking C executable granite-demo\n[100%] Built target granite-demo") { - selectable = true, - wrap = true, - xalign = 0, - yalign = 0 - }; - - var terminal_scroll = new Gtk.ScrolledWindow () { - min_content_height = 70, - child = terminal - }; - terminal_scroll.add_css_class (Granite.STYLE_CLASS_TERMINAL); - var accent_color_label = new Granite.HeaderLabel ("Colored labels and icons"); var accent_color_box = new Gtk.Box (HORIZONTAL, 6); @@ -139,8 +124,6 @@ public class CSSView : DemoPage { }; box.append (card_header); box.append (card_box); - box.append (terminal_label); - box.append (terminal_scroll); box.append (accent_color_label); box.append (accent_color_box); box.append (success_color_box); diff --git a/demo/Views/TerminalOutputView.vala b/demo/Views/TerminalOutputView.vala new file mode 100644 index 000000000..de71d734e --- /dev/null +++ b/demo/Views/TerminalOutputView.vala @@ -0,0 +1,33 @@ +/* + * Copyright 20205 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +public class TerminalOutputView: DemoPage { + construct { + title = "Terminal Output"; + var terminal = new Granite.TerminalView () { + autoscroll = true, + vexpand = true, + margin_top = 12, + margin_bottom = 12, + margin_start = 12, + margin_end = 12 + }; + terminal.text = "[ 25%] Performing optimization passes\n"; + terminal.text += "[ 65%] Inserting nonsense functions to pad binary size\n"; + terminal.text += "[ 73%] Linking C executable granite-demo\n"; + terminal.text += "[100%] Built target granite-demo\n"; + terminal.text += "Counting to one hundred…\n"; + + for (int i = 0; i <= 100; i++) { + var itos = i.to_string ("%i\n"); + terminal.text += itos; + } + + terminal.add_css_class (Granite.CssClass.CARD); + + child = terminal; + } + +} diff --git a/demo/meson.build b/demo/meson.build index ef8a75d02..c52e24ae6 100644 --- a/demo/meson.build +++ b/demo/meson.build @@ -18,6 +18,7 @@ executable( 'Views/OverlayBarView.vala', 'Views/SettingsUrisView.vala', 'Views/StyleManagerView.vala', + 'Views/TerminalOutputView.vala', 'Views/ToastView.vala', 'Views/UtilsView.vala', 'Views/VideoView.vala', diff --git a/lib/Constants.vala b/lib/Constants.vala index 99764619d..c4ba970b8 100644 --- a/lib/Constants.vala +++ b/lib/Constants.vala @@ -131,6 +131,7 @@ namespace Granite { * When used with {@link Gtk.Label} this style includes internal padding. When used with {@link Gtk.TextView} * interal padding will need to be set with {@link Gtk.Container.border_width} */ + [Version (deprecated = true, deprecated_since = "7.7.0", replacement = "Granite.TerminalView")] public const string STYLE_CLASS_TERMINAL = "terminal"; /** * Style class for title label text in a {@link Granite.MessageDialog} @@ -285,6 +286,12 @@ namespace Granite { * Style class for non-terminal text that uses a monospace font. */ public const string MONOSPACE = "monospace"; + + /** + * Internal style class for {@link Granite.TerminalView} to emulate the appearance of Terminal. This includes + * text color, background color, selection highlighting, and selecting the system monospace font. + */ + internal const string TERMINAL = "terminal"; } /** diff --git a/lib/Styles/Granite/Index.scss b/lib/Styles/Granite/Index.scss index 0a3a64e4e..13a73e5ed 100644 --- a/lib/Styles/Granite/Index.scss +++ b/lib/Styles/Granite/Index.scss @@ -6,5 +6,6 @@ @import 'MessageDialog.scss'; @import 'OverlayBar.scss'; @import 'Placeholder.scss'; +@import 'TerminalView.scss'; @import 'Toast.scss'; @import 'ToolBox.scss'; diff --git a/lib/Styles/Granite/TerminalView.scss b/lib/Styles/Granite/TerminalView.scss new file mode 100644 index 000000000..c9339a86e --- /dev/null +++ b/lib/Styles/Granite/TerminalView.scss @@ -0,0 +1,19 @@ +.terminal { + font-family: monospace; + + background-color: $SLATE_900; + color: $SILVER_200; + + // this is roughly 3 lines + min-height: 9ex; + + & selection { + background-color: $SILVER_200; + color: $SLATE_900; + + &:backdrop { + // Cancelling values set in non-terminal selection + background-color: inherit; + } + } +} diff --git a/lib/Styles/Granite/_classes.scss b/lib/Styles/Granite/_classes.scss index eaf129810..970eb84ad 100644 --- a/lib/Styles/Granite/_classes.scss +++ b/lib/Styles/Granite/_classes.scss @@ -47,4 +47,3 @@ paper { .monospace { font-family: monospace; } - diff --git a/lib/Widgets/MessageDialog.vala b/lib/Widgets/MessageDialog.vala index f0fdbaa08..fa4330a8d 100644 --- a/lib/Widgets/MessageDialog.vala +++ b/lib/Widgets/MessageDialog.vala @@ -169,7 +169,10 @@ public class Granite.MessageDialog : Granite.Dialog { */ private Gtk.Grid message_grid; - private Gtk.Label? details_view; + /** + * The {@link Granite.TerminalView} used to display error details + */ + private Granite.TerminalView? details_view; /** * The {@link Gtk.Expander} used to hold the error details view. @@ -308,22 +311,9 @@ public class Granite.MessageDialog : Granite.Dialog { if (details_view == null) { secondary_label.margin_bottom = 18; - details_view = new Gtk.Label ("") { - selectable = true, - wrap = true, - xalign = 0, - yalign = 0 - }; - - var scroll_box = new Gtk.ScrolledWindow () { - margin_top = 12, - min_content_height = 70, - child = details_view - }; - scroll_box.add_css_class (Granite.STYLE_CLASS_TERMINAL); - + details_view = new Granite.TerminalView (); expander = new Gtk.Expander (_("Details")) { - child = scroll_box + child = details_view }; message_grid.attach (expander, 1, 2, 1, 1); @@ -333,6 +323,6 @@ public class Granite.MessageDialog : Granite.Dialog { } } - details_view.label = error_message; + details_view.text = error_message; } } diff --git a/lib/Widgets/TerminalView.vala b/lib/Widgets/TerminalView.vala new file mode 100644 index 000000000..7de1531e3 --- /dev/null +++ b/lib/Widgets/TerminalView.vala @@ -0,0 +1,74 @@ +/** + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +[Version (since = "7.7.0")] +public class Granite.TerminalView : Granite.Bin { + + public bool autoscroll { get; set; default = false; } + public string text { get; set; } + + private Gtk.TextBuffer buffer; + private double prev_upper_adj = 0; + private Gtk.ScrolledWindow scrolled_window; + + construct { + var view = new Gtk.TextView () { + cursor_visible = false, + editable = false, + monospace = true, + pixels_below_lines = 3, + left_margin = 9, + right_margin = 9, + top_margin = 6, + bottom_margin = 6, + wrap_mode = Gtk.WrapMode.WORD + }; + + buffer = view.get_buffer (); + + scrolled_window = new Gtk.ScrolledWindow () { + child = view, + hexpand = true, + vexpand = true, + hscrollbar_policy = NEVER, + }; + + child = scrolled_window; + add_css_class (Granite.CssClass.TERMINAL); + + bind_property ("text", buffer, "text", BIDIRECTIONAL | SYNC_CREATE); + + notify["autoscroll"].connect ((s, p) => { + update_autoscroll (); + }); + } + + private void update_autoscroll () { + // FIXME: this disjoints the window closing and the application finishing + Idle.add (() => { + attempt_scroll (); + if (autoscroll) { + return GLib.Source.CONTINUE; + } else { + return GLib.Source.REMOVE; + } + }); + } + + private void attempt_scroll () { + var adj = scrolled_window.vadjustment; + var units_from_end = prev_upper_adj - adj.page_size - adj.value; + + if (adj.upper - prev_upper_adj <= 0) { + return; + } + + if (prev_upper_adj <= adj.page_size || units_from_end <= 50) { + adj.value = adj.upper; + } + + prev_upper_adj = adj.upper; + } +} diff --git a/lib/meson.build b/lib/meson.build index 5d73c9fdb..d7f1bab5f 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -33,6 +33,7 @@ libgranite_sources = files( 'Widgets/SettingsSidebar.vala', 'Widgets/Settings.vala', 'Widgets/SwitchModelButton.vala', + 'Widgets/TerminalView.vala', 'Widgets/TimePicker.vala', 'Widgets/ToolBox.vala', 'Widgets/Toast.vala',