From 275b51924b495df6029fe539e766acbd9f0c252e Mon Sep 17 00:00:00 2001 From: Isaac Daly Date: Tue, 12 Nov 2024 22:50:22 +1100 Subject: [PATCH] Load XAML in YAML (#1075) Add support for loading XAML styles via YAML. --- docs/configure/core/styling.md | 24 ++- .../YamlLoader/YamlLoader_LoadStylesTests.cs | 170 ++++++++++++++++++ src/Whim.Yaml/ErrorWindow.xaml.cs | 36 +++- src/Whim.Yaml/YamlLoader.cs | 76 +++++++- src/Whim.Yaml/schema.json | 13 ++ 5 files changed, 305 insertions(+), 14 deletions(-) create mode 100644 src/Whim.Yaml.Tests/YamlLoader/YamlLoader_LoadStylesTests.cs diff --git a/docs/configure/core/styling.md b/docs/configure/core/styling.md index 957d7a326..94b672e3b 100644 --- a/docs/configure/core/styling.md +++ b/docs/configure/core/styling.md @@ -1,5 +1,19 @@ # Styling +## XAML Styling + +XAML resources can be loaded via the `styles` key in the YAML/JSON configuration. Each path should be a path to a XAML file. Paths must be absolute (e.g. `C:\Users\user\.whim\resources\styles.xaml`) or relative to the `.whim` directory (e.g. `resources\styles.xaml`). + +For example: + +```yaml +styles: + user_dictionaries: + - resources/styles.xaml +``` + +[!INCLUDE [Styling](../../_includes/core/styling.md)] + ## Backdrops Different Whim windows can support custom backdrops. They will generally be associated with a `backdrop` key in the YAML/JSON configuration. The following backdrops are available: @@ -16,13 +30,15 @@ Different Whim windows can support custom backdrops. They will generally be asso | `mica` | An opaque, dynamic material that incorpoates theme and the desktop wallpaper. Mica has better performance than Acrylic. | [Mica material](https://learn.microsoft.com/en-us/windows/apps/design/style/mica) | | `mica_alt` | A variant of Mica with stronger tinting of the user's background color. | [Mica alt material](https://learn.microsoft.com/en-us/windows/apps/design/style/mica) | -### Configuration +### Backdrops Configuration | Property | Description | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | The type of backdrop to use. | | `always_show_backdrop` | By default, WinUI will disable the backdrop when the window loses focus. Whim overrides this setting. Set this to false to disable the backdrop when the window loses focus. | -## XAML Styling - -Loading XAML styles via YAML/JSON is being tracked in [this GitHub issue](https://github.com/dalyIsaac/Whim/issues/1064). In the meantime, it is available [via C# scripting](../../script/core/styling.md). +```yaml +backdrop: + type: acrylic + always_show_backdrop: true +``` diff --git a/src/Whim.Yaml.Tests/YamlLoader/YamlLoader_LoadStylesTests.cs b/src/Whim.Yaml.Tests/YamlLoader/YamlLoader_LoadStylesTests.cs new file mode 100644 index 000000000..5162c0a22 --- /dev/null +++ b/src/Whim.Yaml.Tests/YamlLoader/YamlLoader_LoadStylesTests.cs @@ -0,0 +1,170 @@ +using NSubstitute; +using Whim.TestUtils; +using Xunit; + +namespace Whim.Yaml.Tests; + +public class YamlLoader_LoadStylesTests +{ + public static TheoryData ValidStylesConfig => + new() + { + { + """ + styles: + user_dictionaries: + - "path/to/dict1.xaml" + - "path/to/dict2.xaml" + """, + true + }, + { + """ + { + "styles": { + "user_dictionaries": [ + "path/to/dict1.xaml", + "path/to/dict2.xaml" + ] + } + } + """, + false + }, + }; + + [Theory] + [MemberAutoSubstituteData(nameof(ValidStylesConfig))] + public void Load_ValidStyles_AddsUserDictionaries(string config, bool isYaml, IContext ctx) + { + // Given + YamlLoaderTestUtils.SetupFileConfig(ctx, config, isYaml); + ctx.FileManager.FileExists(Arg.Is(p => p.StartsWith("path"))).Returns(true); + + // When + bool result = YamlLoader.Load(ctx, showErrorWindow: false); + + // Then + Assert.True(result); + ctx.ResourceManager.Received(2).AddUserDictionary(Arg.Any()); + } + + [Theory] + [MemberAutoSubstituteData(nameof(ValidStylesConfig))] + public void Load_FallbackStyles_AddsUserDictionaries(string config, bool isYaml, IContext ctx) + { + // Given + YamlLoaderTestUtils.SetupFileConfig(ctx, config, isYaml); + ctx.FileManager.WhimDir.Returns("C:\\Users\\username\\.whim"); + ctx.FileManager.FileExists(Arg.Is(p => p.StartsWith("path"))).Returns(false); + ctx.FileManager.FileExists(Arg.Is(p => p.StartsWith("C:\\Users\\username\\.whim"))).Returns(true); + + // When + bool result = YamlLoader.Load(ctx, showErrorWindow: false); + + // Then + Assert.True(result); + ctx.ResourceManager.Received(2).AddUserDictionary(Arg.Any()); + } + + public static TheoryData NoStylesConfig => + new() + { + { + """ + styles: + user_dictionaries: [] + """, + true + }, + { + """ + { + "styles": { + "user_dictionaries": [] + } + } + """, + false + }, + { + """ + styles: {} + """, + true + }, + { + """ + { + "styles": {} + } + """, + false + }, + }; + + public static TheoryData InvalidPathsStylesConfig => + new() + { + { + """ + styles: + user_dictionaries: + - "the path to nowhere" + """, + true + }, + { + """ + { + "styles": { + "user_dictionaries": [ + "the path to nowhere" + ] + } + } + """, + false + }, + }; + + public static TheoryData InvalidStylesConfig => + new() + { + { + """ + styles: + user_dictionaries: "path/to/dict.xaml" + """, + true + }, + { + """ + { + "styles": { + "user_dictionaries": "path/to/dict.xaml" + } + } + """, + false + }, + }; + + [Theory] + [MemberAutoSubstituteData(nameof(NoStylesConfig))] + [MemberAutoSubstituteData(nameof(InvalidPathsStylesConfig))] + [MemberAutoSubstituteData(nameof(InvalidStylesConfig))] + public void Load_DoesNotAddUserDictionaries(string config, bool isYaml, IContext ctx) + { + // Given + YamlLoaderTestUtils.SetupFileConfig(ctx, config, isYaml); + ctx.FileManager.FileExists(Arg.Is(p => !p.Contains("yaml") && !p.Contains("json"))).Returns(false); + + // When + bool result = YamlLoader.Load(ctx, showErrorWindow: false); + + // Then + Assert.True(result); + ctx.ResourceManager.DidNotReceive().AddUserDictionary(Arg.Any()); + } +} diff --git a/src/Whim.Yaml/ErrorWindow.xaml.cs b/src/Whim.Yaml/ErrorWindow.xaml.cs index e2420c81d..fb75d4b68 100644 --- a/src/Whim.Yaml/ErrorWindow.xaml.cs +++ b/src/Whim.Yaml/ErrorWindow.xaml.cs @@ -1,4 +1,5 @@ -using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; using Microsoft.UI.Xaml; namespace Whim.Yaml; @@ -6,15 +7,35 @@ namespace Whim.Yaml; /// /// Exposes YAML errors encountered during parsing to the user. /// -public sealed partial class ErrorWindow : Microsoft.UI.Xaml.Window, IDisposable +public sealed partial class ErrorWindow : Microsoft.UI.Xaml.Window, IDisposable, INotifyPropertyChanged { private readonly IContext _ctx; private readonly WindowBackdropController _backdropController; + private string _message = string.Empty; /// /// The errors. /// - public string Message { get; } + public string Message + { + get => _message; + private set + { + if (_message != value) + { + _message = value; + OnPropertyChanged(); + } + } + } + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } /// /// Exposes YAML errors encountered during parsing to the user. @@ -46,6 +67,15 @@ private void Quit_Click(object sender, RoutedEventArgs e) _ctx.Exit(new ExitEventArgs() { Reason = ExitReason.User, Message = Message }); } + /// + /// Appends additional text to the error message. + /// + /// The text to append. + public void AppendMessage(string text) + { + Message = Message + Environment.NewLine + text; + } + /// public void Dispose() { diff --git a/src/Whim.Yaml/YamlLoader.cs b/src/Whim.Yaml/YamlLoader.cs index 0d8e6c468..d9145db28 100644 --- a/src/Whim.Yaml/YamlLoader.cs +++ b/src/Whim.Yaml/YamlLoader.cs @@ -14,6 +14,7 @@ public static class YamlLoader { private const string JsonConfigFileName = "whim.config.json"; private const string YamlConfigFileName = "whim.config.yaml"; + private static ErrorWindow? _errorWindow; /// /// Loads and applies the declarative configuration from a JSON or YAML file. @@ -30,16 +31,14 @@ public static bool Load(IContext ctx, bool showErrorWindow = true) return false; } - if (showErrorWindow) - { - ValidateConfig(ctx, schema); - } + ValidateConfig(ctx, schema, showErrorWindow); UpdateWorkspaces(ctx, schema); UpdateKeybinds(ctx, schema); UpdateFilters(ctx, schema); UpdateRouters(ctx, schema); + UpdateStyles(ctx, schema, showErrorWindow); YamlPluginLoader.LoadPlugins(ctx, schema); YamlLayoutEngineLoader.UpdateLayoutEngines(ctx, schema); @@ -76,7 +75,7 @@ public static bool Load(IContext ctx, bool showErrorWindow = true) return null; } - private static void ValidateConfig(IContext ctx, Schema schema) + private static void ValidateConfig(IContext ctx, Schema schema, bool showErrorWindow) { ValidationContext result = schema.Validate(ValidationContext.ValidContext, ValidationLevel.Detailed); if (result.IsValid) @@ -106,8 +105,23 @@ private static void ValidateConfig(IContext ctx, Schema schema) Logger.Error("Configuration file is not valid."); Logger.Error(errors); - using ErrorWindow window = new(ctx, errors); - window.Activate(); + if (showErrorWindow) + { + ShowError(ctx, errors); + } + } + + private static void ShowError(IContext ctx, string errors) + { + if (_errorWindow == null) + { + _errorWindow = new(ctx, errors); + _errorWindow.Activate(); + } + else + { + _errorWindow.AppendMessage(errors); + } } private static void UpdateWorkspaces(IContext ctx, Schema schema) @@ -265,4 +279,52 @@ private static void UpdateRouters(IContext ctx, Schema schema) } } } + + private static void UpdateStyles(IContext ctx, Schema schema, bool showErrorWindow) + { + if (!schema.Styles.IsValid()) + { + Logger.Debug("Styles config is not valid."); + return; + } + + if (schema.Styles.UserDictionaries.AsOptional() is not { } userDictionaries) + { + Logger.Debug("No styles found."); + return; + } + + foreach (var userDictionary in userDictionaries) + { + if (GetUserDictionaryPath(ctx, (string)userDictionary, showErrorWindow) is not string filePath) + { + continue; + } + + ctx.ResourceManager.AddUserDictionary(filePath); + } + } + + private static string? GetUserDictionaryPath(IContext ctx, string filePath, bool showErrorWindow) + { + if (ctx.FileManager.FileExists(filePath)) + { + return filePath; + } + + string relativePath = Path.Combine(ctx.FileManager.WhimDir, filePath); + if (!ctx.FileManager.FileExists(relativePath)) + { + string error = $"User dictionary not found: {filePath}"; + Logger.Error(error); + + if (showErrorWindow) + { + ShowError(ctx, error); + } + return null; + } + + return relativePath; + } } diff --git a/src/Whim.Yaml/schema.json b/src/Whim.Yaml/schema.json index 84c8ab63c..2f378e257 100644 --- a/src/Whim.Yaml/schema.json +++ b/src/Whim.Yaml/schema.json @@ -113,6 +113,19 @@ "description": "Plugin to add a tree layout" } } + }, + + "styles": { + "type": "object", + "properties": { + "user_dictionaries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Paths to XAML dictionaries to use for styling" + } + } } },