Skip to content

Commit

Permalink
Refactoring: UI TopLevel services and MainWindowViewModel injection (#…
Browse files Browse the repository at this point in the history
…744)

* fix: Don't trigger memory read breakpoints (disasm)

... and don't support writes, as it is unused.

Signed-off-by: Maximilien Noal <[email protected]>

* removed unused using

Signed-off-by: Maximilien Noal <[email protected]>

* refactor: UI uses DI for most services

Signed-off-by: Maximilien Noal <[email protected]>

* fix: Double validation errors (AvaloniaUI + CI)

Signed-off-by: Maximilien Noal <[email protected]>

* refactor: MainWindowViewModel is an injected service now

Signed-off-by: Maximilien Noal <[email protected]>

* refactor: removed commands about starting a new program

Signed-off-by: Maximilien Noal <[email protected]>

* refactor: CommandLineParser is an injected service

Signed-off-by: Maximilien Noal <[email protected]>

---------

Signed-off-by: Maximilien Noal <[email protected]>
  • Loading branch information
maximilien-noal authored Jul 7, 2024
1 parent 9d01f58 commit 64a7a7f
Show file tree
Hide file tree
Showing 14 changed files with 153 additions and 238 deletions.
16 changes: 5 additions & 11 deletions src/Spice86.Core/CLI/CommandLineParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,11 @@
using System.IO;
using System.Reflection;

/// <summary>
/// Parses the command line options to create a <see cref="Configuration"/>.
/// </summary>
public static class CommandLineParser {
/// <summary>
/// Parses the command line into a <see cref="Configuration"/> object.
/// </summary>
/// <param name="args">The application command line arguments</param>
/// <returns>A <see cref="Configuration"/> object representing the command line arguments</returns>
/// <exception cref="UnreachableException">When the command line arguments are unrecognized.</exception>
public static Configuration ParseCommandLine(string[] args) {

/// <inheritdoc cref="ICommandLineParser" />
public class CommandLineParser : ICommandLineParser {
/// <inheritdoc />
public Configuration ParseCommandLine(string[] args) {
ParserResult<Configuration> result = Parser.Default.ParseArguments<Configuration>(args);
return result.MapResult(initialConfig => {
initialConfig.Exe = ParseExePath(initialConfig.Exe);
Expand Down
16 changes: 16 additions & 0 deletions src/Spice86.Core/CLI/ICommandLineParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Spice86.Core.CLI;

using System.Diagnostics;

/// <summary>
/// Parses the command line options to create a <see cref="Configuration"/>.
/// </summary>
public interface ICommandLineParser {
/// <summary>
/// Parses the command line into a <see cref="Configuration"/> object.
/// </summary>
/// <param name="args">The application command line arguments</param>
/// <returns>A <see cref="Configuration"/> object representing the command line arguments</returns>
/// <exception cref="UnreachableException">When the command line arguments are unrecognized.</exception>
public Configuration ParseCommandLine(string[] args);
}
9 changes: 9 additions & 0 deletions src/Spice86/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Spice86;

using Avalonia;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;

/// <summary>
Expand All @@ -12,4 +13,12 @@ internal partial class App : Application {
/// Initializes the Spice86 UI.
/// </summary>
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();
}
}
42 changes: 42 additions & 0 deletions src/Spice86/DependencyInjection/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<ICommandLineParser, CommandLineParser>();
serviceCollection.AddSingleton<Configuration>(serviceProvider => {
ICommandLineParser commandLineParser = serviceProvider.GetRequiredService<ICommandLineParser>();
return commandLineParser.ParseCommandLine(args);
});
}

public static void AddLogging(this IServiceCollection serviceCollection) {
serviceCollection.AddSingleton<ILoggerPropertyBag, LoggerPropertyBag>();
serviceCollection.AddSingleton<ILoggerService, LoggerService>((serviceProvider) => {
Configuration configuration = serviceProvider.GetRequiredService<Configuration>();
LoggerService loggerService = new LoggerService(serviceProvider.GetRequiredService<ILoggerPropertyBag>());
Startup.SetLoggingLevel(loggerService, configuration);
return loggerService;
});
}

public static void AddGuiInfrastructure(this IServiceCollection serviceCollection, TopLevel mainWindow) {
serviceCollection.AddSingleton<IAvaloniaKeyScanCodeConverter, AvaloniaKeyScanCodeConverter>();
serviceCollection.AddSingleton<IWindowService, WindowService>();
serviceCollection.AddSingleton<IUIDispatcher, UIDispatcher>((_) => new UIDispatcher(Dispatcher.UIThread));
serviceCollection.AddSingleton<IUIDispatcherTimerFactory, UIDispatcherTimerFactory>();
serviceCollection.AddSingleton<IStorageProvider>((_) => mainWindow.StorageProvider);
serviceCollection.AddSingleton<IHostStorageProvider, HostStorageProvider>();
serviceCollection.AddSingleton<ITextClipboard>((_) => new TextClipboard(mainWindow.Clipboard));
}
}
28 changes: 0 additions & 28 deletions src/Spice86/Infrastructure/HostStorageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ public class HostStorageProvider : IHostStorageProvider {
return await _storageProvider.SaveFilePickerAsync(options);
}

/// <inheritdoc />
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) {
return await _storageProvider.OpenFilePickerAsync(options);
}

/// <inheritdoc />
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options) {
return await _storageProvider.OpenFolderPickerAsync(options);
Expand Down Expand Up @@ -92,27 +87,4 @@ public async Task DumpEmulatorStateToFile(Configuration configuration, IProgramE
}
}
}

