From 64a7a7faacd9159ca102c31687e5cb6d7f1527a2 Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Sun, 7 Jul 2024 08:52:50 +0200 Subject: [PATCH] Refactoring: UI TopLevel services and MainWindowViewModel injection (#744) * fix: Don't trigger memory read breakpoints (disasm) ... and don't support writes, as it is unused. Signed-off-by: Maximilien Noal * removed unused using Signed-off-by: Maximilien Noal * refactor: UI uses DI for most services Signed-off-by: Maximilien Noal * fix: Double validation errors (AvaloniaUI + CI) Signed-off-by: Maximilien Noal * refactor: MainWindowViewModel is an injected service now Signed-off-by: Maximilien Noal * refactor: removed commands about starting a new program Signed-off-by: Maximilien Noal * refactor: CommandLineParser is an injected service Signed-off-by: Maximilien Noal --------- Signed-off-by: Maximilien Noal --- src/Spice86.Core/CLI/CommandLineParser.cs | 16 +-- src/Spice86.Core/CLI/ICommandLineParser.cs | 16 +++ src/Spice86/App.axaml.cs | 9 ++ .../ServiceCollectionExtensions.cs | 42 +++++++ .../Infrastructure/HostStorageProvider.cs | 28 ----- .../Infrastructure/IHostStorageProvider.cs | 14 --- src/Spice86/Interfaces/IPauseStatus.cs | 1 - ...tedMemoryStream.cs => CodeMemoryStream.cs} | 28 ++--- ...inaryDocument.cs => DataMemoryDocument.cs} | 4 +- src/Spice86/Program.cs | 89 ++++++++------- .../ViewModels/DisassemblyViewModel.cs | 14 +-- src/Spice86/ViewModels/MainWindowViewModel.cs | 108 ++---------------- src/Spice86/ViewModels/MemoryViewModel.cs | 4 +- src/Spice86/Views/MainWindow.axaml | 18 +-- 14 files changed, 153 insertions(+), 238 deletions(-) create mode 100644 src/Spice86.Core/CLI/ICommandLineParser.cs create mode 100644 src/Spice86/DependencyInjection/ServiceCollectionExtensions.cs rename src/Spice86/MemoryWrappers/{EmulatedMemoryStream.cs => CodeMemoryStream.cs} (65%) rename src/Spice86/MemoryWrappers/{MemoryBinaryDocument.cs => DataMemoryDocument.cs} (91%) diff --git a/src/Spice86.Core/CLI/CommandLineParser.cs b/src/Spice86.Core/CLI/CommandLineParser.cs index e2cd07853..a01a76fe6 100644 --- a/src/Spice86.Core/CLI/CommandLineParser.cs +++ b/src/Spice86.Core/CLI/CommandLineParser.cs @@ -11,17 +11,11 @@ using System.IO; using System.Reflection; -/// -/// Parses the command line options to create a . -/// -public static class CommandLineParser { - /// - /// Parses the command line into a object. - /// - /// The application command line arguments - /// A object representing the command line arguments - /// When the command line arguments are unrecognized. - public static Configuration ParseCommandLine(string[] args) { + +/// +public class CommandLineParser : ICommandLineParser { + /// + public Configuration ParseCommandLine(string[] args) { ParserResult result = Parser.Default.ParseArguments(args); return result.MapResult(initialConfig => { initialConfig.Exe = ParseExePath(initialConfig.Exe); diff --git a/src/Spice86.Core/CLI/ICommandLineParser.cs b/src/Spice86.Core/CLI/ICommandLineParser.cs new file mode 100644 index 000000000..220c7612c --- /dev/null +++ b/src/Spice86.Core/CLI/ICommandLineParser.cs @@ -0,0 +1,16 @@ +namespace Spice86.Core.CLI; + +using System.Diagnostics; + +/// +/// Parses the command line options to create a . +/// +public interface ICommandLineParser { + /// + /// Parses the command line into a object. + /// + /// The application command line arguments + /// A object representing the command line arguments + /// When the command line arguments are unrecognized. + public Configuration ParseCommandLine(string[] args); +} diff --git a/src/Spice86/App.axaml.cs b/src/Spice86/App.axaml.cs index 45c8d3f4e..7f34d7944 100644 --- a/src/Spice86/App.axaml.cs +++ b/src/Spice86/App.axaml.cs @@ -1,6 +1,7 @@ namespace Spice86; using Avalonia; +using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; /// @@ -12,4 +13,12 @@ internal partial class App : Application { /// Initializes the Spice86 UI. /// public override void Initialize() => AvaloniaXamlLoader.Load(this); + + public override void OnFrameworkInitializationCompleted() { + // If you use CommunityToolkit, line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + BindingPlugins.DataValidators.RemoveAt(0); + + base.OnFrameworkInitializationCompleted(); + } } \ No newline at end of file diff --git a/src/Spice86/DependencyInjection/ServiceCollectionExtensions.cs b/src/Spice86/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..c32e6e61f --- /dev/null +++ b/src/Spice86/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +namespace Spice86.DependencyInjection; + +using Avalonia.Controls; +using Avalonia.Platform.Storage; +using Avalonia.Threading; + +using Microsoft.Extensions.DependencyInjection; + +using Spice86.Core.CLI; +using Spice86.Infrastructure; +using Spice86.Logging; +using Spice86.Shared.Interfaces; + +public static class ServiceCollectionExtensions { + public static void AddConfiguration(this IServiceCollection serviceCollection, string[] args) { + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(serviceProvider => { + ICommandLineParser commandLineParser = serviceProvider.GetRequiredService(); + return commandLineParser.ParseCommandLine(args); + }); + } + + public static void AddLogging(this IServiceCollection serviceCollection) { + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton((serviceProvider) => { + Configuration configuration = serviceProvider.GetRequiredService(); + LoggerService loggerService = new LoggerService(serviceProvider.GetRequiredService()); + Startup.SetLoggingLevel(loggerService, configuration); + return loggerService; + }); + } + + public static void AddGuiInfrastructure(this IServiceCollection serviceCollection, TopLevel mainWindow) { + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton((_) => new UIDispatcher(Dispatcher.UIThread)); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton((_) => mainWindow.StorageProvider); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton((_) => new TextClipboard(mainWindow.Clipboard)); + } +} \ No newline at end of file diff --git a/src/Spice86/Infrastructure/HostStorageProvider.cs b/src/Spice86/Infrastructure/HostStorageProvider.cs index c53a9dc10..f6a028e04 100644 --- a/src/Spice86/Infrastructure/HostStorageProvider.cs +++ b/src/Spice86/Infrastructure/HostStorageProvider.cs @@ -31,11 +31,6 @@ public class HostStorageProvider : IHostStorageProvider { return await _storageProvider.SaveFilePickerAsync(options); } - /// - public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) { - return await _storageProvider.OpenFilePickerAsync(options); - } - /// public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { return await _storageProvider.OpenFolderPickerAsync(options); @@ -92,27 +87,4 @@ public async Task DumpEmulatorStateToFile(Configuration configuration, IProgramE } } } - - public async Task PickExecutableFile(string lastExecutableDirectory) { - FilePickerOpenOptions options = new() { - Title = "Start Executable...", - AllowMultiple = false, - FileTypeFilter = new[] { - new FilePickerFileType("DOS Executables") { - Patterns = new[] {"*.com", "*.exe", "*.EXE", "*.COM"} - }, - new FilePickerFileType("All files") { - Patterns = new[] {"*"} - } - } - }; - IStorageFolder? folder = await TryGetWellKnownFolderAsync(WellKnownFolder.Documents); - options.SuggestedStartLocation = folder; - if (Directory.Exists(lastExecutableDirectory)) { - options.SuggestedStartLocation = await TryGetFolderFromPathAsync(lastExecutableDirectory); - } - - IReadOnlyList files = await OpenFilePickerAsync(options); - return files.FirstOrDefault(); - } } diff --git a/src/Spice86/Infrastructure/IHostStorageProvider.cs b/src/Spice86/Infrastructure/IHostStorageProvider.cs index f204c2c92..2c108249e 100644 --- a/src/Spice86/Infrastructure/IHostStorageProvider.cs +++ b/src/Spice86/Infrastructure/IHostStorageProvider.cs @@ -55,13 +55,6 @@ public interface IHostStorageProvider { /// A list of selected folders. Task> OpenFolderPickerAsync(FolderPickerOpenOptions options); - /// - /// Opens file picker dialog. - /// - /// The file picker configuration. - /// A list of selected files. - Task> OpenFilePickerAsync(FilePickerOpenOptions options); - /// /// Spawns the file pciker to saves a bitmap to a file. /// @@ -76,13 +69,6 @@ public interface IHostStorageProvider { /// The emulated program's executor, to save the emulator dump and other reverse-engineering information. Task DumpEmulatorStateToFile(Configuration configuration, IProgramExecutor programExecutor); - /// - /// Spanws the file picker to pick an executable file, and returns the first file picked by the user. - /// - /// The directory of the last file picked, if any. - /// The operation as an awaitable Task, containing the first picked file, or null. - Task PickExecutableFile(string lastExecutableDirectory); - /// /// Spanws the file picker to save a binary file. /// diff --git a/src/Spice86/Interfaces/IPauseStatus.cs b/src/Spice86/Interfaces/IPauseStatus.cs index 2529f710d..a18828e7d 100644 --- a/src/Spice86/Interfaces/IPauseStatus.cs +++ b/src/Spice86/Interfaces/IPauseStatus.cs @@ -1,6 +1,5 @@ namespace Spice86.Interfaces; -using Spice86.Core.Emulator.InternalDebugger; using System.ComponentModel; public interface IPauseStatus : INotifyPropertyChanged { diff --git a/src/Spice86/MemoryWrappers/EmulatedMemoryStream.cs b/src/Spice86/MemoryWrappers/CodeMemoryStream.cs similarity index 65% rename from src/Spice86/MemoryWrappers/EmulatedMemoryStream.cs rename to src/Spice86/MemoryWrappers/CodeMemoryStream.cs index 257cf38b5..f6fde2620 100644 --- a/src/Spice86/MemoryWrappers/EmulatedMemoryStream.cs +++ b/src/Spice86/MemoryWrappers/CodeMemoryStream.cs @@ -2,16 +2,16 @@ namespace Spice86.MemoryWrappers; using Spice86.Core.Emulator.Memory; -public class EmulatedMemoryStream : Stream { +public class CodeMemoryStream : Stream { private readonly IMemory _memory; private long _length; private readonly bool _canRead; private readonly bool _canWrite; private readonly bool _canSeek; - public EmulatedMemoryStream(IMemory memory) { + public CodeMemoryStream(IMemory memory) { _memory = memory; _length = memory.Length; - _canWrite = true; + _canWrite = false; _canSeek = true; _canRead = true; } @@ -21,16 +21,10 @@ public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) { - int bytesRead = 0; - for (int i = 0; i < buffer.Length; i++) { - if (i + offset > buffer.Length || Position > _memory.Length) { - break; - } - buffer[i + offset] = _memory[(uint)Position]; - bytesRead++; - Position++; - } - return bytesRead; + byte[] ramCopy = _memory.ReadRam((uint)Math.Min(count, _memory.Length - Position), (uint)Position); + ramCopy.CopyTo(buffer, offset); + Position += ramCopy.Length; + return ramCopy.Length; } public override long Seek(long offset, SeekOrigin origin) { @@ -53,13 +47,7 @@ public override void SetLength(long value) { } public override void Write(byte[] buffer, int offset, int count) { - for (int i = 0; i < count; i++) { - if (i + offset > buffer.Length || Position > _memory.Length) { - break; - } - _memory[(uint)Position] = buffer[i + offset]; - Position++; - } + throw new NotSupportedException(); } public override bool CanRead => _canRead; diff --git a/src/Spice86/MemoryWrappers/MemoryBinaryDocument.cs b/src/Spice86/MemoryWrappers/DataMemoryDocument.cs similarity index 91% rename from src/Spice86/MemoryWrappers/MemoryBinaryDocument.cs rename to src/Spice86/MemoryWrappers/DataMemoryDocument.cs index 8d8b4e230..c76a61b17 100644 --- a/src/Spice86/MemoryWrappers/MemoryBinaryDocument.cs +++ b/src/Spice86/MemoryWrappers/DataMemoryDocument.cs @@ -5,12 +5,12 @@ using System; -public class MemoryBinaryDocument : IBinaryDocument { +public class DataMemoryDocument : IBinaryDocument { private readonly IMemory _memory; private readonly uint _startAddress; private readonly uint _endAddress; - public MemoryBinaryDocument(IMemory memory, uint startAddress, uint endAddress) { + public DataMemoryDocument(IMemory memory, uint startAddress, uint endAddress) { IsReadOnly = false; CanInsert = false; CanRemove = false; diff --git a/src/Spice86/Program.cs b/src/Spice86/Program.cs index 8fc001efb..abf73023e 100644 --- a/src/Spice86/Program.cs +++ b/src/Spice86/Program.cs @@ -1,19 +1,20 @@ namespace Spice86; using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; + +using Microsoft.Extensions.DependencyInjection; using Spice86.Core.CLI; using Spice86.Core.Emulator; -using Spice86.Shared.Interfaces; +using Spice86.Core.Emulator.Devices.Timer; +using Spice86.Core.Emulator.Function.Dump; +using Spice86.DependencyInjection; using Spice86.Infrastructure; -using Avalonia.Threading; - -using Microsoft.Extensions.DependencyInjection; - -using Spice86.Logging; +using Spice86.Shared.Interfaces; using Spice86.ViewModels; +using Spice86.Views; /// /// Entry point for Spice86 application. @@ -41,53 +42,53 @@ public class Program { /// The command-line arguments. [STAThread] public static void Main(string[] args) { - Configuration configuration = CommandLineParser.ParseCommandLine(args); - - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - + ServiceCollection serviceCollection = InjectCommonServices(args); + //We need to build the service provider before retrieving the configuration service ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); - ILoggerService loggerService = serviceProvider.GetRequiredService(); - Startup.SetLoggingLevel(loggerService, configuration); - - if (!configuration.HeadlessMode) { - AppBuilder appBuilder = BuildAvaloniaApp(); - ClassicDesktopStyleApplicationLifetime desktop = SetupWithClassicDesktopLifetime(appBuilder, args); - App? app = (App?)appBuilder.Instance; - - if (app is null) { - return; - } - - Views.MainWindow mainWindow = new(); - using var mainWindowViewModel = new MainWindowViewModel(new WindowService(), new AvaloniaKeyScanCodeConverter(), - new ProgramExecutorFactory(configuration, loggerService), - new UIDispatcher(Dispatcher.UIThread), new HostStorageProvider(mainWindow.StorageProvider), - new TextClipboard(mainWindow.Clipboard), new UIDispatcherTimerFactory(), configuration, loggerService); - mainWindow.DataContext = mainWindowViewModel; - desktop.MainWindow = mainWindow; - desktop.Start(args); - } - else { - ProgramExecutor programExecutor = new(configuration, loggerService, null); + Configuration configuration = serviceProvider.GetRequiredService(); + if (configuration.HeadlessMode) { + ProgramExecutor programExecutor = new(configuration, serviceProvider.GetRequiredService(), null); programExecutor.Run(); + } else { + ClassicDesktopStyleApplicationLifetime desktop = CreateDesktopApp(); + MainWindow mainWindow = new(); + serviceCollection.AddGuiInfrastructure(mainWindow); + //We need to rebuild the service provider after adding new services to the collection + using MainWindowViewModel mainWindowViewModel = serviceCollection.BuildServiceProvider().GetRequiredService(); + StartGraphicalUserInterface(desktop, mainWindowViewModel, mainWindow, args); } } - - /// - /// Configures and builds an Avalonia application instance. - /// - /// The built instance. + + private static void StartGraphicalUserInterface(ClassicDesktopStyleApplicationLifetime desktop, MainWindowViewModel mainWindowViewModel, MainWindow mainWindow, string[] args) { + mainWindow.DataContext = mainWindowViewModel; + desktop.MainWindow = mainWindow; + desktop.Start(args); + } + + private static ClassicDesktopStyleApplicationLifetime CreateDesktopApp() { + AppBuilder appBuilder = BuildAvaloniaApp(); + ClassicDesktopStyleApplicationLifetime desktop = SetupWithClassicDesktopLifetime(appBuilder); + return desktop; + } + + private static ServiceCollection InjectCommonServices(string[] args) { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddConfiguration(args); + serviceCollection.AddLogging(); + + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + return serviceCollection; + } + private static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() .LogToTrace() .WithInterFont(); - private static ClassicDesktopStyleApplicationLifetime SetupWithClassicDesktopLifetime( - AppBuilder builder, string[] args) { + private static ClassicDesktopStyleApplicationLifetime SetupWithClassicDesktopLifetime(AppBuilder builder) { var lifetime = new ClassicDesktopStyleApplicationLifetime { - Args = args, ShutdownMode = ShutdownMode.OnMainWindowClose }; builder.SetupWithLifetime(lifetime); diff --git a/src/Spice86/ViewModels/DisassemblyViewModel.cs b/src/Spice86/ViewModels/DisassemblyViewModel.cs index a94fa08d8..4e87c914d 100644 --- a/src/Spice86/ViewModels/DisassemblyViewModel.cs +++ b/src/Spice86/ViewModels/DisassemblyViewModel.cs @@ -144,7 +144,7 @@ private void UpdateDisassembly() { return; } _needToUpdateDisassembly = false; - CodeReader codeReader = CreateCodeReader(_memory, out EmulatedMemoryStream emulatedMemoryStream); + CodeReader codeReader = CreateCodeReader(_memory, out CodeMemoryStream emulatedMemoryStream); Decoder decoder = InitializeDecoder(codeReader, StartAddress.Value); try { DecodeInstructions(_state, _memory, emulatedMemoryStream, decoder, StartAddress.Value); @@ -153,13 +153,13 @@ private void UpdateDisassembly() { } } - private void DecodeInstructions(State state, IMemory memory, EmulatedMemoryStream emulatedMemoryStream, + private void DecodeInstructions(State state, IMemory memory, CodeMemoryStream codeMemoryStream, Decoder decoder, uint startAddress) { int byteOffset = 0; - emulatedMemoryStream.Position = startAddress; + codeMemoryStream.Position = startAddress; var instructions = new List(); while (instructions.Count < NumberOfInstructionsShown) { - long instructionAddress = emulatedMemoryStream.Position; + long instructionAddress = codeMemoryStream.Position; decoder.Decode(out Instruction instruction); CpuInstructionInfo instructionInfo = new() { Instruction = instruction, @@ -194,9 +194,9 @@ private Decoder InitializeDecoder(CodeReader codeReader, uint currentIp) { return decoder; } - private static CodeReader CreateCodeReader(IMemory memory, out EmulatedMemoryStream emulatedMemoryStream) { - emulatedMemoryStream = new EmulatedMemoryStream(memory); - CodeReader codeReader = new StreamCodeReader(emulatedMemoryStream); + private static CodeReader CreateCodeReader(IMemory memory, out CodeMemoryStream codeMemoryStream) { + codeMemoryStream = new CodeMemoryStream(memory); + CodeReader codeReader = new StreamCodeReader(codeMemoryStream); return codeReader; } } \ No newline at end of file diff --git a/src/Spice86/ViewModels/MainWindowViewModel.cs b/src/Spice86/ViewModels/MainWindowViewModel.cs index e055a95eb..36aa12173 100644 --- a/src/Spice86/ViewModels/MainWindowViewModel.cs +++ b/src/Spice86/ViewModels/MainWindowViewModel.cs @@ -1,12 +1,10 @@ namespace Spice86.ViewModels; using Avalonia; -using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media.Imaging; using Avalonia.Platform; -using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -104,11 +102,6 @@ internal void OnKeyUp(KeyEventArgs e) { _avaloniaKeyScanCodeConverter.GetKeyReleasedScancode((Key)e.Key)))); } - [RelayCommand] - public void EndProgram() { - ClearDialogCommand.Execute(null); - } - [RelayCommand] public async Task SaveBitmap() { if (Bitmap is not null) { @@ -172,11 +165,6 @@ public bool IsPaused { } } - [ObservableProperty] - [NotifyCanExecuteChangedFor(nameof(StartMostRecentlyUsedCommand))] - private AvaloniaList _mostRecentlyUsed = new(); - - public int Width { get; private set; } public int Height { get; private set; } @@ -190,16 +178,16 @@ public bool IsPaused { [NotifyCanExecuteChangedFor(nameof(PauseCommand))] [NotifyCanExecuteChangedFor(nameof(PlayCommand))] [NotifyCanExecuteChangedFor(nameof(DumpEmulatorStateToFileCommand))] - private bool _isMachineRunning; + private bool _isEmulatorRunning; - [RelayCommand(CanExecute = nameof(IsMachineRunning))] + [RelayCommand(CanExecute = nameof(IsEmulatorRunning))] public async Task DumpEmulatorStateToFile() { if (ProgramExecutor is not null) { await _hostStorageProvider.DumpEmulatorStateToFile(Configuration, ProgramExecutor); } } - [RelayCommand(CanExecute = nameof(IsMachineRunning))] + [RelayCommand(CanExecute = nameof(IsEmulatorRunning))] public void Pause() { if (ProgramExecutor is null) { return; @@ -207,7 +195,7 @@ public void Pause() { IsPaused = ProgramExecutor.IsPaused = true; } - [RelayCommand(CanExecute = nameof(IsMachineRunning))] + [RelayCommand(CanExecute = nameof(IsEmulatorRunning))] public void Play() { if (ProgramExecutor is null) { return; @@ -221,55 +209,6 @@ public void Play() { [ObservableProperty] private string? _mainTitle; - [RelayCommand(CanExecute = nameof(CanStartMostRecentlyUsed))] - public async Task StartMostRecentlyUsed(object? parameter) { - int index = Convert.ToInt32(parameter); - if (MostRecentlyUsed.Count > index) { - await StartNewExecutable(MostRecentlyUsed[index].FullName); - } - } - - private bool CanStartMostRecentlyUsed() => MostRecentlyUsed.Count > 0; - - [RelayCommand] - public async Task DebugExecutable() { - _closeAppOnEmulatorExit = false; - await StartNewExecutable(); - Pause(); - } - - [RelayCommand] - public async Task StartExecutable(object? filePath) { - _closeAppOnEmulatorExit = false; - await StartNewExecutable(filePath as string); - } - - private async Task StartNewExecutable(string? filePath = null) { - if(!string.IsNullOrWhiteSpace(filePath) && - File.Exists(filePath)) { - await RestartEmulatorWithNewProgram(filePath); - } - else if (_hostStorageProvider.CanOpen) { - IStorageFile? file = await _hostStorageProvider.PickExecutableFile(_lastExecutableDirectory); - if (file is not null) { - filePath = file.Path.LocalPath; - await RestartEmulatorWithNewProgram(filePath); - } - } - } - - private async Task RestartEmulatorWithNewProgram(string filePath) { - Configuration.Exe = filePath; - Configuration.ExeArgs = ""; - Configuration.CDrive = Path.GetDirectoryName(Configuration.Exe); - Configuration.UseCodeOverride = false; - Play(); - await _uiDispatcher.InvokeAsync(DisposeEmulator, DispatcherPriority.MaxValue); - IsMachineRunning = false; - _closeAppOnEmulatorExit = false; - RunEmulator(); - } - private double _timeMultiplier = 1; public double? TimeMultiplier { @@ -282,7 +221,7 @@ public double? TimeMultiplier { } } - [RelayCommand(CanExecute = nameof(IsMachineRunning))] + [RelayCommand(CanExecute = nameof(IsEmulatorRunning))] public void ShowPerformance() => IsPerformanceVisible = !IsPerformanceVisible; [RelayCommand] @@ -324,34 +263,11 @@ public void OnMainWindowInitialized(Action uiUpdateMethod) { } } - [ObservableProperty] - private string? _firstProgramName = ""; - - [ObservableProperty] - private string? _secondProgramName = ""; - - [ObservableProperty] - private string? _thirdProgramName = ""; - - private void AddOrReplaceMostRecentlyUsed(string filePath) { - if (MostRecentlyUsed.Any(x => x.FullName == filePath)) { - return; - } - MostRecentlyUsed.Insert(0,new FileInfo(filePath)); - if (MostRecentlyUsed.Count > 3) { - MostRecentlyUsed.RemoveAt(3); - } - FirstProgramName = MostRecentlyUsed.ElementAtOrDefault(0)?.Name; - SecondProgramName = MostRecentlyUsed.ElementAtOrDefault(1)?.Name; - ThirdProgramName = MostRecentlyUsed.ElementAtOrDefault(2)?.Name; - } - private bool RunEmulator() { if (string.IsNullOrWhiteSpace(Configuration.Exe) || string.IsNullOrWhiteSpace(Configuration.CDrive)) { return false; } - AddOrReplaceMostRecentlyUsed(Configuration.Exe); _lastExecutableDirectory = Configuration.CDrive; StatusMessage = "Emulator starting..."; AsmOverrideStatus = Configuration switch { @@ -362,7 +278,7 @@ private bool RunEmulator() { }; SetLogLevel(Configuration.SilencedLogs ? "Silent" : _loggerService.LogLevelSwitch.MinimumLevel.ToString()); SetMainTitle(); - RunMachine(); + StartEmulatorThread(); return true; } @@ -428,7 +344,7 @@ private void Dispose(bool disposing) { }, DispatcherPriority.MaxValue); _drawingSemaphoreSlim?.Dispose(); PlayCommand.Execute(null); - IsMachineRunning = false; + IsEmulatorRunning = false; DisposeEmulator(); if (_emulatorThread?.IsAlive == true) { _emulatorThread.Join(); @@ -475,8 +391,8 @@ private void SetLogLevel(string logLevel) { } } - private void RunMachine() { - _emulatorThread = new Thread(MachineThread) { + private void StartEmulatorThread() { + _emulatorThread = new Thread(EmulatorThread) { Name = "Emulator" }; _emulatorThread.Start(); @@ -487,7 +403,7 @@ private void OnEmulatorErrorOccured(Exception e) => _uiDispatcher.Post(() => { ShowError(e); }); - private void MachineThread() { + private void EmulatorThread() { try { try { StartProgramExecutor(); @@ -498,7 +414,7 @@ private void MachineThread() { OnEmulatorErrorOccured(e); } } finally { - _uiDispatcher.Post(() => IsMachineRunning = false); + _uiDispatcher.Post(() => IsEmulatorRunning = false); _uiDispatcher.Post(() => StatusMessage = "Emulator: stopped."); _uiDispatcher.Post(() => AsmOverrideStatus = ""); } @@ -526,7 +442,7 @@ private void StartProgramExecutor() { PerformanceViewModel = new(_uiDispatcherTimerFactory, ProgramExecutor, new PerformanceMeasurer(), this); _windowService.CloseDebugWindow(); TimeMultiplier = Configuration.TimeMultiplier; - _uiDispatcher.Post(() => IsMachineRunning = true); + _uiDispatcher.Post(() => IsEmulatorRunning = true); _uiDispatcher.Post(() => StatusMessage = "Emulator started."); ProgramExecutor?.Run(); if (_closeAppOnEmulatorExit) { diff --git a/src/Spice86/ViewModels/MemoryViewModel.cs b/src/Spice86/ViewModels/MemoryViewModel.cs index 62aaedf56..5561507ef 100644 --- a/src/Spice86/ViewModels/MemoryViewModel.cs +++ b/src/Spice86/ViewModels/MemoryViewModel.cs @@ -23,7 +23,7 @@ public partial class MemoryViewModel : ViewModelBaseWithErrorDialog, IInternalDe private bool _needToUpdateBinaryDocument; [ObservableProperty] - private MemoryBinaryDocument? _memoryBinaryDocument; + private DataMemoryDocument? _memoryBinaryDocument; public bool NeedsToVisitEmulator => _memory is null; @@ -142,7 +142,7 @@ private void UpdateBinaryDocument() { if (_memory is null || StartAddress is null || EndAddress is null) { return; } - MemoryBinaryDocument = new MemoryBinaryDocument(_memory, StartAddress.Value, EndAddress.Value); + MemoryBinaryDocument = new DataMemoryDocument(_memory, StartAddress.Value, EndAddress.Value); MemoryBinaryDocument.MemoryReadInvalidOperation -= OnMemoryReadInvalidOperation; MemoryBinaryDocument.MemoryReadInvalidOperation += OnMemoryReadInvalidOperation; } diff --git a/src/Spice86/Views/MainWindow.axaml b/src/Spice86/Views/MainWindow.axaml index e12180cd6..9d0c39f16 100644 --- a/src/Spice86/Views/MainWindow.axaml +++ b/src/Spice86/Views/MainWindow.axaml @@ -23,18 +23,10 @@ - - - - - - - - - + - - + + @@ -44,9 +36,9 @@ - + - +