Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions samples/AvalonDraw/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
<MenuItem Header="Edit Content..." Click="EditContentMenuItem_Click"/>
<MenuItem Header="Create Symbol..." Click="CreateSymbolMenuItem_Click"/>
<MenuItem Header="Insert Symbol" Click="SymbolToolMenuItem_Click"/>
<MenuItem Header="Create Style" Click="CreateStyleMenuItem_Click"/>
<MenuItem Header="Update Style" Click="UpdateStyleMenuItem_Click"/>
<MenuItem Header="Delete Style" Click="DeleteStyleMenuItem_Click"/>
<MenuItem Header="Reset File" Click="ResetFileMenuItem_Click" InputGesture="Ctrl+R"/>
<Separator />
<MenuItem Header="Undo" Click="UndoMenuItem_Click" InputGesture="Ctrl+Z"/>
Expand Down Expand Up @@ -204,6 +207,10 @@
<ListBox x:Name="BrushList" Margin="4" Height="80"
ItemsSource="{Binding BrushStyles}" SelectionChanged="BrushList_OnSelectionChanged"/>
</TabItem>
<TabItem Header="Styles">
<ListBox x:Name="StyleList" Margin="4" Height="80"
ItemsSource="{Binding Styles}" SelectionChanged="StyleList_OnSelectionChanged"/>
</TabItem>
<TabItem Header="Layers">
<DockPanel>
<StackPanel Orientation="Horizontal" Margin="4" DockPanel.Dock="Top">
Expand Down
73 changes: 73 additions & 0 deletions samples/AvalonDraw/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PropertyEntry> Properties => _propertiesService.Properties;
public ObservableCollection<PropertyEntry> FilteredProperties => _propertiesService.FilteredProperties;
private ObservableCollection<SvgNode> Nodes { get; } = new();
Expand All @@ -64,14 +65,17 @@ private struct DragInfo
public ObservableCollection<LayerService.LayerEntry> Layers => _layerService.Layers;
public ObservableCollection<PatternService.PatternEntry> Patterns => _patternService.Patterns;
public ObservableCollection<BrushService.BrushEntry> BrushStyles => _brushService.Brushes;
public ObservableCollection<AppearanceService.StyleEntry> 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<string> _expandedIds = new();
private HashSet<string> _filterBackup = new();

Expand Down Expand Up @@ -350,6 +354,7 @@ public MainWindow()
_layerTree = this.FindControl<TreeView>("LayerTree");
_swatchList = this.FindControl<ListBox>("SwatchList");
_brushList = this.FindControl<ListBox>("BrushList");
_styleList = this.FindControl<ListBox>("StyleList");
_strokeWidthBox = this.FindControl<TextBox>("StrokeWidthBox");
if (_strokeWidthBox is { })
_strokeWidthBox.Text = _toolService.CurrentStrokeWidth.ToString(System.Globalization.CultureInfo.InvariantCulture);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1554,6 +1563,7 @@ private void BuildTree()
UpdateLayers();
UpdatePatterns();
UpdateBrushes();
UpdateStyles();
}

private void ApplyPropertyFilter()
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<string?>(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();
Expand Down
159 changes: 159 additions & 0 deletions samples/AvalonDraw/Services/AppearanceService.cs
Original file line number Diff line number Diff line change
@@ -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<StyleEntry> 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<string>();
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);
}
}
Loading