public async Task<IStorageFile?> 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<IStorageFile> files = await OpenFilePickerAsync(options);
return files.FirstOrDefault();
}
}
14 changes: 0 additions & 14 deletions src/Spice86/Infrastructure/IHostStorageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,6 @@ public interface IHostStorageProvider {
/// <returns>A list of selected folders.</returns>
Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options);

/// <summary>
/// Opens file picker dialog.
/// </summary>
/// <param name="options">The file picker configuration.</param>
/// <returns>A list of selected files.</returns>
Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options);

/// <summary>
/// Spawns the file pciker to saves a bitmap to a file.
/// </summary>
Expand All @@ -76,13 +69,6 @@ public interface IHostStorageProvider {
/// <param name="programExecutor">The emulated program's executor, to save the emulator dump and other reverse-engineering information.</param>
Task DumpEmulatorStateToFile(Configuration configuration, IProgramExecutor programExecutor);

/// <summary>
/// Spanws the file picker to pick an executable file, and returns the first file picked by the user.
/// </summary>
/// <param name="lastExecutableDirectory">The directory of the last file picked, if any.</param>
/// <returns>The operation as an awaitable Task, containing the first picked file, or <c>null</c>.</returns>
Task<IStorageFile?> PickExecutableFile(string lastExecutableDirectory);

/// <summary>
/// Spanws the file picker to save a binary file.
/// </summary>
Expand Down
1 change: 0 additions & 1 deletion src/Spice86/Interfaces/IPauseStatus.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
namespace Spice86.Interfaces;

using Spice86.Core.Emulator.InternalDebugger;
using System.ComponentModel;

public interface IPauseStatus : INotifyPropertyChanged {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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) {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
89 changes: 45 additions & 44 deletions src/Spice86/Program.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Entry point for Spice86 application.
Expand Down Expand Up @@ -41,53 +42,53 @@ public class Program {
/// <param name="args">The command-line arguments.</param>
[STAThread]
public static void Main(string[] args) {
Configuration configuration = CommandLineParser.ParseCommandLine(args);

var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<ILoggerPropertyBag, LoggerPropertyBag>();
serviceCollection.AddSingleton<ILoggerService, LoggerService>();

ServiceCollection serviceCollection = InjectCommonServices(args);
//We need to build the service provider before retrieving the configuration service
ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
ILoggerService loggerService = serviceProvider.GetRequiredService<ILoggerService>();
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<Configuration>();
if (configuration.HeadlessMode) {
ProgramExecutor programExecutor = new(configuration, serviceProvider.GetRequiredService<ILoggerService>(), 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<MainWindowViewModel>();
StartGraphicalUserInterface(desktop, mainWindowViewModel, mainWindow, args);
}
}

/// <summary>
/// Configures and builds an Avalonia application instance.
/// </summary>
/// <returns>The built <see cref="AppBuilder"/> instance.</returns>

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<IProgramExecutorFactory, ProgramExecutorFactory>();
serviceCollection.AddScoped<MainWindowViewModel>();
return serviceCollection;
}

private static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>()
.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);
Expand Down
Loading

0 comments on commit 64a7a7f

Please sign in to comment.