From 1be464353a730a774e4bb97b8470b78f27bb53c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 18 Jul 2025 22:56:17 +0200 Subject: [PATCH] Add appearance style management --- samples/AvalonDraw/MainWindow.axaml | 7 + samples/AvalonDraw/MainWindow.axaml.cs | 73 ++++++++ .../AvalonDraw/Services/AppearanceService.cs | 159 ++++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 samples/AvalonDraw/Services/AppearanceService.cs diff --git a/samples/AvalonDraw/MainWindow.axaml b/samples/AvalonDraw/MainWindow.axaml index 5773ea868c..aed6b8e522 100644 --- a/samples/AvalonDraw/MainWindow.axaml +++ b/samples/AvalonDraw/MainWindow.axaml @@ -54,6 +54,9 @@ + + + @@ -204,6 +207,10 @@ + + + diff --git a/samples/AvalonDraw/MainWindow.axaml.cs b/samples/AvalonDraw/MainWindow.axaml.cs index 591d8dbbb7..fcec408d33 100644 --- a/samples/AvalonDraw/MainWindow.axaml.cs +++ b/samples/AvalonDraw/MainWindow.axaml.cs @@ -56,6 +56,7 @@ private struct DragInfo private readonly LayerService _layerService = new(); private readonly PatternService _patternService = new(); private readonly BrushService _brushService = new(); + private readonly AppearanceService _appearanceService = new(); public ObservableCollection Properties => _propertiesService.Properties; public ObservableCollection FilteredProperties => _propertiesService.FilteredProperties; private ObservableCollection Nodes { get; } = new(); @@ -64,14 +65,17 @@ private struct DragInfo public ObservableCollection Layers => _layerService.Layers; public ObservableCollection Patterns => _patternService.Patterns; public ObservableCollection BrushStyles => _brushService.Brushes; + public ObservableCollection Styles => _appearanceService.Styles; private ArtboardInfo? _selectedArtboard; private LayerService.LayerEntry? _selectedLayer; private PatternService.PatternEntry? _selectedPattern; private BrushService.BrushEntry? _selectedBrush; + private AppearanceService.StyleEntry? _selectedStyle; private ListBox? _artboardList; private TreeView? _layerTree; private ListBox? _swatchList; private ListBox? _brushList; + private ListBox? _styleList; private readonly HashSet _expandedIds = new(); private HashSet _filterBackup = new(); @@ -350,6 +354,7 @@ public MainWindow() _layerTree = this.FindControl("LayerTree"); _swatchList = this.FindControl("SwatchList"); _brushList = this.FindControl("BrushList"); + _styleList = this.FindControl("StyleList"); _strokeWidthBox = this.FindControl("StrokeWidthBox"); if (_strokeWidthBox is { }) _strokeWidthBox.Text = _toolService.CurrentStrokeWidth.ToString(System.Globalization.CultureInfo.InvariantCulture); @@ -422,6 +427,10 @@ private void LoadDocument(string path) UpdateTitle(); BuildTree(); UpdateArtboards(); + UpdateLayers(); + UpdatePatterns(); + UpdateBrushes(); + UpdateStyles(); } private async void OpenMenuItem_Click(object? sender, RoutedEventArgs e) @@ -1554,6 +1563,7 @@ private void BuildTree() UpdateLayers(); UpdatePatterns(); UpdateBrushes(); + UpdateStyles(); } private void ApplyPropertyFilter() @@ -1625,6 +1635,17 @@ private void UpdateBrushes() } } + private void UpdateStyles() + { + _appearanceService.Load(_document); + if (Styles.Count > 0) + { + _selectedStyle = Styles[0]; + if (_styleList is { }) + _styleList.SelectedIndex = 0; + } + } + private SvgNode CreateNode(SvgElement element, SvgNode? parent = null) { @@ -2298,6 +2319,25 @@ private void BrushList_OnSelectionChanged(object? sender, SelectionChangedEventA } } + private void StyleList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (e.AddedItems.Count > 0 && e.AddedItems[0] is AppearanceService.StyleEntry info) + { + _selectedStyle = info; + if (_selectedSvgElement is SvgVisualElement ve) + { + SaveUndoState(); + info.Apply(ve); + SvgView.SkSvg!.FromSvgDocument(_document); + UpdateSelectedDrawable(); + SaveExpandedNodes(); + BuildTree(); + SelectNodeFromElement(ve); + SvgView.InvalidateVisual(); + } + } + } + private void SelectToolButton_Click(object? sender, RoutedEventArgs e) { if (_pathService.IsEditing) @@ -3080,6 +3120,39 @@ private void SimplifyPathMenuItem_Click(object? sender, RoutedEventArgs e) private void MoveLayerDownMenuItem_Click(object? sender, RoutedEventArgs e) => LayerDown(); private void LockLayerMenuItem_Click(object? sender, RoutedEventArgs e) => LayerLock(); private void UnlockLayerMenuItem_Click(object? sender, RoutedEventArgs e) => LayerUnlock(); + private async void CreateStyleMenuItem_Click(object? sender, RoutedEventArgs e) + { + if (_document is null || _selectedSvgElement is not SvgVisualElement ve) + return; + var win = new SymbolNameWindow(); + var name = await win.ShowDialog(this); + if (string.IsNullOrWhiteSpace(name)) + return; + SaveUndoState(); + var entry = AppearanceService.StyleEntry.FromElement(name!, ve); + _appearanceService.AddOrUpdateStyle(_document, entry); + UpdateStyles(); + } + + private void UpdateStyleMenuItem_Click(object? sender, RoutedEventArgs e) + { + if (_document is null || _selectedStyle is null || _selectedSvgElement is not SvgVisualElement ve) + return; + SaveUndoState(); + var entry = AppearanceService.StyleEntry.FromElement(_selectedStyle.Name, ve); + _appearanceService.AddOrUpdateStyle(_document, entry); + UpdateStyles(); + } + + private void DeleteStyleMenuItem_Click(object? sender, RoutedEventArgs e) + { + if (_document is null || _selectedStyle is null) + return; + SaveUndoState(); + _appearanceService.RemoveStyle(_document, _selectedStyle); + _selectedStyle = null; + UpdateStyles(); + } private void BringForwardMenuItem_Click(object? sender, RoutedEventArgs e) => BringForward(); private void SendBackwardMenuItem_Click(object? sender, RoutedEventArgs e) => SendBackward(); private void LayerAdd_Click(object? sender, RoutedEventArgs e) => LayerAdd(); diff --git a/samples/AvalonDraw/Services/AppearanceService.cs b/samples/AvalonDraw/Services/AppearanceService.cs new file mode 100644 index 0000000000..ab18c1866b --- /dev/null +++ b/samples/AvalonDraw/Services/AppearanceService.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using Svg; + +namespace AvalonDraw.Services; + +public class AppearanceService +{ + public class StyleEntry + { + public string Name { get; set; } = string.Empty; + public string? Fill { get; set; } + public string? Stroke { get; set; } + public float Opacity { get; set; } = 1f; + + public override string ToString() => Name; + + public static StyleEntry FromElement(string name, SvgVisualElement element) + { + var converter = TypeDescriptor.GetConverter(typeof(SvgPaintServer)); + var entry = new StyleEntry { Name = name, Opacity = element.Opacity }; + if (element.Fill is { }) + { + try + { + entry.Fill = converter.ConvertToInvariantString(element.Fill); + } + catch + { + entry.Fill = element.Fill.ToString(); + } + } + if (element.Stroke is { }) + { + try + { + entry.Stroke = converter.ConvertToInvariantString(element.Stroke); + } + catch + { + entry.Stroke = element.Stroke.ToString(); + } + } + return entry; + } + + public void Apply(SvgVisualElement element) + { + var converter = TypeDescriptor.GetConverter(typeof(SvgPaintServer)); + if (Fill is { }) + { + try + { + element.Fill = (SvgPaintServer?)converter.ConvertFromInvariantString(Fill); + } + catch + { + } + } + if (Stroke is { }) + { + try + { + element.Stroke = (SvgPaintServer?)converter.ConvertFromInvariantString(Stroke); + } + catch + { + } + } + element.Opacity = Opacity; + } + } + + public ObservableCollection Styles { get; } = new(); + + private const string Prefix = "data-style-"; + + public void Load(SvgDocument? document) + { + Styles.Clear(); + if (document is null) + return; + foreach (var pair in document.CustomAttributes) + { + if (pair.Key.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + { + var name = pair.Key.Substring(Prefix.Length); + var entry = ParseStyle(name, pair.Value); + if (entry is { }) + Styles.Add(entry); + } + } + } + + private static StyleEntry? ParseStyle(string name, string data) + { + var entry = new StyleEntry { Name = name, Opacity = 1f }; + foreach (var part in data.Split(';', StringSplitOptions.RemoveEmptyEntries)) + { + var items = part.Split(':'); + if (items.Length != 2) + continue; + var key = items[0]; + var val = items[1]; + switch (key) + { + case "fill": + entry.Fill = val; + break; + case "stroke": + entry.Stroke = val; + break; + case "opacity": + if (float.TryParse(val, NumberStyles.Float, CultureInfo.InvariantCulture, out var o)) + entry.Opacity = o; + break; + } + } + return entry; + } + + private static string SerializeStyle(StyleEntry style) + { + var parts = new List(); + if (style.Fill is { }) + parts.Add($"fill:{style.Fill}"); + if (style.Stroke is { }) + parts.Add($"stroke:{style.Stroke}"); + parts.Add($"opacity:{style.Opacity.ToString(CultureInfo.InvariantCulture)}"); + return string.Join(';', parts); + } + + public void AddOrUpdateStyle(SvgDocument document, StyleEntry style) + { + var data = SerializeStyle(style); + document.CustomAttributes[Prefix + style.Name] = data; + var existing = Styles.FirstOrDefault(s => s.Name == style.Name); + if (existing is { }) + { + existing.Fill = style.Fill; + existing.Stroke = style.Stroke; + existing.Opacity = style.Opacity; + } + else + { + Styles.Add(style); + } + } + + public void RemoveStyle(SvgDocument document, StyleEntry style) + { + document.CustomAttributes.Remove(Prefix + style.Name); + Styles.Remove(style); + } +}