From fbf2c2d00ae60f7adcd5d418cb1bc9b169e843ca Mon Sep 17 00:00:00 2001 From: Kaotic Date: Sun, 12 Jul 2026 23:16:21 +0200 Subject: [PATCH] [GUI] Add detachable console window (#91) Add a Split button to detach the emulator console into a resizable window. The detached console keeps search, auto-scroll, copy, and clear controls. The main console hides while detached and restores when the split window closes if it was previously open. --- src/SharpEmu.GUI/ConsoleWindow.cs | 150 +++++++++++++++++++++++++++ src/SharpEmu.GUI/MainWindow.axaml | 12 ++- src/SharpEmu.GUI/MainWindow.axaml.cs | 29 +++++- 3 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 src/SharpEmu.GUI/ConsoleWindow.cs diff --git a/src/SharpEmu.GUI/ConsoleWindow.cs b/src/SharpEmu.GUI/ConsoleWindow.cs new file mode 100644 index 0000000..127cbe7 --- /dev/null +++ b/src/SharpEmu.GUI/ConsoleWindow.cs @@ -0,0 +1,150 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Threading; +using System.Collections.Specialized; + +namespace SharpEmu.GUI; + +public sealed class ConsoleWindow : Window +{ + private readonly AvaloniaList _sourceLines; + private readonly AvaloniaList _visibleLines = new(); + private readonly ListBox _list; + private readonly TextBox _searchBox; + private readonly CheckBox _autoScrollCheck; + + public ConsoleWindow( + AvaloniaList lines, + Action clear, + bool autoScroll) + { + _sourceLines = lines; + Title = "SharpEmu Console"; + Width = 980; + Height = 620; + MinWidth = 520; + MinHeight = 320; + Background = new SolidColorBrush(Color.Parse("#0D1017")); + Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico"))); + + _searchBox = new TextBox { Watermark = "Search...", Width = 320, Margin = new Thickness(0, 0, 12, 0) }; + _autoScrollCheck = new CheckBox + { + Content = "Auto-scroll", + IsChecked = autoScroll, + FontSize = 12, + Margin = new Thickness(0, 0, 12, 0), + VerticalAlignment = VerticalAlignment.Center, + }; + var copyButton = new Button + { + Classes = { "ghost" }, + Content = "Copy", + Padding = new Thickness(10, 4), + Margin = new Thickness(0, 0, 8, 0), + }; + var clearButton = new Button { Classes = { "ghost" }, Content = "Clear", Padding = new Thickness(10, 4) }; + copyButton.Click += async (_, _) => await CopyAsync(); + clearButton.Click += (_, _) => clear(); + _searchBox.TextChanged += (_, _) => RefreshVisibleLines(); + + _list = new ListBox + { + Classes = { "console" }, + ItemsSource = _visibleLines, + BorderThickness = new Thickness(1), + BorderBrush = new SolidColorBrush(Color.Parse("#232B3A")), + ItemTemplate = new FuncDataTemplate((_, _) => + { + var text = new TextBlock { TextWrapping = TextWrapping.NoWrap }; + text.Bind(TextBlock.TextProperty, new Binding(nameof(LogLine.Text))); + text.Bind(TextBlock.ForegroundProperty, new Binding(nameof(LogLine.Brush))); + return text; + }), + }; + + Content = new Grid + { + Margin = new Thickness(12), + RowDefinitions = new RowDefinitions("Auto,*"), + Children = + { + new Grid + { + Margin = new Thickness(0, 0, 0, 8), + ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"), + Children = + { + new TextBlock + { + Classes = { "sectionTitle" }, + Text = "CONSOLE", + VerticalAlignment = VerticalAlignment.Center, + }, + _searchBox.WithGridColumn(1), + _autoScrollCheck.WithGridColumn(2), + copyButton.WithGridColumn(3), + clearButton.WithGridColumn(4), + }, + }, + _list.WithGridRow(1), + }, + }; + + lines.CollectionChanged += OnLinesChanged; + Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged; + RefreshVisibleLines(); + } + + private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + RefreshVisibleLines(); + if (_autoScrollCheck.IsChecked == true) + { + Dispatcher.UIThread.Post(() => (_list.Scroll as ScrollViewer)?.ScrollToEnd()); + } + } + + private void RefreshVisibleLines() + { + var query = _searchBox.Text ?? string.Empty; + _visibleLines.Clear(); + _visibleLines.AddRange(string.IsNullOrWhiteSpace(query) + ? _sourceLines + : _sourceLines.Where(line => line.Text.Contains(query, StringComparison.OrdinalIgnoreCase))); + } + + private async Task CopyAsync() + { + if (_visibleLines.Count == 0 || Clipboard is null) + { + return; + } + + await Clipboard.SetTextAsync(string.Join(Environment.NewLine, _visibleLines.Select(line => line.Text))); + } +} + +file static class GridExtensions +{ + public static T WithGridColumn(this T control, int column) where T : Control + { + Grid.SetColumn(control, column); + return control; + } + + public static T WithGridRow(this T control, int row) where T : Control + { + Grid.SetRow(control, row); + return control; + } +} diff --git a/src/SharpEmu.GUI/MainWindow.axaml b/src/SharpEmu.GUI/MainWindow.axaml index 53b6871..555f3f5 100644 --- a/src/SharpEmu.GUI/MainWindow.axaml +++ b/src/SharpEmu.GUI/MainWindow.axaml @@ -160,15 +160,17 @@ SPDX-License-Identifier: GPL-2.0-or-later - + - + -