From 62b387bcfe798d9d1c2172f50f37b7b4f23b5be6 Mon Sep 17 00:00:00 2001 From: "13997737+wolframhaussig@users.noreply.github.com" <13997737+wolframhaussig@users.noreply.github.com> Date: Thu, 26 Oct 2023 13:26:16 +0200 Subject: [PATCH 1/9] first draft of new file structure --- WinFormsThemes/TestProject/FileThemeTest.cs | 132 +++++++- .../ThemeLookup/FileThemeLookup.cs | 6 +- .../ThemeLookup/ResourceThemeLookup.cs | 4 +- .../WinFormsThemes/Themes/AbstractTheme.cs | 5 +- .../WinFormsThemes/Themes/FileTheme.cs | 287 +++++++++++++----- .../WinFormsThemes/WinFormsThemes.csproj | 11 +- .../WinFormsThemes/themes.schema.json | 216 +++++++++++++ 7 files changed, 572 insertions(+), 89 deletions(-) create mode 100644 WinFormsThemes/WinFormsThemes/themes.schema.json diff --git a/WinFormsThemes/TestProject/FileThemeTest.cs b/WinFormsThemes/TestProject/FileThemeTest.cs index c46a8cf..4af1fc0 100644 --- a/WinFormsThemes/TestProject/FileThemeTest.cs +++ b/WinFormsThemes/TestProject/FileThemeTest.cs @@ -1,23 +1,25 @@ -using System.Drawing; +using System.Drawing; +using Microsoft.Extensions.Logging; using TestProject.Properties; +using WinFormsThemes.Extensions; using WinFormsThemes.Themes; namespace TestProject { [TestClass] - public class FileThemeTest + public class FileThemeTest : AbstractTestClass { [TestMethod] public void LoadShouldNotThrow_MissingCapabilities() { - FileTheme? theme = FileTheme.Load(Resources.MISSING_CAPS); + FileTheme? theme = FileTheme.Load(Resources.MISSING_CAPS, getLogger()); Assert.IsNull(theme); } [TestMethod] public void LoadShouldNotThrow_MissingColors() { - FileTheme? theme = FileTheme.Load(Resources.MISSING_COLORS); + FileTheme? theme = FileTheme.Load(Resources.MISSING_COLORS, getLogger()); Assert.IsNotNull(theme); Assert.AreEqual(SystemColors.Control, theme.BackgroundColor); } @@ -25,15 +27,131 @@ public void LoadShouldNotThrow_MissingColors() [TestMethod] public void LoadShouldNotThrow_MissingName() { - FileTheme? theme = FileTheme.Load(Resources.MISSING_NAME); + FileTheme? theme = FileTheme.Load(Resources.MISSING_NAME, getLogger()); Assert.IsNull(theme); } [TestMethod] public void LoadShouldNotThrow_NonJson() { - FileTheme? theme = FileTheme.Load("abc"); + FileTheme? theme = FileTheme.Load("abc", getLogger()); Assert.IsNull(theme); } + + [TestMethod] + public void LoadV2Simple() + { + FileTheme? theme = FileTheme.Load(@" + { + 'name': 'theme-name', + 'capabilities': ['DarkMode', 'HighContrast'], + 'version': 3, + 'variables': { + + }, + 'colors': { + 'backColor': '#082a56', + 'foreColor': '#082a56', + 'controls': { + 'backColor': '#082a56', + 'foreColor': '#082a56' + } + } + }".Replace("'", "\"", StringComparison.Ordinal), getLogger()); + Assert.IsNotNull(theme); + } + + [TestMethod] + public void LoadV2WithVariable() + { + FileTheme? theme = FileTheme.Load(@" + { + 'name': 'theme-name', + 'capabilities': ['DarkMode', 'HighContrast'], + 'version': 3, + 'variables': { + 'backColor': '#082a56', + 'foreColor': '#082a57' + }, + 'colors': { + 'backColor': 'backColor', + 'foreColor': 'foreColor', + 'controls': { + 'backColor': 'backColor', + 'foreColor': 'foreColor' + } + } + }".Replace("'", "\"", StringComparison.CurrentCulture), getLogger()); + Assert.IsNotNull(theme); + Assert.AreEqual("#082a56".ToColor(), theme.ControlBackColor); + Assert.AreEqual("#082a57".ToColor(), theme.ControlForeColor); + } + + [TestMethod] + public void LoadV2InvalidSchema() + { + FileTheme? theme = FileTheme.Load(@" + { + 'name': 'theme-name', + 'capabilities': ['DarkMode', 'HighContrast'], + 'version': 3, + 'variables': { + + } + }".Replace("'", "\"", StringComparison.CurrentCulture), getLogger()); + Assert.IsNull(theme); + } + + [TestMethod] + public void LoadV2DefaultValue() + { + FileTheme? theme = FileTheme.Load(@" + { + 'name': 'theme-name', + 'capabilities': ['DarkMode', 'HighContrast'], + 'version': 3, + 'variables': { + + }, + 'colors': { + 'backColor': '#082a56', + 'foreColor': '#082a56', + 'controls': { + 'backColor': '#082a56', + 'foreColor': '#082a56' + } + } + }".Replace("'", "\"", StringComparison.CurrentCulture), getLogger()); + Assert.IsNotNull(theme); + Assert.AreEqual("#082a56".ToColor(), theme.TableBackColor); + } + + [TestMethod] + public void LoadV2InvalidColor() + { + FileTheme? theme = FileTheme.Load(@" + { + 'name': 'theme-name', + 'capabilities': ['DarkMode', 'HighContrast'], + 'version': 3, + 'variables': { + + }, + 'colors': { + 'backColor': '#082a5656', + 'foreColor': '#082a56', + 'controls': { + 'backColor': '#082a56', + 'foreColor': '#082a56' + } + } + }".Replace("'", "\"", StringComparison.CurrentCulture), getLogger()); + Assert.IsNull(theme); + } + + private ILogger getLogger() + { + return new Logger(LoggerFactory); + } } -} \ No newline at end of file +} diff --git a/WinFormsThemes/WinFormsThemes/ThemeLookup/FileThemeLookup.cs b/WinFormsThemes/WinFormsThemes/ThemeLookup/FileThemeLookup.cs index 3c0b354..603077c 100644 --- a/WinFormsThemes/WinFormsThemes/ThemeLookup/FileThemeLookup.cs +++ b/WinFormsThemes/WinFormsThemes/ThemeLookup/FileThemeLookup.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using WinFormsThemes.Themes; @@ -33,7 +33,7 @@ public IList Lookup() _logger.LogDebug("found {count} theme files in {folder}", files.Count(), _folder.FullName); foreach (FileInfo file in files) { - ITheme? theme = FileTheme.Load(File.ReadAllText(file.FullName)); + ITheme? theme = FileTheme.Load(File.ReadAllText(file.FullName), _logger); if (theme is not null) { results.Add(theme); @@ -52,4 +52,4 @@ public void UseLogger(ILoggerFactory loggerFactory) _logger = new Logger(loggerFactory); } } -} \ No newline at end of file +} diff --git a/WinFormsThemes/WinFormsThemes/ThemeLookup/ResourceThemeLookup.cs b/WinFormsThemes/WinFormsThemes/ThemeLookup/ResourceThemeLookup.cs index 95c940f..937e511 100644 --- a/WinFormsThemes/WinFormsThemes/ThemeLookup/ResourceThemeLookup.cs +++ b/WinFormsThemes/WinFormsThemes/ThemeLookup/ResourceThemeLookup.cs @@ -107,7 +107,7 @@ private void handleEmbeddedResource(Stream? stream, string resName) if (stream is not null) { using StreamReader reader = new(stream); - add(FileTheme.Load(reader.ReadToEnd()), resName); + add(FileTheme.Load(reader.ReadToEnd(), _logger), resName); } } @@ -129,7 +129,7 @@ private void handleResource(string resourceName, Assembly assembly) if (entry.Key is string key && key.StartsWith(_resThemePrefix, StringComparison.Ordinal) && entry.Value is string value) { - add(FileTheme.Load(value), key); + add(FileTheme.Load(value, _logger), key); } } } diff --git a/WinFormsThemes/WinFormsThemes/Themes/AbstractTheme.cs b/WinFormsThemes/WinFormsThemes/Themes/AbstractTheme.cs index bc97c40..140acd9 100644 --- a/WinFormsThemes/WinFormsThemes/Themes/AbstractTheme.cs +++ b/WinFormsThemes/WinFormsThemes/Themes/AbstractTheme.cs @@ -47,6 +47,7 @@ public abstract class AbstractTheme : ITheme public virtual Color ControlBorderColor => ControlHighlightColor; [ExcludeFromCodeCoverage] + [Obsolete(message: "use ControlBorderColor instead")] public virtual Color ControlBorderLightColor => ControlBorderColor; [ExcludeFromCodeCoverage] @@ -62,9 +63,11 @@ public abstract class AbstractTheme : ITheme public abstract Color ControlHighlightColor { get; } [ExcludeFromCodeCoverage] + [Obsolete(message: "use ControlHighlightColor instead")] public virtual Color ControlHighlightDarkColor => GetSoftenedColor(ControlBorderColor); [ExcludeFromCodeCoverage] + [Obsolete(message: "use ControlHighlightColor instead")] public virtual Color ControlHighlightLightColor => GetSoftenedColor(ControlBorderColor, true); [ExcludeFromCodeCoverage] @@ -208,7 +211,7 @@ public void Apply(Control control, ThemeOptions options) { ts.Renderer = new ThemedToolStripRenderer( new ThemedColorTable( - Color.Transparent, ControlBorderLightColor, ButtonHoverColor, ControlHighlightColor, ControlBackColor), + Color.Transparent, ControlBorderColor, ButtonHoverColor, ControlHighlightColor, ControlBackColor), ButtonForeColor, getForegroundColorForStyle(options, true)) { diff --git a/WinFormsThemes/WinFormsThemes/Themes/FileTheme.cs b/WinFormsThemes/WinFormsThemes/Themes/FileTheme.cs index 188cdb7..7cace65 100644 --- a/WinFormsThemes/WinFormsThemes/Themes/FileTheme.cs +++ b/WinFormsThemes/WinFormsThemes/Themes/FileTheme.cs @@ -1,5 +1,9 @@ using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using Json.Schema; +using Microsoft.Extensions.Logging; using WinFormsThemes.Extensions; namespace WinFormsThemes.Themes @@ -9,95 +13,111 @@ namespace WinFormsThemes.Themes /// internal sealed class FileTheme : AbstractTheme { + private static readonly Regex HEX_COLOR_VALUE = new("^#[0-9A-Fa-f]{6}$", RegexOptions.Compiled); [ExcludeFromCodeCoverage] public override IList AdvancedCapabilities { get; } + private Color _backgroundColor; [ExcludeFromCodeCoverage] - public override Color BackgroundColor { get; } + public override Color BackgroundColor => _backgroundColor; + private Color _buttonBackColor; [ExcludeFromCodeCoverage] - public override Color ButtonBackColor { get; } + public override Color ButtonBackColor => _buttonBackColor; + private Color _buttonForeColor; [ExcludeFromCodeCoverage] - public override Color ButtonForeColor { get; } + public override Color ButtonForeColor => _buttonForeColor; + private Color _buttonHoverColor; [ExcludeFromCodeCoverage] - public override Color ButtonHoverColor { get; } + public override Color ButtonHoverColor => _buttonHoverColor; [ExcludeFromCodeCoverage] public override ThemeCapabilities Capabilities { get; } + private Color _comboBoxItemBackColor; [ExcludeFromCodeCoverage] - public override Color ComboBoxItemBackColor { get; } + public override Color ComboBoxItemBackColor => _comboBoxItemBackColor; + private Color _comboBoxItemHoverColor; [ExcludeFromCodeCoverage] - public override Color ComboBoxItemHoverColor { get; } + public override Color ComboBoxItemHoverColor => _comboBoxItemHoverColor; + private Color _controlBackColor; [ExcludeFromCodeCoverage] - public override Color ControlBackColor { get; } + public override Color ControlBackColor => _controlBackColor; + private Color _controlBorderColor; [ExcludeFromCodeCoverage] - public override Color ControlBorderColor { get; } + public override Color ControlBorderColor => _controlBorderColor; + private Color _controlErrorBackColor; [ExcludeFromCodeCoverage] - public override Color ControlBorderLightColor { get; } + public override Color ControlErrorBackColor => _controlErrorBackColor; + private Color _controlErrorForeColor; [ExcludeFromCodeCoverage] - public override Color ControlErrorBackColor { get; } + public override Color ControlErrorForeColor => _controlErrorForeColor; + private Color _controlForeColor; [ExcludeFromCodeCoverage] - public override Color ControlErrorForeColor { get; } + public override Color ControlForeColor => _controlForeColor; + private Color _controlHighlightColor; [ExcludeFromCodeCoverage] - public override Color ControlForeColor { get; } + public override Color ControlHighlightColor => _controlHighlightColor; + private Color _controlSuccessBackColor; [ExcludeFromCodeCoverage] - public override Color ControlHighlightColor { get; } + public override Color ControlSuccessBackColor => _controlSuccessBackColor; + private Color _controlSuccessForeColor; [ExcludeFromCodeCoverage] - public override Color ControlHighlightDarkColor { get; } + public override Color ControlSuccessForeColor => _controlSuccessForeColor; + private Color _controlWarningBackColor; [ExcludeFromCodeCoverage] - public override Color ControlHighlightLightColor { get; } + public override Color ControlWarningBackColor => _controlWarningBackColor; + private Color _controlWarningForeColor; [ExcludeFromCodeCoverage] - public override Color ControlSuccessBackColor { get; } + public override Color ControlWarningForeColor => _controlWarningForeColor; + private Color _foregroundColor; [ExcludeFromCodeCoverage] - public override Color ControlSuccessForeColor { get; } + public override Color ForegroundColor => _foregroundColor; + private Color _listViewHeaderGroupColor; [ExcludeFromCodeCoverage] - public override Color ControlWarningBackColor { get; } - - [ExcludeFromCodeCoverage] - public override Color ControlWarningForeColor { get; } - - [ExcludeFromCodeCoverage] - public override Color ForegroundColor { get; } - - [ExcludeFromCodeCoverage] - public override Color ListViewHeaderGroupColor { get; } + public override Color ListViewHeaderGroupColor => _listViewHeaderGroupColor; [ExcludeFromCodeCoverage] public override string Name { get; } + private Color _tableBackColor; [ExcludeFromCodeCoverage] - public override Color TableBackColor { get; } + public override Color TableBackColor => _tableBackColor; + private Color _tableCellBackColor; [ExcludeFromCodeCoverage] - public override Color TableCellBackColor { get; } + public override Color TableCellBackColor => _tableCellBackColor; + private Color _tableCellForeColor; [ExcludeFromCodeCoverage] - public override Color TableCellForeColor { get; } + public override Color TableCellForeColor => _tableCellForeColor; + private Color _tableHeaderBackColor; [ExcludeFromCodeCoverage] - public override Color TableHeaderBackColor { get; } + public override Color TableHeaderBackColor => _tableHeaderBackColor; + private Color _tableHeaderForeColor; [ExcludeFromCodeCoverage] - public override Color TableHeaderForeColor { get; } + public override Color TableHeaderForeColor => _tableHeaderForeColor; + private Color _tableSelectionBackColor; [ExcludeFromCodeCoverage] - public override Color TableSelectionBackColor { get; } + public override Color TableSelectionBackColor => _tableSelectionBackColor; /// /// constructor @@ -116,66 +136,182 @@ private FileTheme(JsonNode doc) //use the theme version to update the configured colors if necessary //e.g. when a new version adds a new color you may calculate the missing value from the existing ones + int themeVersion = ((int?)doc["version"]) ?? 1; + if (themeVersion >= 3) + { + loadFromNewFileStructure(doc); + } + else + { + loadFromOldFileStructure(doc); + } + } + /// + /// run the old import logic. can be removed after all json files in this repo are migrated + /// + /// + private void loadFromOldFileStructure(JsonNode doc) + { int themeVersion = ((int?)doc["version"]) ?? 1; if (themeVersion >= 1) { - BackgroundColor = ((string?)doc["colors"]?["backColor"]).ToColor(); - ForegroundColor = ((string?)doc["colors"]?["foreColor"]).ToColor(); + _backgroundColor = ((string?)doc["colors"]?["backColor"]).ToColor(); + _foregroundColor = ((string?)doc["colors"]?["foreColor"]).ToColor(); - ControlBackColor = ((string?)doc["colors"]?["controlBackColor"]).ToColor(); - ControlForeColor = ((string?)doc["colors"]?["controlForeColor"]).ToColor(); - ControlHighlightColor = ((string?)doc["colors"]?["controlHighlightColor"]).ToColor(); + _controlBackColor = ((string?)doc["colors"]?["controlBackColor"]).ToColor(); + _controlForeColor = ((string?)doc["colors"]?["controlForeColor"]).ToColor(); + _controlHighlightColor = ((string?)doc["colors"]?["controlHighlightColor"]).ToColor(); - ButtonBackColor = ((string?)doc["colors"]?["buttonBackColor"]).ToColor(); - ButtonForeColor = ((string?)doc["colors"]?["buttonForeColor"]).ToColor(); - ButtonHoverColor = ((string?)doc["colors"]?["buttonHoverColor"]).ToColor(); + _buttonBackColor = ((string?)doc["colors"]?["buttonBackColor"]).ToColor(); + _buttonForeColor = ((string?)doc["colors"]?["buttonForeColor"]).ToColor(); + _buttonHoverColor = ((string?)doc["colors"]?["buttonHoverColor"]).ToColor(); - ControlSuccessBackColor = ((string?)doc["colors"]?["successBackColor"]).ToColor(); - ControlSuccessForeColor = ((string?)doc["colors"]?["successForeColor"]).ToColor(); - ControlWarningBackColor = ((string?)doc["colors"]?["warningBackColor"]).ToColor(); - ControlWarningForeColor = ((string?)doc["colors"]?["warningForeColor"]).ToColor(); - ControlErrorBackColor = ((string?)doc["colors"]?["errorBackColor"]).ToColor(); - ControlErrorForeColor = ((string?)doc["colors"]?["errorForeColor"]).ToColor(); + _controlSuccessBackColor = ((string?)doc["colors"]?["successBackColor"]).ToColor(); + _controlSuccessForeColor = ((string?)doc["colors"]?["successForeColor"]).ToColor(); + _controlWarningBackColor = ((string?)doc["colors"]?["warningBackColor"]).ToColor(); + _controlWarningForeColor = ((string?)doc["colors"]?["warningForeColor"]).ToColor(); + _controlErrorBackColor = ((string?)doc["colors"]?["errorBackColor"]).ToColor(); + _controlErrorForeColor = ((string?)doc["colors"]?["errorForeColor"]).ToColor(); //backwards compatibility for themes V2 - TableBackColor = ControlBackColor; - TableHeaderBackColor = TableBackColor; - TableHeaderForeColor = ControlForeColor; - TableSelectionBackColor = ControlHighlightColor; - TableCellBackColor = TableBackColor; - TableCellForeColor = ControlForeColor; - ListViewHeaderGroupColor = GetSoftenedColor(ControlHighlightColor, true); - ComboBoxItemBackColor = ControlHighlightColor; - ComboBoxItemHoverColor = GetSoftenedColor(ControlHighlightColor, true); - ControlHighlightLightColor = GetSoftenedColor(ControlBorderColor, true); - ControlHighlightDarkColor = GetSoftenedColor(ControlBorderColor); - ControlBorderColor = ControlHighlightColor; - ControlBorderLightColor = ControlHighlightColor; + _tableBackColor = ControlBackColor; + _tableHeaderBackColor = TableBackColor; + _tableHeaderForeColor = ControlForeColor; + _tableSelectionBackColor = ControlHighlightColor; + _tableCellBackColor = TableBackColor; + _tableCellForeColor = ControlForeColor; + _listViewHeaderGroupColor = GetSoftenedColor(ControlHighlightColor, true); + _comboBoxItemBackColor = ControlHighlightColor; + _comboBoxItemHoverColor = GetSoftenedColor(ControlHighlightColor, true); + _controlBorderColor = ControlHighlightColor; } if (themeVersion >= 2) { - TableBackColor = ((string?)doc["colors"]?["tableBackColor"]).ToColor(); - TableHeaderBackColor = ((string?)doc["colors"]?["tableHeaderBackColor"]).ToColor(); - TableHeaderForeColor = ((string?)doc["colors"]?["tableHeaderForeColor"]).ToColor(); - TableSelectionBackColor = ((string?)doc["colors"]?["tableSelectionBackColor"]).ToColor(); - TableCellBackColor = ((string?)doc["colors"]?["tableCellBackColor"]).ToColor(); - TableCellForeColor = ((string?)doc["colors"]?["tableCellForeColor"]).ToColor(); - ListViewHeaderGroupColor = ((string?)doc["colors"]?["listViewHeaderGroupColor"]).ToColor(); - ComboBoxItemBackColor = ((string?)doc["colors"]?["comboBoxItemBackColor"]).ToColor(); - ComboBoxItemHoverColor = ((string?)doc["colors"]?["comboBoxItemHoverColor"]).ToColor(); - ControlHighlightLightColor = ((string?)doc["colors"]?["controlHighlightLightColor"]).ToColor(); - ControlHighlightDarkColor = ((string?)doc["colors"]?["controlHighlightDarkColor"]).ToColor(); - ControlBorderColor = ((string?)doc["colors"]?["controlBorderColor"]).ToColor(); - ControlBorderLightColor = ((string?)doc["colors"]?["controlBorderLightColor"]).ToColor(); + _tableBackColor = ((string?)doc["colors"]?["tableBackColor"]).ToColor(); + _tableHeaderBackColor = ((string?)doc["colors"]?["tableHeaderBackColor"]).ToColor(); + _tableHeaderForeColor = ((string?)doc["colors"]?["tableHeaderForeColor"]).ToColor(); + _tableSelectionBackColor = ((string?)doc["colors"]?["tableSelectionBackColor"]).ToColor(); + _tableCellBackColor = ((string?)doc["colors"]?["tableCellBackColor"]).ToColor(); + _tableCellForeColor = ((string?)doc["colors"]?["tableCellForeColor"]).ToColor(); + _listViewHeaderGroupColor = ((string?)doc["colors"]?["listViewHeaderGroupColor"]).ToColor(); + _comboBoxItemBackColor = ((string?)doc["colors"]?["comboBoxItemBackColor"]).ToColor(); + _comboBoxItemHoverColor = ((string?)doc["colors"]?["comboBoxItemHoverColor"]).ToColor(); + _controlBorderColor = ((string?)doc["colors"]?["controlBorderColor"]).ToColor(); + } + } + /// + /// loads the JSON Schema for validation + /// + private static JsonSchema? getSchema() + { + using Stream? str = Assembly.GetExecutingAssembly().GetManifestResourceStream("WinFormsThemes.themes.schema.json"); + if (str is not null) + { + return JsonSchema.FromStream(str).AsTask().Result; + } + return null; + } + /// + /// load all color variables + /// + /// + private static Dictionary loadVariables(JsonNode? vars) + { + Dictionary result = new(); + if (vars is not null) + { + foreach (string k in vars.AsObject().Select(p => p.Key).ToList()) + { + result.Add(k, ((string?)vars[k]).ToColor()); + } + } + return result; + } + /// + /// parse the given color code + /// + /// the hex color code + /// the default color if no hex code is set + /// the dictionary for looking up variable declarations + /// the color could not be parsed + private static Color parseColor(JsonNode? value, Color defaultColor, Dictionary vars) + { + string? hexColor = (string?)value; + if (hexColor is null) + { + return defaultColor; + } + if (vars.ContainsKey(hexColor)) + { + return vars[hexColor]; + } + if (HEX_COLOR_VALUE.IsMatch(hexColor)) + { + return hexColor.ToColor(); + } + else + { + throw new ArgumentException($"Invalid color '{hexColor}' - color is not a valid hex value and was not defined as a variable!"); + } + } + /// + /// load the new JSON structure including schema validation + /// + /// the theme to load + /// if the document was not valid + private void loadFromNewFileStructure(JsonNode doc) + { + if (getSchema()?.Evaluate(doc)?.IsValid == false) + { + throw new ArgumentException("Invalid schema"); } + Dictionary vars = loadVariables(doc["variables"]); + JsonNode colors = doc["colors"]!; + _backgroundColor = parseColor(colors["backColor"], Color.Empty, vars); + _foregroundColor = parseColor(colors["foreColor"], Color.Empty, vars); + + JsonNode controls = colors["controls"]!; + _controlBackColor = parseColor(controls["backColor"], Color.Empty, vars); + _controlForeColor = parseColor(controls["foreColor"], Color.Empty, vars); + _controlHighlightColor = parseColor(controls["highlightColor"], _controlBackColor, vars); + _controlBorderColor = parseColor(controls["borderColor"], _controlBackColor, vars); + + JsonNode? button = colors["button"]; + _buttonBackColor = parseColor(button?["backColor"], _controlBackColor, vars); + _buttonForeColor = parseColor(button?["foreColor"], _controlForeColor, vars); + _buttonHoverColor = parseColor(button?["hoverColor"], _buttonBackColor, vars); + + JsonNode? comboBox = colors["comboBox"]; + _comboBoxItemBackColor = parseColor(comboBox?["itemBackColor"], _controlBackColor, vars); + _comboBoxItemHoverColor = parseColor(comboBox?["itemHoverColor"], _controlBackColor, vars); + + JsonNode? listView = colors["listView"]; + _listViewHeaderGroupColor = parseColor(listView?["headerGroupColor"], _controlBackColor, vars); + + JsonNode? dataGridView = colors["dataGridView"]; + _tableBackColor = parseColor(dataGridView?["backColor"], _controlBackColor, vars); + _tableHeaderBackColor = parseColor(dataGridView?["headerBackColor"], _tableBackColor, vars); + _tableHeaderForeColor = parseColor(dataGridView?["headerForeColor"], _controlForeColor, vars); + _tableSelectionBackColor = parseColor(dataGridView?["selectionBackColor"], _tableBackColor, vars); + _tableCellBackColor = parseColor(dataGridView?["cellBackColor"], _tableBackColor, vars); + _tableCellForeColor = parseColor(dataGridView?["cellForeColor"], _controlForeColor, vars); + + JsonNode? status = colors["status"]; + _controlSuccessBackColor = parseColor(status?["success"]?["backColor"], _controlBackColor, vars); + _controlSuccessForeColor = parseColor(status?["success"]?["foreColor"], _controlForeColor, vars); + _controlWarningBackColor = parseColor(status?["warning"]?["backColor"], _controlBackColor, vars); + _controlWarningForeColor = parseColor(status?["warning"]?["foreColor"], _controlForeColor, vars); + _controlErrorBackColor = parseColor(status?["error"]?["backColor"], _controlBackColor, vars); + _controlErrorForeColor = parseColor(status?["error"]?["foreColor"], _controlForeColor, vars); + } /// /// Parse a theme JSON config /// /// the JSON content - public static FileTheme? Load(string jsonContent) + public static FileTheme? Load(string jsonContent, ILogger log) { try { @@ -187,8 +323,9 @@ private FileTheme(JsonNode doc) return new FileTheme(json); } - catch (Exception) + catch (Exception ex) { + log.Log(LogLevel.Error, ex, "failed to read theme JSON"); return null; } } diff --git a/WinFormsThemes/WinFormsThemes/WinFormsThemes.csproj b/WinFormsThemes/WinFormsThemes/WinFormsThemes.csproj index 325c9fa..198b1de 100644 --- a/WinFormsThemes/WinFormsThemes/WinFormsThemes.csproj +++ b/WinFormsThemes/WinFormsThemes/WinFormsThemes.csproj @@ -20,6 +20,14 @@ snupkg + + + + + + + + True @@ -29,8 +37,9 @@ + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/WinFormsThemes/WinFormsThemes/themes.schema.json b/WinFormsThemes/WinFormsThemes/themes.schema.json new file mode 100644 index 0000000..6c8ed91 --- /dev/null +++ b/WinFormsThemes/WinFormsThemes/themes.schema.json @@ -0,0 +1,216 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Filebased theme definition", + "description": "A filebased theme definition for https://github.com/Assorted-Development/winforms-themes", + "type": "object", + + "properties": { + + "name": { + "description": "The theme name", + "type": "string" + }, + + "capabilities": { + "description": "Either one of 'DarkMode', 'LightMode', 'HighContrast' or any string as custom capability", + "type": "array", + "items": { + "type": "string" + } + }, + + "version": { + "description": "The schema version. required backwards compatibility. should be set to 3 for now.", + "type": "integer", + "minimum": 3 + }, + + "variables": { + "description": "Can be used to reuse color definitions. The variable names can be used instead of a color below", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/color_type" + } + }, + "colors": { + "description": "The theme definition", + "type": "object", + "properties": { + "backColor": { + "description": "the form background color", + "$ref": "#/$defs/color_or_var_type" + }, + "foreColor": { + "description": "the form foreground color", + "$ref": "#/$defs/color_or_var_type" + }, + "status": { + "description": "The definition for all status colors", + "type": "object", + "properties": { + "success": { + "description": "The definition for all success status controls", + "type": "object", + "properties": { + "backColor": { + "description": "the success background color", + "$ref": "#/$defs/color_or_var_type" + }, + "foreColor": { + "description": "the success foreground color", + "$ref": "#/$defs/color_or_var_type" + } + } + }, + "warning": { + "description": "The definition for all warning status controls", + "type": "object", + "properties": { + "backColor": { + "description": "the warning background color", + "$ref": "#/$defs/color_or_var_type" + }, + "foreColor": { + "description": "the warning foreground color", + "$ref": "#/$defs/color_or_var_type" + } + } + }, + "error": { + "description": "The definition for all error status controls", + "type": "object", + "properties": { + "backColor": { + "description": "the error background color", + "$ref": "#/$defs/color_or_var_type" + }, + "foreColor": { + "description": "the error foreground color", + "$ref": "#/$defs/color_or_var_type" + } + } + } + } + }, + "controls": { + "description": "The definition for all UI controls", + "type": "object", + "properties": { + "backColor": { + "description": "the general control background color", + "$ref": "#/$defs/color_or_var_type" + }, + "foreColor": { + "description": "the general control foreground color", + "$ref": "#/$defs/color_or_var_type" + }, + "highlightColor": { + "description": "the general control background color when a control should be highlighted", + "$ref": "#/$defs/color_or_var_type" + }, + "borderColor": { + "description": "the general control border color", + "$ref": "#/$defs/color_or_var_type" + }, + "button": { + "description": "The definition for all Buttons", + "type": "object", + "properties": { + "backColor": { + "description": "the button background color", + "$ref": "#/$defs/color_or_var_type" + }, + "foreColor": { + "description": "the button foreground color", + "$ref": "#/$defs/color_or_var_type" + }, + "hoverColor": { + "description": "the button background color when hovering over it", + "$ref": "#/$defs/color_or_var_type" + } + } + }, + "comboBox": { + "description": "The definition for all ComboBoxes", + "type": "object", + "properties": { + "itemBackColor": { + "description": "the comboBox background color for items", + "$ref": "#/$defs/color_or_var_type" + }, + "itemHoverColor": { + "description": "the comboBox background color for items when hovering over them", + "$ref": "#/$defs/color_or_var_type" + } + } + }, + "listView": { + "description": "The definition for all ListViews", + "type": "object", + "properties": { + "headerGroupColor": { + "description": "the background color for the ListViews Header Group", + "$ref": "#/$defs/color_or_var_type" + } + } + }, + "dataGridView": { + "description": "The definition for all DataGridViews", + "type": "object", + "properties": { + "backColor": { + "description": "the background color for the DataGridView", + "$ref": "#/$defs/color_or_var_type" + }, + "headerBackColor": { + "description": "the background color for the DataGridView header", + "$ref": "#/$defs/color_or_var_type" + }, + "headerForeColor": { + "description": "the foreground color for the DataGridView header", + "$ref": "#/$defs/color_or_var_type" + }, + "selectionBackColor": { + "description": "the background color for the DataGridViews selected rows", + "$ref": "#/$defs/color_or_var_type" + }, + "cellBackColor": { + "description": "the background color for the DataGridView cells", + "$ref": "#/$defs/color_or_var_type" + }, + "cellForeColor": { + "description": "the foreground color for the DataGridView cells", + "$ref": "#/$defs/color_or_var_type" + } + } + } + }, + "required": [ "backColor", "foreColor" ] + } + }, + "required": [ "backColor", "foreColor", "controls" ] + } + }, + + "required": [ "name", "capabilities", "version", "colors" ], + + "$defs": { + "color_or_var_type": { + "description": "the color value or a variable name", + "oneOf": [ + { + "$ref": "#/$defs/color_type" + }, + { + "description": "the variable name from the list of variables above", + "type": "string" + } + ] + }, + "color_type": { + "description": "the color value", + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + } +} From 228525c853e5b842e6e6af204690f1bdadfcf632 Mon Sep 17 00:00:00 2001 From: "13997737+wolframhaussig@users.noreply.github.com" <13997737+wolframhaussig@users.noreply.github.com> Date: Thu, 26 Oct 2023 13:28:28 +0200 Subject: [PATCH 2/9] update readme --- README.md | 47 +++++++++++++++-------------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index caa5513..9bebcc3 100644 --- a/README.md +++ b/README.md @@ -117,38 +117,21 @@ Out of the box, there are 2 ways you can add custom themes: Both ways use the same JSON format for the theme definition(the version defines the format of the file): ```json { - "name": "theme-name", - "capabilities": ["DarkMode", "LightMode", "HighContrast"], - "version": 2, - "colors": { - "backColor": "#082a56", - "foreColor": "#082a56", - "buttonBackColor": "#082a56", - "buttonForeColor": "#082a56", - "buttonHoverColor": "#082a56", - "comboBoxItemBackColor": "#082a56", - "comboBoxItemHoverColor": "#082a56", - "controlBackColor": "#082a56", - "controlForeColor": "#082a56", - "controlHighlightColor": "#082a56", - "controlHighlightLightColor": "#082a56", - "controlHighlightDarkColor": "#082a56", - "controlBorderColor": "#082a56", - "controlBorderLightColor": "#082a56", - "listViewHeaderGroupColor": "#082a56", - "tableBackColor": "#082a56", - "tableHeaderBackColor": "#082a56", - "tableHeaderForeColor": "#082a56", - "tableSelectionBackColor": "#082a56", - "tableCellBackColor": "#082a56", - "tableCellForeColor": "#082a56", - "successBackColor": "#082a56", - "successForeColor": "#082a56", - "warningBackColor": "#082a56", - "warningForeColor": "#082a56", - "errorBackColor": "#082a56", - "errorForeColor": "#082a56" - } + "name": "theme-name", + "capabilities": ["DarkMode", "HighContrast"], + "version": 3, + "variables": { + "backColor": "#082a56", + "foreColor": "#082a57" + }, + "colors": { + "backColor": "backColor", + "foreColor": "foreColor", + "controls": { + "backColor": "backColor", + "foreColor": "foreColor" + } + } } ``` From 6181934251583c16e5a1eaee3641654eb524125a Mon Sep 17 00:00:00 2001 From: "13997737+wolframhaussig@users.noreply.github.com" <13997737+wolframhaussig@users.noreply.github.com> Date: Thu, 26 Oct 2023 16:49:05 +0200 Subject: [PATCH 3/9] minor fixes --- README.md | 4 +++- WinFormsThemes/WinFormsThemes/themes.schema.json | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9bebcc3..19ae082 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,8 @@ Out of the box, there are 2 ways you can add custom themes: - Files with the file ending `.theme.json` stored in a `themes` directory of the working dir. - Assembly resources in any assembly where the name starts with `CONFIG_THEMING_THEME_` -Both ways use the same JSON format for the theme definition(the version defines the format of the file): +Both ways use the same JSON format for the theme definition(the version defines the format of the file). +A simple example of this could be: ```json { "name": "theme-name", @@ -134,6 +135,7 @@ Both ways use the same JSON format for the theme definition(the version defines } } ``` +For the complete list of available settings please check our JSON schema. If those 2 ways are not flexible enough, you can implement a theme by yourself and register it using a custom theme source (see below): The prefered way is to subclass `AbstractTheme` as you just need to implement the base colors and optionally override the extended colors - styling the controls is done by the base class. diff --git a/WinFormsThemes/WinFormsThemes/themes.schema.json b/WinFormsThemes/WinFormsThemes/themes.schema.json index 6c8ed91..95242d0 100644 --- a/WinFormsThemes/WinFormsThemes/themes.schema.json +++ b/WinFormsThemes/WinFormsThemes/themes.schema.json @@ -203,7 +203,8 @@ }, { "description": "the variable name from the list of variables above", - "type": "string" + "type": "string", + "pattern": "^[^#]*$" } ] }, From a772892e608110cb0efcc069a096080a93f9b099 Mon Sep 17 00:00:00 2001 From: "13997737+wolframhaussig@users.noreply.github.com" <13997737+wolframhaussig@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:15:44 +0200 Subject: [PATCH 4/9] improve code coverage --- WinFormsThemes/TestProject/FileThemeTest.cs | 28 +++++++++++++-- .../WinFormsThemes/Themes/FileTheme.cs | 34 +++++++------------ 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/WinFormsThemes/TestProject/FileThemeTest.cs b/WinFormsThemes/TestProject/FileThemeTest.cs index 4af1fc0..1621a5e 100644 --- a/WinFormsThemes/TestProject/FileThemeTest.cs +++ b/WinFormsThemes/TestProject/FileThemeTest.cs @@ -127,7 +127,7 @@ public void LoadV2DefaultValue() } [TestMethod] - public void LoadV2InvalidColor() + public void LoadV2InvalidColorOrVariable() { FileTheme? theme = FileTheme.Load(@" { @@ -138,7 +138,7 @@ public void LoadV2InvalidColor() }, 'colors': { - 'backColor': '#082a5656', + 'backColor': 'unknown', 'foreColor': '#082a56', 'controls': { 'backColor': '#082a56', @@ -149,6 +149,30 @@ public void LoadV2InvalidColor() Assert.IsNull(theme); } + [TestMethod] + public void LoadV2SkipEmptyCapabilities() + { + FileTheme? theme = FileTheme.Load(@" + { + 'name': 'theme-name', + 'capabilities': ['DarkMode', 'HighContrast', ''], + 'version': 3, + 'variables': { + + }, + 'colors': { + 'backColor': '#082a56', + 'foreColor': '#082a56', + 'controls': { + 'backColor': '#082a56', + 'foreColor': '#082a56' + } + } + }".Replace("'", "\"", StringComparison.Ordinal), getLogger()); + Assert.IsNotNull(theme); + Assert.AreEqual(0, theme.AdvancedCapabilities.Count); + } + private ILogger getLogger() { return new Logger(LoggerFactory); diff --git a/WinFormsThemes/WinFormsThemes/Themes/FileTheme.cs b/WinFormsThemes/WinFormsThemes/Themes/FileTheme.cs index 7cace65..6685357 100644 --- a/WinFormsThemes/WinFormsThemes/Themes/FileTheme.cs +++ b/WinFormsThemes/WinFormsThemes/Themes/FileTheme.cs @@ -205,12 +205,8 @@ private void loadFromOldFileStructure(JsonNode doc) /// private static JsonSchema? getSchema() { - using Stream? str = Assembly.GetExecutingAssembly().GetManifestResourceStream("WinFormsThemes.themes.schema.json"); - if (str is not null) - { - return JsonSchema.FromStream(str).AsTask().Result; - } - return null; + using Stream str = Assembly.GetExecutingAssembly().GetManifestResourceStream("WinFormsThemes.themes.schema.json")!; + return JsonSchema.FromStream(str).AsTask().Result; } /// /// load all color variables @@ -237,22 +233,22 @@ private static Dictionary loadVariables(JsonNode? vars) /// the color could not be parsed private static Color parseColor(JsonNode? value, Color defaultColor, Dictionary vars) { - string? hexColor = (string?)value; - if (hexColor is null) + string? hexColorOrVariable = (string?)value; + if (hexColorOrVariable is null) { return defaultColor; } - if (vars.ContainsKey(hexColor)) + if (vars.ContainsKey(hexColorOrVariable)) { - return vars[hexColor]; + return vars[hexColorOrVariable]; } - if (HEX_COLOR_VALUE.IsMatch(hexColor)) + if (HEX_COLOR_VALUE.IsMatch(hexColorOrVariable)) { - return hexColor.ToColor(); + return hexColorOrVariable.ToColor(); } else { - throw new ArgumentException($"Invalid color '{hexColor}' - color is not a valid hex value and was not defined as a variable!"); + throw new ArgumentException($"Invalid color '{hexColorOrVariable}' - color is not a valid hex value and was not defined as a variable!"); } } /// @@ -315,13 +311,7 @@ private void loadFromNewFileStructure(JsonNode doc) { try { - JsonNode? json = JsonNode.Parse(jsonContent); - if (json is null) - { - return null; - } - - return new FileTheme(json); + return new FileTheme(JsonNode.Parse(jsonContent)!); } catch (Exception ex) { @@ -335,7 +325,7 @@ private static List getAdvancedCapabilities(JsonArray caps) List advancedCaps = new(); foreach (string? s in caps.Select(node => (string?)node)) { - if (s is null) + if (string.IsNullOrEmpty(s)) { continue; } @@ -354,7 +344,7 @@ private static ThemeCapabilities getThemeCapabilities(JsonArray caps) ThemeCapabilities capabilities = ThemeCapabilities.None; foreach (string? s in caps.Select(node => (string?)node)) { - if (s is null) + if (string.IsNullOrEmpty(s)) { continue; } From fd0697cce10989b0dd43f3460b6b2a1ce3fa13e9 Mon Sep 17 00:00:00 2001 From: wolframhaussig <13997737+wolframhaussig@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:18:46 +0100 Subject: [PATCH 5/9] Create SECURITY.md (#25) * Create SECURITY.md --------- Signed-off-by: wolframhaussig <13997737+wolframhaussig@users.noreply.github.com> --- SECURITY.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..69c6599 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,26 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | Comment | +| ------- | ------------------ | ------- | +| 1.x | :white_check_mark: | not yet released | +| <1.x | :x: | those are prereleases and should not be used in production | + +## Reporting a Vulnerability +**Please do not report security vulnerabilities through public GitHub issues.** + +If you find a vulnerability, please report it to security@nockiro.de. We will get in contact with you about the details and inform you as soon as we have any updates. + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. missing input validation, native code execution, ...) + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +## Preferred Languages + +We support communications in English or German. From e3ea7a35748b0e73b4d973d642ae472a55446844 Mon Sep 17 00:00:00 2001 From: wolframhaussig <13997737+wolframhaussig@users.noreply.github.com> Date: Sat, 24 Feb 2024 14:49:17 +0100 Subject: [PATCH 6/9] added AbstractThemePlugin to make plugin implementations more error proof (#29) --- README.md | 7 +++-- .../TestProject/AbstractThemeTest.cs | 12 ++++----- .../TestProject/ThemeRegistryBuilderTest.cs | 10 +++---- .../ThemeConfig/IThemeRegistryBuilder.cs | 3 +-- .../ThemeConfig/ThemeRegistryBuilder.cs | 4 +-- .../Themes/AbstractThemePlugin.cs | 27 +++++++++++++++++++ .../WinFormsThemes/Themes/IThemePlugin.cs | 8 ++++-- 7 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 WinFormsThemes/WinFormsThemes/Themes/AbstractThemePlugin.cs diff --git a/README.md b/README.md index 8a2efff..5b76c98 100644 --- a/README.md +++ b/README.md @@ -190,11 +190,10 @@ After this, you need to register this class in the builder: As we do not want to force you to use a specific WinForms control library, we currently only support styling of standard controls and controls from our [winforms-stylable-controls](https://github.com/Assorted-Development/winforms-stylable-controls) project. As we understand you may want to also style other controls, we support adding specialised plugins to handle styling of a specific type of control. To do this, you need to implement ``: ```csharp - internal class MyCustomControlThemePlugin : IThemePlugin + internal class MyCustomControlThemePlugin : AbstractThemePlugin { - public void Apply(Control control, AbstractTheme theme) + protected override void ApplyPlugin(MyCustomControl mcc, AbstractTheme theme) { - MyCustomControl mcc = (MyCustomControl)control; //style control based on the colors available in the Theme } } @@ -202,7 +201,7 @@ As we understand you may want to also style other controls, we support adding sp At last, you just need to register it for the correct type: ```csharp ThemeRegistryHolder.GetBuilder() - .AddThemePlugin(new MyCustomControlThemePlugin()) + .AddThemePlugin(new MyCustomControlThemePlugin()) .Build(); ``` diff --git a/WinFormsThemes/TestProject/AbstractThemeTest.cs b/WinFormsThemes/TestProject/AbstractThemeTest.cs index a84e6be..d39d241 100644 --- a/WinFormsThemes/TestProject/AbstractThemeTest.cs +++ b/WinFormsThemes/TestProject/AbstractThemeTest.cs @@ -1,4 +1,4 @@ -using System.Windows.Forms; +using System.Windows.Forms; using WinFormsThemes; using WinFormsThemes.Themes; @@ -13,7 +13,7 @@ public void PluginShouldBeCalledForExactType() ThemePlugin plugin = new(); IThemeRegistry registry = ThemeRegistryHolder.GetBuilder() .SetLoggerFactory(LoggerFactory) - .AddThemePlugin