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">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+