From 1bc6ef131729a2ddc30a55ba49702a0dbf9c2d35 Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Fri, 30 Aug 2024 15:08:16 +0200 Subject: [PATCH] feature: memory search UI Signed-off-by: Maximilien Noal --- src/Spice86.Shared/Utils/ConvertUtils.cs | 31 +++- .../InvalidNumberToQuestionMarkConverter.cs | 17 +- src/Spice86/Spice86DependencyInjection.cs | 8 +- .../ViewModels/DebugWindowViewModel.cs | 4 +- src/Spice86/ViewModels/MemoryViewModel.cs | 146 +++++++++++++++++- src/Spice86/Views/MemoryView.axaml | 83 +++++++++- 6 files changed, 269 insertions(+), 20 deletions(-) diff --git a/src/Spice86.Shared/Utils/ConvertUtils.cs b/src/Spice86.Shared/Utils/ConvertUtils.cs index 94c8bc3aa..926c908b4 100644 --- a/src/Spice86.Shared/Utils/ConvertUtils.cs +++ b/src/Spice86.Shared/Utils/ConvertUtils.cs @@ -2,6 +2,7 @@ using Spice86.Shared.Emulator.Memory; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; using System.Text.RegularExpressions; @@ -91,6 +92,8 @@ public static uint BytesToInt32(byte[] data, int start) { return (uint)((data[start] << 24 & 0xFF000000) | ((uint)data[start + 1] << 16 & 0x00FF0000) | ((uint)data[start + 2] << 8 & 0x0000FF00) | ((uint)data[start + 3] & 0x000000FF)); } + private const int HexadecimalByteDigitLength = 2; + /// /// Converts a hexadecimal string to a byte array. /// @@ -99,12 +102,36 @@ public static uint BytesToInt32(byte[] data, int start) { public static byte[] HexToByteArray(string valueString) { byte[] res = new byte[valueString.Length / 2]; for (int i = 0; i < valueString.Length; i += 2) { - string hex = valueString.Substring(i, 2); - res[i / 2] = byte.Parse(hex, NumberStyles.HexNumber); + string hex = valueString.Substring(i, HexadecimalByteDigitLength); + res[i / HexadecimalByteDigitLength] = byte.Parse(hex, NumberStyles.HexNumber); } return res; } + + /// + /// Tries to convert a hexadecimal string to a byte array. + /// + /// The hexadecimal string to convert. + /// The byte array representation of the hexadecimal string. + /// False if the conversion fails, True otherwise + public static bool TryParseHexToByteArray(string valueString, [NotNullWhen(true)] out byte[]? bytes) { + byte[] result = new byte[valueString.Length / 2]; + for (int i = 0; i < valueString.Length; i += HexadecimalByteDigitLength) { + if(i + HexadecimalByteDigitLength > valueString.Length) { + bytes = null; + return false; + } + string hex = valueString.Substring(i, HexadecimalByteDigitLength); + if (!byte.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out byte b)) { + bytes = null; + return false; + } + result[i / 2] = b; + } + bytes = result; + return true; + } /// Sign extend value considering it is a 16 bit value /// diff --git a/src/Spice86/Converters/InvalidNumberToQuestionMarkConverter.cs b/src/Spice86/Converters/InvalidNumberToQuestionMarkConverter.cs index 955fa5410..5affb1f80 100644 --- a/src/Spice86/Converters/InvalidNumberToQuestionMarkConverter.cs +++ b/src/Spice86/Converters/InvalidNumberToQuestionMarkConverter.cs @@ -11,8 +11,7 @@ internal class InvalidNumberToQuestionMarkConverter : IValueConverter { /// public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - switch (value) - { + switch (value) { case null: case long l when l == -1: case int i when i == -1: @@ -30,12 +29,10 @@ public object Convert(object? value, Type targetType, object? parameter, Culture /// public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { - if (value is string str && str == "?") { - return -1; - } - if (value is string longStr && long.TryParse(longStr, out long longValue)) { - return longValue; - } - return null; + return value switch { + string and "?" => -1, + string longStr when long.TryParse(longStr, out long longValue) => longValue, + _ => null + }; } -} +} \ No newline at end of file diff --git a/src/Spice86/Spice86DependencyInjection.cs b/src/Spice86/Spice86DependencyInjection.cs index fc2e50c9c..25f084a03 100644 --- a/src/Spice86/Spice86DependencyInjection.cs +++ b/src/Spice86/Spice86DependencyInjection.cs @@ -145,9 +145,11 @@ public Spice86DependencyInjection(ILoggerService loggerService, Configuration co ClassicDesktopStyleApplicationLifetime? desktop = null; ITextClipboard? textClipboard = null; IHostStorageProvider? hostStorageProvider = null; + IUIDispatcher? uiThreadDispatcher = null; if (!configuration.HeadlessMode) { desktop = CreateDesktopApp(); + uiThreadDispatcher = new UIDispatcher(Dispatcher.UIThread); PerformanceViewModel performanceViewModel = new(state, pauseHandler); mainWindow = new() { PerformanceViewModel = performanceViewModel @@ -156,7 +158,7 @@ public Spice86DependencyInjection(ILoggerService loggerService, Configuration co hostStorageProvider = new HostStorageProvider(mainWindow.StorageProvider, configuration, emulatorStateSerializer); mainWindowViewModel = new MainWindowViewModel( - timer, new UIDispatcher(Dispatcher.UIThread), hostStorageProvider, textClipboard, configuration, + timer, uiThreadDispatcher, hostStorageProvider, textClipboard, configuration, loggerService, pauseHandler); } @@ -236,11 +238,11 @@ public Spice86DependencyInjection(ILoggerService loggerService, Configuration co } DebugWindowViewModel? debugWindowViewModel = null; - if (textClipboard != null && hostStorageProvider != null) { + if (textClipboard != null && hostStorageProvider != null && uiThreadDispatcher != null) { IMessenger messenger = WeakReferenceMessenger.Default; debugWindowViewModel = new DebugWindowViewModel(state, memory, midiDevice, videoState.DacRegisters.ArgbPalette, softwareMixer, vgaRenderer, videoState, - cfgCpu.ExecutionContextManager, messenger, textClipboard, hostStorageProvider, + cfgCpu.ExecutionContextManager, messenger, uiThreadDispatcher, textClipboard, hostStorageProvider, new StructureViewModelFactory(configuration, loggerService, pauseHandler), pauseHandler); } diff --git a/src/Spice86/ViewModels/DebugWindowViewModel.cs b/src/Spice86/ViewModels/DebugWindowViewModel.cs index a484b233a..b1a8d044d 100644 --- a/src/Spice86/ViewModels/DebugWindowViewModel.cs +++ b/src/Spice86/ViewModels/DebugWindowViewModel.cs @@ -55,7 +55,7 @@ public partial class DebugWindowViewModel : ViewModelBase, public DebugWindowViewModel(State cpuState, IMemory memory, Midi externalMidiDevice, ArgbPalette argbPalette, SoftwareMixer softwareMixer, IVgaRenderer vgaRenderer, VideoState videoState, - ExecutionContextManager executionContextManager, IMessenger messenger, + ExecutionContextManager executionContextManager, IMessenger messenger, IUIDispatcher uiDispatcher, ITextClipboard textClipboard, IHostStorageProvider storageProvider, IStructureViewModelFactory structureViewModelFactory, IPauseHandler pauseHandler) { messenger.Register>(this); @@ -73,7 +73,7 @@ public DebugWindowViewModel(State cpuState, IMemory memory, Midi externalMidiDev VideoCardViewModel = new(vgaRenderer, videoState); CpuViewModel = new(cpuState, pauseHandler); MidiViewModel = new(externalMidiDevice); - MemoryViewModels.Add(new(memory, pauseHandler, messenger, textClipboard, storageProvider, structureViewModelFactory)); + MemoryViewModels.Add(new(memory, pauseHandler, messenger, uiDispatcher, textClipboard, storageProvider, structureViewModelFactory)); CfgCpuViewModel = new(executionContextManager, pauseHandler, new PerformanceMeasurer()); } diff --git a/src/Spice86/ViewModels/MemoryViewModel.cs b/src/Spice86/ViewModels/MemoryViewModel.cs index ecf9dd9d6..b842d35b7 100644 --- a/src/Spice86/ViewModels/MemoryViewModel.cs +++ b/src/Spice86/ViewModels/MemoryViewModel.cs @@ -18,12 +18,38 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Text; public partial class MemoryViewModel : ViewModelWithErrorDialog { private readonly IMemory _memory; private readonly IStructureViewModelFactory _structureViewModelFactory; private readonly IMessenger _messenger; private readonly IPauseHandler _pauseHandler; + private readonly IUIDispatcher _uiDispatcher; + + public enum MemorySearchDataType { + Binary, + Ascii, + } + + [ObservableProperty] + private MemorySearchDataType _searchDataType; + + public bool SearchDataTypeIsBinary => SearchDataType == MemorySearchDataType.Binary; + + public bool SearchDataTypeIsAscii => SearchDataType == MemorySearchDataType.Ascii; + + [RelayCommand] + private void SetSearchDataTypeToBinary() => SetSearchDataType(MemorySearchDataType.Binary); + + private void SetSearchDataType(MemorySearchDataType searchDataType) { + SearchDataType = searchDataType; + OnPropertyChanged(nameof(SearchDataTypeIsBinary)); + OnPropertyChanged(nameof(SearchDataTypeIsAscii)); + } + + [RelayCommand] + private void SetSearchDataTypeToAscii() => SetSearchDataType(MemorySearchDataType.Ascii); [ObservableProperty] private DataMemoryDocument? _dataMemoryDocument; @@ -72,6 +98,110 @@ public uint? EndAddress { TryUpdateHeaderAndMemoryDocument(); } } + + [RelayCommand(CanExecute = nameof(IsPaused))] + public async Task CopySelection() { + if (SelectionRange is not null && StartAddress is not null) { + byte[] memoryBytes = _memory.GetData( + (uint)(StartAddress.Value + SelectionRange.Value.Start.ByteIndex), + (uint)SelectionRange.Value.ByteLength); + string hexRepresentation = ConvertUtils.ByteArrayToHexString(memoryBytes); + await _textClipboard.SetTextAsync($"{hexRepresentation}").ConfigureAwait(false); + } + } + + [ObservableProperty] + private string? _memorySearchValue; + + private enum SearchDirection { + FirstOccurence, + Forward, + Backward, + } + + private SearchDirection _searchDirection; + + [RelayCommand] + private async Task PreviousOccurrence() { + if (SearchMemoryCommand.CanExecute(null)) { + _searchDirection = SearchDirection.Backward; + await SearchMemoryCommand.ExecuteAsync(null).ConfigureAwait(false); + } + } + + [RelayCommand] + private async Task NextOccurrence() { + if (SearchMemoryCommand.CanExecute(null)) { + _searchDirection = SearchDirection.Forward; + await SearchMemoryCommand.ExecuteAsync(null).ConfigureAwait(false); + } + } + + [RelayCommand] + private async Task FirstOccurrence() { + if (SearchMemoryCommand.CanExecute(null)) { + _searchDirection = SearchDirection.FirstOccurence; + await SearchMemoryCommand.ExecuteAsync(null).ConfigureAwait(false); + } + } + + [ObservableProperty] + private bool _isBusy; + + [RelayCommand] + private void StartMemorySearch() { + IsSearchingMemory = true; + } + + [ObservableProperty] + private bool _isSearchingMemory; + + [ObservableProperty] + private uint? _addressOFoundOccurence; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(GoToFoundOccurenceCommand))] + private bool _isAddressOfFoundOccurrenceValid; + + [RelayCommand(CanExecute = nameof(IsPaused), FlowExceptionsToTaskScheduler = false, IncludeCancelCommand = true)] + private async Task SearchMemory(CancellationToken token) { + if (string.IsNullOrWhiteSpace(MemorySearchValue) || token.IsCancellationRequested) { + return; + } + try { + IsBusy = true; + int searchLength = (int)_memory.Length; + uint searchStartAddress = 0; + + if (_searchDirection == SearchDirection.Forward && AddressOFoundOccurence is not null) { + searchStartAddress = AddressOFoundOccurence.Value + 1; + searchLength = (int)(_memory.Length - searchStartAddress); + } else if (_searchDirection == SearchDirection.Backward && AddressOFoundOccurence is not null) { + searchStartAddress = 0; + searchLength = (int)(AddressOFoundOccurence.Value - 1); + } + if(SearchDataType == MemorySearchDataType.Binary && ConvertUtils.TryParseHexToByteArray(MemorySearchValue, out byte[]? searchBytes)) { + AddressOFoundOccurence = await Task.Run( + () =>_memory.SearchValue(searchStartAddress, searchLength, searchBytes), token).ConfigureAwait(false); + } else if(SearchDataType == MemorySearchDataType.Ascii) { + searchBytes = Encoding.ASCII.GetBytes(MemorySearchValue); + AddressOFoundOccurence = await Task.Run( + () => _memory.SearchValue(searchStartAddress, searchLength, searchBytes), token).ConfigureAwait(false); + } + } finally { + await _uiDispatcher.InvokeAsync(() => { + IsBusy = false; + IsAddressOfFoundOccurrenceValid = AddressOFoundOccurence is not null; + }); + } + } + + [RelayCommand(CanExecute = nameof(IsAddressOfFoundOccurrenceValid))] + private void GoToFoundOccurence() { + if (AddressOFoundOccurence is not null && NewMemoryViewCommand.CanExecute(null)) { + CreateNewMemoryView(AddressOFoundOccurence); + } + } [ObservableProperty] private string _header = "Memory View"; @@ -79,6 +209,8 @@ public uint? EndAddress { [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(NewMemoryViewCommand))] [NotifyCanExecuteChangedFor(nameof(EditMemoryCommand))] + [NotifyCanExecuteChangedFor(nameof(CopySelectionCommand))] + [NotifyCanExecuteChangedFor(nameof(SearchMemoryCommand))] private bool _isPaused; [ObservableProperty] @@ -88,8 +220,11 @@ public uint? EndAddress { private readonly IHostStorageProvider _storageProvider; - public MemoryViewModel(IMemory memory, IPauseHandler pauseHandler, IMessenger messenger, ITextClipboard textClipboard, IHostStorageProvider storageProvider, IStructureViewModelFactory structureViewModelFactory, bool canCloseTab = false, uint startAddress = 0, uint endAddress = A20Gate.EndOfHighMemoryArea) : base(textClipboard) { + public MemoryViewModel(IMemory memory, IPauseHandler pauseHandler, IMessenger messenger, IUIDispatcher uiDispatcher, + ITextClipboard textClipboard, IHostStorageProvider storageProvider, IStructureViewModelFactory structureViewModelFactory, + bool canCloseTab = false, uint startAddress = 0, uint endAddress = A20Gate.EndOfHighMemoryArea) : base(textClipboard) { _pauseHandler = pauseHandler; + _uiDispatcher = uiDispatcher; _memory = memory; _pauseHandler.Pausing += OnPause; IsPaused = pauseHandler.IsPaused; @@ -143,9 +278,16 @@ private void OnPause() { [RelayCommand(CanExecute = nameof(IsPaused))] private void NewMemoryView() { - MemoryViewModel memoryViewModel = new(_memory, _pauseHandler, _messenger, _textClipboard, + CreateNewMemoryView(); + } + + private void CreateNewMemoryView(uint? startAddress = null) { + MemoryViewModel memoryViewModel = new(_memory, _pauseHandler, _messenger, _uiDispatcher, _textClipboard, _storageProvider, _structureViewModelFactory, canCloseTab: true); + if (startAddress is not null) { + memoryViewModel.StartAddress = startAddress; + } _messenger.Send(new AddViewModelMessage(memoryViewModel)); } diff --git a/src/Spice86/Views/MemoryView.axaml b/src/Spice86/Views/MemoryView.axaml index fad2da18d..bc6f57303 100644 --- a/src/Spice86/Views/MemoryView.axaml +++ b/src/Spice86/Views/MemoryView.axaml @@ -2,6 +2,7 @@ x:Class="Spice86.Views.MemoryView" xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:converters="clr-namespace:Spice86.Converters" xmlns:avaloniaHex="clr-namespace:AvaloniaHex;assembly=AvaloniaHex" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia" @@ -14,10 +15,14 @@ x:CompileBindings="True" x:DataType="viewModels:MemoryViewModel" mc:Ignorable="d"> + + +