Skip to content

Commit

Permalink
Add settings files on the app builder
Browse files Browse the repository at this point in the history
  • Loading branch information
Mattias1 committed Sep 4, 2023
1 parent b4b26d7 commit 9976038
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 82 deletions.
27 changes: 25 additions & 2 deletions AvaloniaExtensions/AppBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,42 @@ public static class AppBuilderExtensions {

public static AppBuilder Init() => AppBuilder.Configure<Application>().UsePlatformDetect();

/// <summary>
/// Add a settings file. It'll save the settings file when closing the app.
/// </summary>
/// <param name="builder"></param>
/// <param name="path">The location of the settings file. You can use './filename.json' to save it in the apps dir.</param>
/// <typeparam name="T">The type of the settings class.</typeparam>
/// <returns></returns>
public static AppBuilder WithSettingsFile<T>(this AppBuilder builder, string path) where T : class, new() {
SettingsFiles.Get.AddSettingsFile(path, () => new T());
return builder;
}

/// <summary>
/// Add a settings file. It'll save the settings file when closing the app.
/// </summary>
/// <param name="builder"></param>
/// <param name="path">The location of the settings file. You can use './filename.json' to save it in the apps dir.</param>
/// <param name="constructorLambda">A lambda function to construct the settings object if it can't be loaded.</param>
/// <typeparam name="T">The type of the settings class.</typeparam>
/// <returns></returns>
public static AppBuilder WithSettingsFile<T>(this AppBuilder builder, string path, Func<T> constructorLambda) where T : class {
SettingsFiles.Get.AddSettingsFile(path, constructorLambda);
return builder;
}

public static Application StartDesktopApp(this AppBuilder builder, string windowTitle, Func<ViewBase> contentFunc) {
return builder.StartDesktopApp(() => ExtendedWindow.Init(windowTitle, contentFunc()));
}
public static Application StartDesktopApp(this AppBuilder builder, string windowTitle, Func<ViewBase> contentFunc,
Size size) {
return builder.StartDesktopApp(() => ExtendedWindow.Init(windowTitle, contentFunc()).WithSize(size));
}

public static Application StartDesktopApp(this AppBuilder builder, string windowTitle, Func<ViewBase> contentFunc,
Size size, Size minSize) {
return builder.StartDesktopApp(() => ExtendedWindow.Init(windowTitle, contentFunc()).WithSize(size, minSize));
}

public static Application StartDesktopApp(this AppBuilder builder, Func<Window> windowFunc) {
// Note that despite it looks like this uses a builder pattern, the order of method- and constructor-calls matter
var lifetime = new ClassicDesktopStyleApplicationLifetime() {
Expand Down
6 changes: 3 additions & 3 deletions AvaloniaExtensions/AvaloniaExtensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.3" />
<PackageReference Include="Avalonia" Version="11.0.4" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.4" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.4" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion AvaloniaExtensions/CanvasComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public static T RegisterOnResizeAction<T>(T control, Action resizeAction) where

public void SwitchToComponent<T>() => FindWindow().SwitchToComponent<T>();

public T GetSettings<T>() where T : class => FindWindow().GetSettings<T>();
public T GetSettings<T>() where T : class => SettingsFiles.Get.GetSettings<T>();

public void Quit() {
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktopApp) {
Expand Down
75 changes: 6 additions & 69 deletions AvaloniaExtensions/ExtendedWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
using Avalonia.Markup.Declarative;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;

namespace AvaloniaExtensions;

Expand All @@ -15,13 +13,13 @@ public sealed class ExtendedWindow : Window {

private readonly Dictionary<Type, ViewBase> _components;
private readonly Dictionary<Type, Func<ViewBase>> _lazyComponents;
private readonly Dictionary<Type, SettingsFile> _settingsFiles;

public SettingsFiles SettingsFiles => SettingsFiles.Get;

private ExtendedWindow() {
_windowObject = this;
_components = new Dictionary<Type, ViewBase>();
_lazyComponents = new Dictionary<Type, Func<ViewBase>>();
_settingsFiles = new Dictionary<Type, SettingsFile>();
}

// --- Miscellaneous functions ---
Expand Down Expand Up @@ -74,7 +72,7 @@ public ExtendedWindow AddLazyComponent<T>(Func<T> componentFunc) where T : ViewB

// --- Settings files ---
protected override void OnClosing(WindowClosingEventArgs e) {
SaveSettings();
SettingsFiles.SaveSettings();
base.OnClosing(e);
}

Expand All @@ -85,7 +83,8 @@ protected override void OnClosing(WindowClosingEventArgs e) {
/// <typeparam name="T">The type of the settings class.</typeparam>
/// <returns></returns>
public ExtendedWindow WithSettingsFile<T>(string path) where T : class, new() {
return WithSettingsFile(path, () => new T());
SettingsFiles.AddSettingsFile(path, () => new T());
return this;
}

/// <summary>
Expand All @@ -96,70 +95,10 @@ protected override void OnClosing(WindowClosingEventArgs e) {
/// <typeparam name="T">The type of the settings class.</typeparam>
/// <returns></returns>
public ExtendedWindow WithSettingsFile<T>(string path, Func<T> constructorLambda) where T : class {
var settings = LoadOrCreateSettings(path, constructorLambda);
_settingsFiles.Add(typeof(T), new SettingsFile(path, settings));
SettingsFiles.AddSettingsFile(path, constructorLambda);
return this;
}

private T LoadOrCreateSettings<T>(string path, Func<T> constructorLambda) where T : class {
try {
using var stream = File.OpenRead(CompletePath(path));
var result = JsonSerializer.Deserialize<T>(stream);
return result ?? constructorLambda();
} catch (Exception e) {
Console.Error.WriteLine("An avalonia extensions app encountered an error while loading a settings file." +
$"Settings type: '{typeof(T)}', error: '{e.Message}'.");
return constructorLambda();
}
}

public bool SaveSettings() {
bool allSavedSuccessfully = true;
foreach (var (type, settingsFile) in _settingsFiles) {
try {
using var stream = File.Create(CompletePath(settingsFile.Path));
JsonSerializer.Serialize(stream, settingsFile.Settings);
} catch (Exception e) {
allSavedSuccessfully = false;
Console.Error.WriteLine("An avalonia extensions app encountered an error while saving a settings file." +
$"Settings type: '{type}', error: '{e.Message}'.");
}
}
return allSavedSuccessfully;
}

private string CompletePath(string path) {
if (string.IsNullOrWhiteSpace(path)) {
path = "./settings.json";
}
if (path.Substring(0, 2) == "./") {
path = AssetExtensions.StartupPath + path.Substring(1);
}
return path;
}

public void ResetSettings<T>() where T : class, new() => OverwriteSettings(new T());
public void ResetSettings<T>(Func<T> constructorLambda) where T : class => OverwriteSettings(constructorLambda());

public void OverwriteSettings<T>(T settings) where T : class {
if (!_settingsFiles.TryGetValue(typeof(T), out SettingsFile? originalSettingsFile)) {
throw new InvalidOperationException($"Cannot find settings with type {typeof(T)}. "
+ $"You can add it with the '{nameof(WithSettingsFile)}' method.");
}
_settingsFiles[typeof(T)] = originalSettingsFile with { Settings = settings };
}

public T GetSettings<T>() where T : class {
if (!_settingsFiles.TryGetValue(typeof(T), out SettingsFile? settingsFile)) {
throw new InvalidOperationException($"Cannot find settings with type {typeof(T)}. "
+ $"You can add it with the '{nameof(WithSettingsFile)}' method.");
}
if (settingsFile.Settings is not T settings) {
throw new InvalidOperationException($"Cannot cast settings with type {typeof(T)} to its type, weird.");
}
return settings;
}

// --- Initialisation ---
public static ExtendedWindow Init<T>(string windowTitle) where T : ViewBase, new() {
return Init(windowTitle, () => new T());
Expand All @@ -172,6 +111,4 @@ public static ExtendedWindow Init<T>(string windowTitle, Func<T> initialComponen
window.Title = windowTitle;
return window;
}

private record SettingsFile(string Path, object Settings);
}
93 changes: 93 additions & 0 deletions AvaloniaExtensions/SettingsFiles.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;

namespace AvaloniaExtensions;

public sealed class SettingsFiles {
public static SettingsFiles Get { get; } = new SettingsFiles();

private readonly Dictionary<Type, SettingsFile> _settingsFiles = new();

/// <summary>
/// Add a settings file. It'll save the settings file when closing the app.
/// </summary>
/// <param name="path">The location of the settings file. You can use './filename.json' to save it in the apps dir.</param>
/// <typeparam name="T">The type of the settings class.</typeparam>
/// <returns></returns>
public void AddSettingsFile<T>(string path) where T : class, new() => AddSettingsFile(path, () => new T());

/// <summary>
/// Add a settings file. It'll save the settings file when closing the app.
/// </summary>
/// <param name="path">The location of the settings file. You can use './filename.json' to save it in the apps dir.</param>
/// <param name="constructorLambda">A lambda function to construct the settings object if it can't be loaded.</param>
/// <typeparam name="T">The type of the settings class.</typeparam>
/// <returns></returns>
public void AddSettingsFile<T>(string path, Func<T> constructorLambda) where T : class {
var settings = LoadOrCreateSettings(path, constructorLambda);
_settingsFiles.Add(typeof(T), new SettingsFile(path, settings));
}

private T LoadOrCreateSettings<T>(string path, Func<T> constructorLambda) where T : class {
try {
using var stream = File.OpenRead(CompletePath(path));
var result = JsonSerializer.Deserialize<T>(stream);
return result ?? constructorLambda();
} catch (Exception e) {
Console.Error.WriteLine("An avalonia extensions app encountered an error while loading a settings file." +
$"Settings type: '{typeof(T)}', error: '{e.Message}'.");
return constructorLambda();
}
}

public bool SaveSettings() {
bool allSavedSuccessfully = true;
foreach (var (type, settingsFile) in _settingsFiles) {
try {
using var stream = File.Create(CompletePath(settingsFile.Path));
JsonSerializer.Serialize(stream, settingsFile.Settings);
} catch (Exception e) {
allSavedSuccessfully = false;
Console.Error.WriteLine("An avalonia extensions app encountered an error while saving a settings file." +
$"Settings type: '{type}', error: '{e.Message}'.");
}
}
return allSavedSuccessfully;
}

private string CompletePath(string path) {
if (string.IsNullOrWhiteSpace(path)) {
path = "./settings.json";
}
if (path.Substring(0, 2) == "./") {
path = AssetExtensions.StartupPath + path.Substring(1);
}
return path;
}

public void ResetSettings<T>() where T : class, new() => OverwriteSettings(new T());
public void ResetSettings<T>(Func<T> constructorLambda) where T : class => OverwriteSettings(constructorLambda());

public void OverwriteSettings<T>(T settings) where T : class {
if (!_settingsFiles.TryGetValue(typeof(T), out SettingsFile? originalSettingsFile)) {
throw new InvalidOperationException($"Cannot find settings with type {typeof(T)}. "
+ "You can add it with the 'WithSettingsFile' or 'AddSettingsFile' method.");
}
_settingsFiles[typeof(T)] = originalSettingsFile with { Settings = settings };
}

public T GetSettings<T>() where T : class {
if (!_settingsFiles.TryGetValue(typeof(T), out SettingsFile? settingsFile)) {
throw new InvalidOperationException($"Cannot find settings with type {typeof(T)}. "
+ "You can add it with the 'WithSettingsFile' or 'AddSettingsFile' method.");
}
if (settingsFile.Settings is not T settings) {
throw new InvalidOperationException($"Cannot cast settings with type {typeof(T)} to its type, weird.");
}
return settings;
}

private record SettingsFile(string Path, object Settings);
}
4 changes: 2 additions & 2 deletions ExampleApp/ExampleApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.3" />
<PackageReference Include="Avalonia" Version="11.0.4" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
</ItemGroup>

Expand Down
9 changes: 5 additions & 4 deletions ExampleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
using Avalonia.Markup.Declarative;
using ExampleApp;

AppBuilderExtensions.Init().StartDesktopApp(() => ExtendedWindow.Init<MainComponent>("Example app")
.AddLazyComponent<SettingsComponent>()
AppBuilderExtensions.Init()
.WithSettingsFile<SettingsComponent.ExampleSettings>("./example-settings.json")
.WithSize(size: new Size(800, 500), minSize: new Size(600, 350))
.Icon(AssetExtensions.LoadWindowIcon("assets/smiley.png")));
.StartDesktopApp(() => ExtendedWindow.Init<MainComponent>("Example app")
.AddLazyComponent<SettingsComponent>()
.WithSize(size: new Size(800, 500), minSize: new Size(600, 350))
.Icon(AssetExtensions.LoadWindowIcon("assets/smiley.png")));
2 changes: 1 addition & 1 deletion ExampleApp/SettingsComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ private void OnSaveClick(RoutedEventArgs e) {
}

private void OnResetSettingsClick(RoutedEventArgs e) {
FindWindow().ResetSettings<ExampleSettings>();
SettingsFiles.Get.ResetSettings<ExampleSettings>();
_settings = null;
LoadSettings();
}
Expand Down

0 comments on commit 9976038

Please sign in to comment.