[GUI] Console Search & Log Path Selector (#74)

* Add console search filter and log file path selector

* Add console search filter and log file path selector

* Increase max width of ConsoleSearchBox
This commit is contained in:
GamingChelsea
2026-07-12 01:31:20 +02:00
committed by GitHub
parent d4bb282c49
commit 5bee81df70
3 changed files with 161 additions and 34 deletions
+12 -1
View File
@@ -23,9 +23,20 @@ public sealed class GuiSettings
public bool StrictDynlibResolution { get; set; } public bool StrictDynlibResolution { get; set; }
/// <summary>Mirror emulator output to user/logs/&lt;titleId&gt;-&lt;timestamp&gt;.log.</summary> /// <summary>
/// Mirror emulator output to user/logs/&lt;titleId&gt;-&lt;timestamp&gt;.log, if <see cref="LogFilePath"/> is null.
/// </summary>
public bool LogToFile { get; set; } public bool LogToFile { get; set; }
/// <summary>If <see cref="LogToFile"/> is true it logs to this file path.</summary>
public string? LogFilePath { get; set; }
/// <summary>
/// If <see cref="OverrideLogFile"/> is false it appends &lt;titleId&gt;-&lt;timestamp&gt; to the filename specified by
/// <see cref="LogFilePath"/>. Otherwise it uses the exact filename from <see cref="LogFilePath"/>
/// </summary>
public bool OverrideLogFile { get; set; }
/// <summary>Loop the selected game's sce_sys/snd0.at9 preview music.</summary> /// <summary>Loop the selected game's sce_sys/snd0.at9 preview music.</summary>
public bool PlayTitleMusic { get; set; } = true; public bool PlayTitleMusic { get; set; } = true;
+16 -5
View File
@@ -159,13 +159,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240" <Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
Margin="0,12,0,0" IsVisible="False"> Margin="0,12,0,0" IsVisible="False">
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,12,16,8"> <Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto, Auto" Margin="16,12,16,8">
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" /> <TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True" <TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
FontSize="12" Margin="0,0,12,0" /> Watermark="Search..." MaxWidth="5500" />
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12" <CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
FontSize="12" Margin="0,0,12,0" />
<Button Grid.Column="3" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
Padding="10,4" Margin="0,0,8,0" /> Padding="10,4" Margin="0,0,8,0" />
<Button Grid.Column="3" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12" <Button Grid.Column="4" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
Padding="10,4" /> Padding="10,4" />
</Grid> </Grid>
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0" <ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
@@ -252,6 +254,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
<TextBlock Classes="fieldLabel" Text="Log to file" /> <TextBlock Classes="fieldLabel" Text="Log to file" />
<ToggleSwitch x:Name="LogToFileToggle" OnContent="On" OffContent="Off" /> <ToggleSwitch x:Name="LogToFileToggle" OnContent="On" OffContent="Off" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Log file path" />
<Button x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
VerticalAlignment="Center" />
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Override log file" />
<ToggleSwitch x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off" />
</StackPanel>
<StackPanel Margin="0,0,24,0"> <StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Title music" /> <TextBlock Classes="fieldLabel" Text="Title music" />
<ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off" IsChecked="True" /> <ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off" IsChecked="True" />
+133 -28
View File
@@ -1,12 +1,8 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Reflection;
using System.Text.Json;
using System.Diagnostics;
using Avalonia; using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@@ -17,6 +13,11 @@ using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using SharpEmu.Libs.Pad; using SharpEmu.Libs.Pad;
using SharpEmu.Logging; using SharpEmu.Logging;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
namespace SharpEmu.GUI; namespace SharpEmu.GUI;
@@ -33,7 +34,8 @@ public partial class MainWindow : Window
private readonly List<GameEntry> _allGames = new(); private readonly List<GameEntry> _allGames = new();
private readonly ObservableCollection<GameEntry> _visibleGames = new(); private readonly ObservableCollection<GameEntry> _visibleGames = new();
private readonly ObservableCollection<LogLine> _consoleLines = new(); private readonly AvaloniaList<LogLine> _consoleLines = new();
private readonly List<LogLine> _allConsoleLines = new();
private readonly ConcurrentQueue<(string Line, bool IsError)> _pendingLines = new(); private readonly ConcurrentQueue<(string Line, bool IsError)> _pendingLines = new();
private readonly DispatcherTimer _consoleFlushTimer; private readonly DispatcherTimer _consoleFlushTimer;
@@ -84,16 +86,20 @@ public partial class MainWindow : Window
GameList.SelectionChanged += (_, _) => UpdateSelectedGame(); GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
GameList.DoubleTapped += (_, _) => LaunchSelected(); GameList.DoubleTapped += (_, _) => LaunchSelected();
SearchBox.TextChanged += (_, _) => RefreshVisibleGames(); SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
ConsoleSearchBox.TextChanged += (_, _) => RefreshVisibleConsoleLines();
AddFolderButton.Click += async (_, _) => await AddFolderAsync(); AddFolderButton.Click += async (_, _) => await AddFolderAsync();
EmptyAddFolderButton.Click += async (_, _) => await AddFolderAsync(); EmptyAddFolderButton.Click += async (_, _) => await AddFolderAsync();
RescanButton.Click += async (_, _) => await RescanLibraryAsync(); RescanButton.Click += async (_, _) => await RescanLibraryAsync();
OpenFileButton.Click += async (_, _) => await OpenFileAsync(); OpenFileButton.Click += async (_, _) => await OpenFileAsync();
LaunchButton.Click += (_, _) => LaunchSelected(); LaunchButton.Click += (_, _) => LaunchSelected();
StopButton.Click += (_, _) => _emulator?.Stop();
ClearLogButton.Click += (_, _) => { _consoleLines.Clear(); _allConsoleLines.Clear(); };
StopButton.Click += (_, _) => StopEmulator(); StopButton.Click += (_, _) => StopEmulator();
ClearLogButton.Click += (_, _) => _consoleLines.Clear(); ClearLogButton.Click += (_, _) => _consoleLines.Clear();
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync(); CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true; OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true;
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true; ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true;
SelectLogFilePathButton.Click += async (_, _) => await SelectFilePathAsync();
TitleMusicToggle.IsCheckedChanged += (_, _) => OnTitleMusicToggled(); TitleMusicToggle.IsCheckedChanged += (_, _) => OnTitleMusicToggled();
DiscordToggle.IsCheckedChanged += (_, _) => DiscordToggle.IsCheckedChanged += (_, _) =>
{ {
@@ -323,7 +329,9 @@ public partial class MainWindow : Window
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096); TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
StrictToggle.IsChecked = _settings.StrictDynlibResolution; StrictToggle.IsChecked = _settings.StrictDynlibResolution;
LogToFileToggle.IsChecked = _settings.LogToFile; LogToFileToggle.IsChecked = _settings.LogToFile;
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic; TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
ToolTip.SetTip(SelectLogFilePathButton, string.IsNullOrWhiteSpace(_settings.LogFilePath) ? "No path selected" : _settings.LogFilePath);
DiscordToggle.IsChecked = _settings.DiscordRichPresence; DiscordToggle.IsChecked = _settings.DiscordRichPresence;
} }
@@ -333,6 +341,7 @@ public partial class MainWindow : Window
_settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0); _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
_settings.StrictDynlibResolution = StrictToggle.IsChecked == true; _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
_settings.LogToFile = LogToFileToggle.IsChecked == true; _settings.LogToFile = LogToFileToggle.IsChecked == true;
_settings.OverrideLogFile = OverrideLogFileToggle.IsChecked == true;
_settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true; _settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true;
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true; _settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
} }
@@ -1028,16 +1037,51 @@ public partial class MainWindow : Window
// Mirror everything the console pane shows into a log file for the // Mirror everything the console pane shows into a log file for the
// duration of the run, regardless of the emulator's log level. // duration of the run, regardless of the emulator's log level.
DropFileLog(); DropFileLog();
if (_settings.LogToFile && BuildLogFilePath(titleId) is { } logFilePath) if (_settings.LogToFile)
{ {
try string filePath;
if (!string.IsNullOrWhiteSpace(_settings.LogFilePath))
{ {
_fileLog = new StreamWriter(logFilePath, append: false); if (_settings.OverrideLogFile)
AppendConsoleLine($"Log file: {logFilePath}", DimLineBrush); {
filePath = _settings.LogFilePath;
}
else
{
string path = _settings.LogFilePath;
string id = string.IsNullOrWhiteSpace(titleId) ? "UNKNOWN" : titleId;
foreach (var invalidChar in Path.GetInvalidFileNameChars())
{
id = id.Replace(invalidChar.ToString(), "");
}
string identifier = $"{id}-{DateTime.Now:yyyyMMdd-HHmmss}";
string? dir = Path.GetDirectoryName(path);
string? fileName = Path.GetFileNameWithoutExtension(path);
string? extension = Path.GetExtension(path);
string newFileName = $"{fileName}-{identifier}{extension}";
filePath = string.IsNullOrEmpty(dir)
? newFileName
: Path.Combine(dir, newFileName);
}
} }
catch (Exception ex) else
{ {
AppendConsoleLine($"Could not open the log file: {ex.Message}", WarningLineBrush); filePath = BuildLogFilePath(titleId) ?? string.Empty;
}
if (!string.IsNullOrEmpty(filePath))
{
try
{
_fileLog = new StreamWriter(filePath, append: false);
AppendConsoleLine($"Log file: {filePath}", DimLineBrush);
}
catch (Exception ex)
{
AppendConsoleLine($"Could not open the log file: {ex.Message}", WarningLineBrush);
}
} }
} }
@@ -1162,6 +1206,27 @@ public partial class MainWindow : Window
OpenFileButton.IsEnabled = !_isRunning; OpenFileButton.IsEnabled = !_isRunning;
} }
private async Task SelectFilePathAsync()
{
SaveFilePickerResult result = await StorageProvider.SaveFilePickerWithResultAsync(new FilePickerSaveOptions
{
Title = "Select where to save the Log file",
SuggestedFileName = "SharpEmuLog",
DefaultExtension = "log",
FileTypeChoices =
[
new FilePickerFileType("Plain Text Files") { Patterns = ["*.txt"] },
new FilePickerFileType("Log Files") { Patterns = ["*.log"] }
]
});
if (result.File is not null)
{
_settings.LogFilePath = result.File.Path.LocalPath;
ToolTip.SetTip(SelectLogFilePathButton, _settings.LogFilePath);
}
}
// ---- Console ---- // ---- Console ----
private void FlushPendingConsoleLines() private void FlushPendingConsoleLines()
@@ -1180,27 +1245,28 @@ public partial class MainWindow : Window
FlushFileLog(); FlushFileLog();
if (incoming.Count >= MaxConsoleLines) _allConsoleLines.AddRange(incoming);
string query = ConsoleSearchBox.Text ?? string.Empty;
IEnumerable<LogLine> linesToAdd = incoming;
if (!string.IsNullOrWhiteSpace(query))
{ {
// A burst larger than the cap: keep only the newest lines. linesToAdd = incoming.Where(line =>
_consoleLines.Clear(); line.Text != null &&
for (var i = incoming.Count - MaxConsoleLines; i < incoming.Count; i++) line.Text.Contains(query, StringComparison.OrdinalIgnoreCase));
{
_consoleLines.Add(incoming[i]);
}
} }
else _consoleLines.AddRange(linesToAdd);
var overflow = _consoleLines.Count - MaxConsoleLines;
while (_allConsoleLines.Count > MaxConsoleLines)
{ {
var overflow = _consoleLines.Count + incoming.Count - MaxConsoleLines; var droppedLine = _allConsoleLines[0];
for (var i = 0; i < overflow; i++) _allConsoleLines.RemoveAt(0);
if (_consoleLines.Count > 0 && _consoleLines[0] == droppedLine)
{ {
_consoleLines.RemoveAt(0); _consoleLines.RemoveAt(0);
} }
foreach (var line in incoming)
{
_consoleLines.Add(line);
}
} }
_autoScrollTicks = 3; _autoScrollTicks = 3;
@@ -1210,11 +1276,50 @@ public partial class MainWindow : Window
{ {
WriteFileLog(text); WriteFileLog(text);
FlushFileLog(); FlushFileLog();
_consoleLines.Add(new LogLine(text, brush));
var line = new LogLine(text, brush);
_allConsoleLines.Add(line);
string query = ConsoleSearchBox.Text ?? string.Empty;
if (string.IsNullOrWhiteSpace(query) || (text != null && text.Contains(query, StringComparison.OrdinalIgnoreCase)))
{
_consoleLines.Add(line);
}
while (_allConsoleLines.Count > MaxConsoleLines)
{
var droppedLine = _allConsoleLines[0];
_allConsoleLines.RemoveAt(0);
if (_consoleLines.Count > 0 && _consoleLines[0] == droppedLine)
{
_consoleLines.RemoveAt(0);
}
}
_autoScrollTicks = 3; _autoScrollTicks = 3;
MaybeAutoScroll(); MaybeAutoScroll();
} }
private void RefreshVisibleConsoleLines()
{
string query = ConsoleSearchBox.Text ?? string.Empty;
_consoleLines.Clear();
if (string.IsNullOrWhiteSpace(query))
{
_consoleLines.AddRange(_allConsoleLines);
}
else
{
var filtered = _allConsoleLines.Where(line =>
line.Text != null &&
line.Text.Contains(query, StringComparison.OrdinalIgnoreCase));
_consoleLines.AddRange(filtered);
}
}
// ---- Console-to-file mirroring ---- // ---- Console-to-file mirroring ----
private void WriteFileLog(string text) private void WriteFileLog(string text)