committed by
GitHub
157 changed files with 4934 additions and 858 deletions
@ -1,19 +1,12 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Controls; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class RelativePanelPage : UserControl |
|||
public partial class RelativePanelPage : UserControl |
|||
{ |
|||
public RelativePanelPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,7 @@ |
|||
<Application xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Generators.Sandbox.App"> |
|||
<Application.Styles> |
|||
<FluentTheme /> |
|||
</Application.Styles> |
|||
</Application> |
|||
@ -0,0 +1,20 @@ |
|||
using Avalonia; |
|||
using Avalonia.Markup.Xaml; |
|||
using Generators.Sandbox.ViewModels; |
|||
|
|||
namespace Generators.Sandbox; |
|||
|
|||
public class App : Application |
|||
{ |
|||
public override void Initialize() => AvaloniaXamlLoader.Load(this); |
|||
|
|||
public override void OnFrameworkInitializationCompleted() |
|||
{ |
|||
var view = new Views.SignUpView |
|||
{ |
|||
ViewModel = new SignUpViewModel() |
|||
}; |
|||
view.Show(); |
|||
base.OnFrameworkInitializationCompleted(); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Generators.Sandbox.Controls; |
|||
|
|||
public class CustomTextBox : TextBox, IStyleable |
|||
{ |
|||
Type IStyleable.StyleKey => typeof(TextBox); |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:controls="using:Generators.Sandbox.Controls" |
|||
x:Class="Generators.Sandbox.Controls.SignUpView"> |
|||
<StackPanel> |
|||
<controls:CustomTextBox Margin="0 10 0 0" |
|||
x:Name="UserNameTextBox" |
|||
Watermark="Please, enter user name..." |
|||
UseFloatingWatermark="True" /> |
|||
<TextBlock x:Name="UserNameValidation" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
<TextBox Margin="0 10 0 0" |
|||
x:Name="PasswordTextBox" |
|||
Watermark="Please, enter your password..." |
|||
UseFloatingWatermark="True" |
|||
PasswordChar="*" /> |
|||
<TextBlock x:Name="PasswordValidation" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
<TextBox Margin="0 10 0 0" |
|||
x:Name="ConfirmPasswordTextBox" |
|||
Watermark="Please, confirm the password..." |
|||
UseFloatingWatermark="True" |
|||
PasswordChar="*" /> |
|||
<TextBlock x:Name="ConfirmPasswordValidation" |
|||
TextWrapping="Wrap" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
<TextBlock> |
|||
<TextBlock.Inlines> |
|||
<InlineCollection> |
|||
<Run x:Name="SignUpButtonDescription" /> |
|||
</InlineCollection> |
|||
</TextBlock.Inlines> |
|||
</TextBlock> |
|||
<Button Margin="0 10 0 5" |
|||
Content="Sign up" |
|||
x:Name="SignUpButton" /> |
|||
<TextBlock x:Name="CompoundValidation" |
|||
TextWrapping="Wrap" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,54 @@ |
|||
using System; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.ReactiveUI; |
|||
using Generators.Sandbox.ViewModels; |
|||
using ReactiveUI; |
|||
using ReactiveUI.Validation.Extensions; |
|||
using ReactiveUI.Validation.Formatters; |
|||
|
|||
namespace Generators.Sandbox.Controls; |
|||
|
|||
/// <summary>
|
|||
/// This is a sample view class with typed x:Name references generated using
|
|||
/// .NET 5 source generators. The class has to be partial because x:Name
|
|||
/// references are living in a separate partial class file. See also:
|
|||
/// https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/
|
|||
/// </summary>
|
|||
public partial class SignUpView : ReactiveUserControl<SignUpViewModel> |
|||
{ |
|||
public SignUpView() |
|||
{ |
|||
// The InitializeComponent method is also generated automatically
|
|||
// and lives in the autogenerated part of the partial class.
|
|||
InitializeComponent(); |
|||
this.WhenActivated(disposables => |
|||
{ |
|||
this.Bind(ViewModel, x => x.UserName, x => x.UserNameTextBox.Text) |
|||
.DisposeWith(disposables); |
|||
this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text) |
|||
.DisposeWith(disposables); |
|||
this.Bind(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordTextBox.Text) |
|||
.DisposeWith(disposables); |
|||
this.BindCommand(ViewModel, x => x.SignUp, x => x.SignUpButton) |
|||
.DisposeWith(disposables); |
|||
|
|||
this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text) |
|||
.DisposeWith(disposables); |
|||
this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text) |
|||
.DisposeWith(disposables); |
|||
this.BindValidation(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordValidation.Text) |
|||
.DisposeWith(disposables); |
|||
|
|||
var newLineFormatter = new SingleLineFormatter(Environment.NewLine); |
|||
this.BindValidation(ViewModel, x => x.CompoundValidation.Text, newLineFormatter) |
|||
.DisposeWith(disposables); |
|||
|
|||
// The references to text boxes below are also auto generated.
|
|||
// Use Ctrl+Click in order to view the generated sources.
|
|||
UserNameTextBox.Text = "Joseph!"; |
|||
PasswordTextBox.Text = "1234"; |
|||
ConfirmPasswordTextBox.Text = "1234"; |
|||
SignUpButtonDescription.Text = "Press the button below to sign up."; |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<AvaloniaResource Include="**\*.xaml"/> |
|||
<!-- Note this AdditionalFiles directive. --> |
|||
<AdditionalFiles Include="**\*.xaml"/> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="ReactiveUI.Validation" Version="3.0.22"/> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj"/> |
|||
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj"/> |
|||
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj"/> |
|||
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj"/> |
|||
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj"/> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\build\BuildTargets.targets"/> |
|||
<Import Project="..\..\build\SourceGenerators.props"/> |
|||
</Project> |
|||
@ -0,0 +1,15 @@ |
|||
using Avalonia; |
|||
using Avalonia.ReactiveUI; |
|||
|
|||
namespace Generators.Sandbox; |
|||
|
|||
internal static class Program |
|||
{ |
|||
public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); |
|||
|
|||
private static AppBuilder BuildAvaloniaApp() |
|||
=> AppBuilder.Configure<App>() |
|||
.UseReactiveUI() |
|||
.UsePlatformDetect() |
|||
.LogToTrace(); |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
using System.Reactive; |
|||
using ReactiveUI; |
|||
using ReactiveUI.Validation.Extensions; |
|||
using ReactiveUI.Validation.Helpers; |
|||
|
|||
namespace Generators.Sandbox.ViewModels; |
|||
|
|||
public class SignUpViewModel : ReactiveValidationObject |
|||
{ |
|||
private string _userName = string.Empty; |
|||
private string _password = string.Empty; |
|||
private string _confirmPassword = string.Empty; |
|||
|
|||
public SignUpViewModel() |
|||
{ |
|||
this.ValidationRule( |
|||
vm => vm.UserName, |
|||
name => !string.IsNullOrWhiteSpace(name), |
|||
"UserName is required."); |
|||
|
|||
this.ValidationRule( |
|||
vm => vm.Password, |
|||
password => !string.IsNullOrWhiteSpace(password), |
|||
"Password is required."); |
|||
|
|||
this.ValidationRule( |
|||
vm => vm.Password, |
|||
password => password?.Length > 2, |
|||
password => $"Password should be longer, current length: {password.Length}"); |
|||
|
|||
this.ValidationRule( |
|||
vm => vm.ConfirmPassword, |
|||
confirmation => !string.IsNullOrWhiteSpace(confirmation), |
|||
"Confirm password field is required."); |
|||
|
|||
var passwordsObservable = |
|||
this.WhenAnyValue( |
|||
x => x.Password, |
|||
x => x.ConfirmPassword, |
|||
(password, confirmation) => |
|||
password == confirmation); |
|||
|
|||
this.ValidationRule( |
|||
vm => vm.ConfirmPassword, |
|||
passwordsObservable, |
|||
"Passwords must match."); |
|||
|
|||
SignUp = ReactiveCommand.Create(() => {}, this.IsValid()); |
|||
} |
|||
|
|||
public ReactiveCommand<Unit, Unit> SignUp { get; } |
|||
|
|||
public string UserName |
|||
{ |
|||
get => _userName; |
|||
set => this.RaiseAndSetIfChanged(ref _userName, value); |
|||
} |
|||
|
|||
public string Password |
|||
{ |
|||
get => _password; |
|||
set => this.RaiseAndSetIfChanged(ref _password, value); |
|||
} |
|||
|
|||
public string ConfirmPassword |
|||
{ |
|||
get => _confirmPassword; |
|||
set => this.RaiseAndSetIfChanged(ref _confirmPassword, value); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:controls="using:Generators.Sandbox.Controls" |
|||
x:Class="Generators.Sandbox.Views.SignUpView"> |
|||
<StackPanel Margin="10"> |
|||
<TextBlock Text="Sign Up" /> |
|||
<controls:SignUpView x:Name="SignUpControl" /> |
|||
</StackPanel> |
|||
</Window> |
|||
@ -0,0 +1,28 @@ |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.ReactiveUI; |
|||
using Generators.Sandbox.ViewModels; |
|||
using ReactiveUI; |
|||
|
|||
namespace Generators.Sandbox.Views; |
|||
|
|||
/// <summary>
|
|||
/// This is a sample view class with typed x:Name references generated using
|
|||
/// .NET 5 source generators. The class has to be partial because x:Name
|
|||
/// references are living in a separate partial class file. See also:
|
|||
/// https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/
|
|||
/// </summary>
|
|||
public partial class SignUpView : ReactiveWindow<SignUpViewModel> |
|||
{ |
|||
public SignUpView() |
|||
{ |
|||
// The InitializeComponent method is also generated automatically
|
|||
// and lives in the autogenerated part of the partial class.
|
|||
InitializeComponent(); |
|||
this.WhenActivated(disposables => |
|||
{ |
|||
this.WhenAnyValue(view => view.ViewModel) |
|||
.BindTo(this, view => view.SignUpControl.ViewModel) |
|||
.DisposeWith(disposables); |
|||
}); |
|||
} |
|||
} |
|||
@ -1,41 +1,48 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:integrationTestApp="clr-namespace:IntegrationTestApp" |
|||
x:Class="IntegrationTestApp.ShowWindowTest" |
|||
Name="SecondaryWindow" |
|||
x:DataType="Window" |
|||
Title="Show Window Test"> |
|||
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> |
|||
<Label Grid.Column="0" Grid.Row="1">Client Size</Label> |
|||
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True" |
|||
Text="{Binding ClientSize, Mode=OneWay}"/> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="2">Frame Size</Label> |
|||
<TextBox Name="CurrentFrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True" |
|||
Text="{Binding FrameSize, Mode=OneWay}"/> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="3">Position</Label> |
|||
<TextBox Name="CurrentPosition" Grid.Column="1" Grid.Row="3" IsReadOnly="True"/> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="4">Owner Rect</Label> |
|||
<TextBox Name="CurrentOwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True"/> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="5">Screen Rect</Label> |
|||
<TextBox Name="CurrentScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True"/> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="6">Scaling</Label> |
|||
<TextBox Name="CurrentScaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True"/> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="7">WindowState</Label> |
|||
<ComboBox Name="CurrentWindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}"> |
|||
<ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem> |
|||
<ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem> |
|||
<ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem> |
|||
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem> |
|||
</ComboBox> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label> |
|||
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True"/> |
|||
|
|||
<Button Name="HideButton" Grid.Row="9" Command="{Binding $parent[Window].Hide}">Hide</Button> |
|||
</Grid> |
|||
<integrationTestApp:MeasureBorder Name="MyBorder"> |
|||
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> |
|||
<Label Grid.Column="0" Grid.Row="1">Client Size</Label> |
|||
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True" |
|||
Text="{Binding ClientSize, Mode=OneWay}" /> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="2">Frame Size</Label> |
|||
<TextBox Name="CurrentFrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True" |
|||
Text="{Binding FrameSize, Mode=OneWay}" /> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="3">Position</Label> |
|||
<TextBox Name="CurrentPosition" Grid.Column="1" Grid.Row="3" IsReadOnly="True" /> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="4">Owner Rect</Label> |
|||
<TextBox Name="CurrentOwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True" /> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="5">Screen Rect</Label> |
|||
<TextBox Name="CurrentScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True" /> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="6">Scaling</Label> |
|||
<TextBox Name="CurrentScaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True" /> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="7">WindowState</Label> |
|||
<ComboBox Name="CurrentWindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}"> |
|||
<ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem> |
|||
<ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem> |
|||
<ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem> |
|||
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem> |
|||
</ComboBox> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label> |
|||
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True" /> |
|||
|
|||
<Label Grid.Row="9" Content="MeasuredWith:" /> |
|||
<TextBlock Grid.Column="1" Grid.Row="9" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" /> |
|||
|
|||
<Button Name="HideButton" Grid.Row="10" Command="{Binding $parent[Window].Hide}">Hide</Button> |
|||
|
|||
</Grid> |
|||
</integrationTestApp:MeasureBorder> |
|||
</Window> |
|||
|
|||
@ -0,0 +1,290 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Globalization; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Media.Fonts |
|||
{ |
|||
public class EmbeddedFontCollection : IFontCollection |
|||
{ |
|||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new(); |
|||
|
|||
private readonly List<FontFamily> _fontFamilies = new List<FontFamily>(1); |
|||
|
|||
private readonly Uri _key; |
|||
|
|||
private readonly Uri _source; |
|||
|
|||
public EmbeddedFontCollection(Uri key, Uri source) |
|||
{ |
|||
_key = key; |
|||
|
|||
_source = source; |
|||
} |
|||
|
|||
public Uri Key => _key; |
|||
|
|||
public FontFamily this[int index] => _fontFamilies[index]; |
|||
|
|||
public int Count => _fontFamilies.Count; |
|||
|
|||
public void Initialize(IFontManagerImpl fontManager) |
|||
{ |
|||
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>(); |
|||
|
|||
var fontAssets = FontFamilyLoader.LoadFontAssets(_source); |
|||
|
|||
foreach (var fontAsset in fontAssets) |
|||
{ |
|||
var stream = assetLoader.Open(fontAsset); |
|||
|
|||
if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface)) |
|||
{ |
|||
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) |
|||
{ |
|||
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>(); |
|||
|
|||
if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces)) |
|||
{ |
|||
_fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName)); |
|||
} |
|||
} |
|||
|
|||
var key = new FontCollectionKey( |
|||
glyphTypeface.Style, |
|||
glyphTypeface.Weight, |
|||
glyphTypeface.Stretch); |
|||
|
|||
glyphTypefaces.TryAdd(key, glyphTypeface); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
foreach (var fontFamily in _fontFamilies) |
|||
{ |
|||
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces)) |
|||
{ |
|||
foreach (var glyphTypeface in glyphTypefaces.Values) |
|||
{ |
|||
glyphTypeface.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
public IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator(); |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
|
|||
public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, |
|||
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) |
|||
{ |
|||
var key = new FontCollectionKey(style, weight, stretch); |
|||
|
|||
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) |
|||
{ |
|||
if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//Try to find a partially matching font
|
|||
for (var i = 0; i < Count; i++) |
|||
{ |
|||
var fontFamily = _fontFamilies[i]; |
|||
|
|||
if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture))) |
|||
{ |
|||
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) && |
|||
TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
glyphTypeface = null; |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private static bool TryGetNearestMatch( |
|||
ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces, |
|||
FontCollectionKey key, |
|||
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
if (key.Style != FontStyle.Normal) |
|||
{ |
|||
key = key with { Style = FontStyle.Normal }; |
|||
} |
|||
|
|||
if (key.Stretch != FontStretch.Normal) |
|||
{ |
|||
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
if (key.Weight != FontWeight.Normal) |
|||
{ |
|||
if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
key = key with { Stretch = FontStretch.Normal }; |
|||
} |
|||
|
|||
if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
//Take the first glyph typeface we can find.
|
|||
foreach (var typeface in glyphTypefaces.Values) |
|||
{ |
|||
glyphTypeface = typeface; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private static bool TryFindStretchFallback( |
|||
ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces, |
|||
FontCollectionKey key, |
|||
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) |
|||
{ |
|||
glyphTypeface = null; |
|||
|
|||
var stretch = (int)key.Stretch; |
|||
|
|||
if (stretch < 5) |
|||
{ |
|||
for (var i = 0; stretch + i < 9; i++) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
for (var i = 0; stretch - i > 1; i++) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private static bool TryFindWeightFallback( |
|||
ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces, |
|||
FontCollectionKey key, |
|||
[NotNullWhen(true)] out IGlyphTypeface? typeface) |
|||
{ |
|||
typeface = null; |
|||
var weight = (int)key.Weight; |
|||
|
|||
//If the target weight given is between 400 and 500 inclusive
|
|||
if (weight >= 400 && weight <= 500) |
|||
{ |
|||
//Look for available weights between the target and 500, in ascending order.
|
|||
for (var i = 0; weight + i <= 500; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//If no match is found, look for available weights less than the target, in descending order.
|
|||
for (var i = 0; weight - i >= 100; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//If no match is found, look for available weights greater than 500, in ascending order.
|
|||
for (var i = 0; weight + i <= 900; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
|
|||
if (weight < 400) |
|||
{ |
|||
for (var i = 0; weight - i >= 100; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//If no match is found, look for available weights less than the target, in descending order.
|
|||
for (var i = 0; weight + i <= 900; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
//If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
|
|||
if (weight > 500) |
|||
{ |
|||
for (var i = 0; weight + i <= 900; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//If no match is found, look for available weights less than the target, in descending order.
|
|||
for (var i = 0; weight - i >= 100; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
namespace Avalonia.Media.Fonts |
|||
{ |
|||
public readonly record struct FontCollectionKey(FontStyle Style, FontWeight Weight, FontStretch Stretch); |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Media.Fonts |
|||
{ |
|||
public interface IFontCollection : IReadOnlyList<FontFamily>, IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Get the font collection's key.
|
|||
/// </summary>
|
|||
Uri Key { get; } |
|||
|
|||
/// <summary>
|
|||
/// Initializes the font collection.
|
|||
/// </summary>
|
|||
/// <param name="fontManager">The font manager the collection is registered with.</param>
|
|||
void Initialize(IFontManagerImpl fontManager); |
|||
|
|||
/// <summary>
|
|||
/// Try to get a glyph typeface for given parameters.
|
|||
/// </summary>
|
|||
/// <param name="familyName">The family name.</param>
|
|||
/// <param name="style">The font style.</param>
|
|||
/// <param name="weight">The font weight.</param>
|
|||
/// <param name="stretch">The font stretch.</param>
|
|||
/// <param name="glyphTypeface">The glyph typeface.</param>
|
|||
/// <returns>Returns <c>true</c> if a glyph typface can be found; otherwise, <c>false</c></returns>
|
|||
bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, |
|||
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); |
|||
} |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Media.Fonts |
|||
{ |
|||
internal class SystemFontCollection : IFontCollection |
|||
{ |
|||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new(); |
|||
|
|||
private readonly FontManager _fontManager; |
|||
private readonly string[] _familyNames; |
|||
|
|||
public SystemFontCollection(FontManager fontManager) |
|||
{ |
|||
_fontManager = fontManager; |
|||
_familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames(); |
|||
} |
|||
|
|||
public Uri Key => FontManager.SystemFontsKey; |
|||
|
|||
public FontFamily this[int index] |
|||
{ |
|||
get |
|||
{ |
|||
var familyName = _familyNames[index]; |
|||
|
|||
return new FontFamily(familyName); |
|||
} |
|||
} |
|||
|
|||
public int Count => _familyNames.Length; |
|||
|
|||
public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, |
|||
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) |
|||
{ |
|||
if (familyName == FontFamily.DefaultFontFamilyName) |
|||
{ |
|||
familyName = _fontManager.DefaultFontFamilyName; |
|||
} |
|||
|
|||
var key = new FontCollectionKey(style, weight, stretch); |
|||
|
|||
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
else |
|||
{ |
|||
if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) && |
|||
glyphTypefaces.TryAdd(key, glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) |
|||
{ |
|||
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>(); |
|||
|
|||
if (glyphTypefaces.TryAdd(key, glyphTypeface) && _glyphTypefaceCache.TryAdd(familyName, glyphTypefaces)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public void Initialize(IFontManagerImpl fontManager) |
|||
{ |
|||
//We initialize the system font collection during construction.
|
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
|
|||
public IEnumerator<FontFamily> GetEnumerator() |
|||
{ |
|||
foreach (var familyName in _familyNames) |
|||
{ |
|||
yield return new FontFamily(familyName); |
|||
} |
|||
} |
|||
|
|||
void IDisposable.Dispose() |
|||
{ |
|||
foreach (var glyphTypefaces in _glyphTypefaceCache.Values) |
|||
{ |
|||
foreach (var pair in glyphTypefaces) |
|||
{ |
|||
pair.Value.Dispose(); |
|||
} |
|||
} |
|||
|
|||
GC.SuppressFinalize(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
using System.Globalization; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Input; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Immutable; |
|||
|
|||
namespace Avalonia.Diagnostics.Controls |
|||
{ |
|||
internal sealed class BrushEditor : Control |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="Brush" /> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<BrushEditor, IBrush?> BrushProperty = |
|||
AvaloniaProperty.RegisterDirect<BrushEditor, IBrush?>( |
|||
nameof(Brush), o => o.Brush, (o, v) => o.Brush = v); |
|||
|
|||
private IBrush? _brush; |
|||
|
|||
public IBrush? Brush |
|||
{ |
|||
get => _brush; |
|||
set => SetAndRaise(BrushProperty, ref _brush, value); |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == BrushProperty) |
|||
{ |
|||
switch (Brush) |
|||
{ |
|||
case ISolidColorBrush scb: |
|||
{ |
|||
var colorView = new ColorView { Color = scb.Color }; |
|||
|
|||
colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor); |
|||
|
|||
FlyoutBase.SetAttachedFlyout(this, new Flyout { Content = colorView }); |
|||
ToolTip.SetTip(this, $"{scb.Color} ({Brush.GetType().Name})"); |
|||
|
|||
break; |
|||
} |
|||
|
|||
default: |
|||
|
|||
FlyoutBase.SetAttachedFlyout(this, null); |
|||
ToolTip.SetTip(this, Brush?.GetType().Name ?? "(null)"); |
|||
|
|||
break; |
|||
} |
|||
|
|||
InvalidateVisual(); |
|||
} |
|||
} |
|||
|
|||
protected override void OnPointerPressed(PointerPressedEventArgs e) |
|||
{ |
|||
base.OnPointerPressed(e); |
|||
|
|||
FlyoutBase.ShowAttachedFlyout(this); |
|||
} |
|||
|
|||
public override void Render(DrawingContext context) |
|||
{ |
|||
base.Render(context); |
|||
|
|||
if (Brush != null) |
|||
{ |
|||
context.FillRectangle(Brush, Bounds); |
|||
} |
|||
else |
|||
{ |
|||
context.FillRectangle(Brushes.Black, Bounds); |
|||
|
|||
var ft = new FormattedText("(null)", |
|||
CultureInfo.CurrentCulture, |
|||
FlowDirection.LeftToRight, |
|||
Typeface.Default, |
|||
10, |
|||
Brushes.White); |
|||
|
|||
context.DrawText(ft, |
|||
new Point(Bounds.Width / 2 - ft.Width / 2, Bounds.Height / 2 - ft.Height / 2)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.Diagnostics.Controls |
|||
{ |
|||
//TODO: UpdateSourceTrigger & Binding.ValidationRules could help removing the need for this control.
|
|||
internal sealed class CommitTextBox : TextBox, IStyleable |
|||
{ |
|||
Type IStyleable.StyleKey => typeof(TextBox); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="CommittedText" /> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<CommitTextBox, string?> CommittedTextProperty = |
|||
AvaloniaProperty.RegisterDirect<CommitTextBox, string?>( |
|||
nameof(CommittedText), o => o.CommittedText, (o, v) => o.CommittedText = v); |
|||
|
|||
private string? _committedText; |
|||
|
|||
public string? CommittedText |
|||
{ |
|||
get => _committedText; |
|||
set => SetAndRaise(CommittedTextProperty, ref _committedText, value); |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == CommittedTextProperty) |
|||
{ |
|||
Text = CommittedText; |
|||
} |
|||
} |
|||
|
|||
protected override void OnKeyUp(KeyEventArgs e) |
|||
{ |
|||
base.OnKeyUp(e); |
|||
|
|||
switch (e.Key) |
|||
{ |
|||
case Key.Enter: |
|||
|
|||
TryCommit(); |
|||
|
|||
e.Handled = true; |
|||
|
|||
break; |
|||
|
|||
case Key.Escape: |
|||
|
|||
Cancel(); |
|||
|
|||
e.Handled = true; |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
protected override void OnLostFocus(RoutedEventArgs e) |
|||
{ |
|||
base.OnLostFocus(e); |
|||
|
|||
TryCommit(); |
|||
} |
|||
|
|||
private void Cancel() |
|||
{ |
|||
Text = CommittedText; |
|||
DataValidationErrors.ClearErrors(this); |
|||
} |
|||
|
|||
private void TryCommit() |
|||
{ |
|||
if (!DataValidationErrors.GetHasErrors(this)) |
|||
{ |
|||
CommittedText = Text; |
|||
} |
|||
else |
|||
{ |
|||
Text = CommittedText; |
|||
DataValidationErrors.ClearErrors(this); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Linq.Expressions; |
|||
using System.Reflection; |
|||
using Avalonia.Reactive; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal static class ReactiveExtensions |
|||
{ |
|||
public static IObservable<TValue> GetObservable<TOwner, TValue>( |
|||
this TOwner vm, |
|||
Expression<Func<TOwner, TValue>> property, |
|||
bool fireImmediately = true) |
|||
where TOwner : INotifyPropertyChanged |
|||
{ |
|||
return Observable.Create<TValue>(o => |
|||
{ |
|||
var propertyInfo = GetPropertyInfo(property); |
|||
|
|||
void Fire() |
|||
{ |
|||
o.OnNext((TValue) propertyInfo.GetValue(vm)!); |
|||
} |
|||
|
|||
void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) |
|||
{ |
|||
if (e.PropertyName == propertyInfo.Name) |
|||
{ |
|||
Fire(); |
|||
} |
|||
} |
|||
|
|||
if (fireImmediately) |
|||
{ |
|||
Fire(); |
|||
} |
|||
|
|||
vm.PropertyChanged += OnPropertyChanged; |
|||
|
|||
return Disposable.Create(() => vm.PropertyChanged -= OnPropertyChanged); |
|||
}); |
|||
} |
|||
|
|||
private static PropertyInfo GetPropertyInfo<TOwner, TValue>(this Expression<Func<TOwner, TValue>> property) |
|||
{ |
|||
if (property.Body is UnaryExpression unaryExpression) |
|||
{ |
|||
return (PropertyInfo)((MemberExpression)unaryExpression.Operand).Member; |
|||
} |
|||
|
|||
var memExpr = (MemberExpression)property.Body; |
|||
|
|||
return (PropertyInfo)memExpr.Member; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,404 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Globalization; |
|||
using System.Reflection; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.Shapes; |
|||
using Avalonia.Data; |
|||
using Avalonia.Data.Converters; |
|||
using Avalonia.Diagnostics.Controls; |
|||
using Avalonia.Diagnostics.ViewModels; |
|||
using Avalonia.Input; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Markup.Xaml.Converters; |
|||
using Avalonia.Media; |
|||
using Avalonia.Reactive; |
|||
|
|||
namespace Avalonia.Diagnostics.Views |
|||
{ |
|||
internal class PropertyValueEditorView : UserControl |
|||
{ |
|||
private static readonly Geometry ImageIcon = Geometry.Parse( |
|||
"M12.25 6C8.79822 6 6 8.79822 6 12.25V35.75C6 37.1059 6.43174 38.3609 7.16525 39.3851L21.5252 25.0251C22.8921 23.6583 25.1081 23.6583 26.475 25.0251L40.8348 39.385C41.5683 38.3608 42 37.1058 42 35.75V12.25C42 8.79822 39.2018 6 35.75 6H12.25ZM34.5 17.5C34.5 19.7091 32.7091 21.5 30.5 21.5C28.2909 21.5 26.5 19.7091 26.5 17.5C26.5 15.2909 28.2909 13.5 30.5 13.5C32.7091 13.5 34.5 15.2909 34.5 17.5ZM39.0024 41.0881L24.7072 26.7929C24.3167 26.4024 23.6835 26.4024 23.293 26.7929L8.99769 41.0882C9.94516 41.6667 11.0587 42 12.25 42H35.75C36.9414 42 38.0549 41.6666 39.0024 41.0881Z"); |
|||
|
|||
private static readonly Geometry GeometryIcon = Geometry.Parse( |
|||
"M23.25 15.5H30.8529C29.8865 8.99258 24.2763 4 17.5 4C10.0442 4 4 10.0442 4 17.5C4 24.2763 8.99258 29.8865 15.5 30.8529V23.25C15.5 18.9698 18.9698 15.5 23.25 15.5ZM23.25 18C20.3505 18 18 20.3505 18 23.25V38.75C18 41.6495 20.3505 44 23.25 44H38.75C41.6495 44 44 41.6495 44 38.75V23.25C44 20.3505 41.6495 18 38.75 18H23.25Z"); |
|||
|
|||
private static readonly ColorToBrushConverter Color2Brush = new(); |
|||
|
|||
private readonly CompositeDisposable _cleanup = new(); |
|||
private PropertyViewModel? Property => (PropertyViewModel?)DataContext; |
|||
|
|||
protected override void OnDataContextChanged(EventArgs e) |
|||
{ |
|||
base.OnDataContextChanged(e); |
|||
|
|||
Content = UpdateControl(); |
|||
} |
|||
|
|||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
base.OnDetachedFromVisualTree(e); |
|||
|
|||
_cleanup.Clear(); |
|||
} |
|||
|
|||
private static bool ImplementsInterface<TInterface>(Type type) |
|||
{ |
|||
var interfaceType = typeof(TInterface); |
|||
return type == interfaceType || interfaceType.IsAssignableFrom(type); |
|||
} |
|||
|
|||
private Control? UpdateControl() |
|||
{ |
|||
_cleanup.Clear(); |
|||
|
|||
if (Property?.PropertyType is not { } propertyType) |
|||
return null; |
|||
|
|||
TControl CreateControl<TControl>(AvaloniaProperty valueProperty, |
|||
IValueConverter? converter = null, |
|||
Action<TControl>? init = null, |
|||
AvaloniaProperty? readonlyProperty = null) |
|||
where TControl : Control, new() |
|||
{ |
|||
var control = new TControl(); |
|||
|
|||
init?.Invoke(control); |
|||
|
|||
control.Bind(valueProperty, |
|||
new Binding(nameof(Property.Value), BindingMode.TwoWay) |
|||
{ |
|||
Source = Property, |
|||
Converter = converter ?? new ValueConverter(), |
|||
ConverterParameter = propertyType |
|||
}).DisposeWith(_cleanup); |
|||
|
|||
if (readonlyProperty != null) |
|||
{ |
|||
control[readonlyProperty] = Property.IsReadonly; |
|||
} |
|||
else |
|||
{ |
|||
control.IsEnabled = !Property.IsReadonly; |
|||
} |
|||
|
|||
return control; |
|||
} |
|||
|
|||
if (propertyType == typeof(bool)) |
|||
return CreateControl<CheckBox>(ToggleButton.IsCheckedProperty); |
|||
|
|||
//TODO: Infinity, NaN not working with NumericUpDown
|
|||
if (propertyType.IsPrimitive && propertyType != typeof(float) && propertyType != typeof(double)) |
|||
return CreateControl<NumericUpDown>( |
|||
NumericUpDown.ValueProperty, |
|||
new ValueToDecimalConverter(), |
|||
init: n => |
|||
{ |
|||
n.Increment = 1; |
|||
n.NumberFormat = new NumberFormatInfo { NumberDecimalDigits = 0 }; |
|||
n.ParsingNumberStyle = NumberStyles.Integer; |
|||
}, |
|||
readonlyProperty: NumericUpDown.IsReadOnlyProperty); |
|||
|
|||
if (propertyType == typeof(Color)) |
|||
{ |
|||
var el = new Ellipse { Width = 12, Height = 12, VerticalAlignment = VerticalAlignment.Center }; |
|||
|
|||
el.Bind( |
|||
Shape.FillProperty, |
|||
new Binding(nameof(Property.Value)) { Source = Property, Converter = Color2Brush }) |
|||
.DisposeWith(_cleanup); |
|||
|
|||
var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; |
|||
|
|||
tbl.Bind( |
|||
TextBlock.TextProperty, |
|||
new Binding(nameof(Property.Value)) { Source = Property }) |
|||
.DisposeWith(_cleanup); |
|||
|
|||
var sp = new StackPanel |
|||
{ |
|||
Orientation = Orientation.Horizontal, |
|||
Spacing = 2, |
|||
Children = { el, tbl }, |
|||
Background = Brushes.Transparent, |
|||
Cursor = new Cursor(StandardCursorType.Hand), |
|||
IsEnabled = !Property.IsReadonly |
|||
}; |
|||
|
|||
var cv = new ColorView(); |
|||
|
|||
cv.Bind( |
|||
ColorView.ColorProperty, |
|||
new Binding(nameof(Property.Value), BindingMode.TwoWay) |
|||
{ |
|||
Source = Property, Converter = Color2Brush |
|||
}) |
|||
.DisposeWith(_cleanup); |
|||
|
|||
FlyoutBase.SetAttachedFlyout(sp, new Flyout { Content = cv }); |
|||
|
|||
sp.PointerPressed += (_, _) => FlyoutBase.ShowAttachedFlyout(sp); |
|||
|
|||
return sp; |
|||
} |
|||
|
|||
if (ImplementsInterface<IBrush>(propertyType)) |
|||
return CreateControl<BrushEditor>(BrushEditor.BrushProperty); |
|||
|
|||
var isImage = ImplementsInterface<IImage>(propertyType); |
|||
var isGeometry = propertyType == typeof(Geometry); |
|||
|
|||
if (isImage || isGeometry) |
|||
{ |
|||
var valueObservable = Property.GetObservable(x => x.Value); |
|||
var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; |
|||
|
|||
tbl.Bind(TextBlock.TextProperty, |
|||
valueObservable.Select( |
|||
value => value switch |
|||
{ |
|||
IImage img => $"{img.Size.Width} x {img.Size.Height}", |
|||
Geometry geom => $"{geom.Bounds.Width} x {geom.Bounds.Height}", |
|||
_ => "(null)" |
|||
})) |
|||
.DisposeWith(_cleanup); |
|||
|
|||
var sp = new StackPanel |
|||
{ |
|||
Background = Brushes.Transparent, |
|||
Orientation = Orientation.Horizontal, |
|||
Spacing = 2, |
|||
Children = |
|||
{ |
|||
new Path |
|||
{ |
|||
Data = isImage ? ImageIcon : GeometryIcon, |
|||
Fill = Brushes.Gray, |
|||
Width = 12, |
|||
Height = 12, |
|||
Stretch = Stretch.Uniform, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
}, |
|||
tbl |
|||
} |
|||
}; |
|||
|
|||
if (isImage) |
|||
{ |
|||
var previewImage = new Image { Stretch = Stretch.Uniform, Width = 300, Height = 300 }; |
|||
|
|||
previewImage |
|||
.Bind(Image.SourceProperty, valueObservable) |
|||
.DisposeWith(_cleanup); |
|||
|
|||
ToolTip.SetTip(sp, previewImage); |
|||
} |
|||
else |
|||
{ |
|||
var previewShape = new Path |
|||
{ |
|||
Stretch = Stretch.Uniform, |
|||
Fill = Brushes.White, |
|||
VerticalAlignment = VerticalAlignment.Center, |
|||
HorizontalAlignment = HorizontalAlignment.Center |
|||
}; |
|||
|
|||
previewShape |
|||
.Bind(Path.DataProperty, valueObservable) |
|||
.DisposeWith(_cleanup); |
|||
|
|||
ToolTip.SetTip(sp, new Border { Child = previewShape, Width = 300, Height = 300 }); |
|||
} |
|||
|
|||
return sp; |
|||
} |
|||
|
|||
if (propertyType.IsEnum) |
|||
return CreateControl<ComboBox>( |
|||
SelectingItemsControl.SelectedItemProperty, init: c => |
|||
{ |
|||
c.Items = Enum.GetValues(propertyType); |
|||
}); |
|||
|
|||
var tb = CreateControl<CommitTextBox>( |
|||
CommitTextBox.CommittedTextProperty, |
|||
new TextToValueConverter(), |
|||
t => |
|||
{ |
|||
t.Watermark = "(null)"; |
|||
}, |
|||
readonlyProperty: TextBox.IsReadOnlyProperty); |
|||
|
|||
tb.IsReadOnly |= propertyType == typeof(object) || |
|||
!StringConversionHelper.CanConvertFromString(propertyType); |
|||
|
|||
if (!tb.IsReadOnly) |
|||
{ |
|||
tb.GetObservable(TextBox.TextProperty).Subscribe(t => |
|||
{ |
|||
try |
|||
{ |
|||
if (t != null) |
|||
{ |
|||
StringConversionHelper.FromString(t, propertyType); |
|||
} |
|||
|
|||
DataValidationErrors.ClearErrors(tb); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
DataValidationErrors.SetError(tb, ex.GetBaseException()); |
|||
} |
|||
}).DisposeWith(_cleanup); |
|||
} |
|||
|
|||
return tb; |
|||
} |
|||
|
|||
//HACK: ValueConverter that skips first target update
|
|||
//TODO: Would be nice to have some kind of "InitialBindingValue" option on TwoWay bindings to control
|
|||
//if the first value comes from the source or target
|
|||
private class ValueConverter : IValueConverter |
|||
{ |
|||
private bool _firstUpdate = true; |
|||
|
|||
object? IValueConverter.Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
return Convert(value, targetType, parameter, culture); |
|||
} |
|||
|
|||
object? IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
if (_firstUpdate) |
|||
{ |
|||
_firstUpdate = false; |
|||
|
|||
return BindingOperations.DoNothing; |
|||
} |
|||
|
|||
//Note: targetType provided by Converter is simply "object"
|
|||
return ConvertBack(value, (Type)parameter!, parameter, culture); |
|||
} |
|||
|
|||
protected virtual object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
return value; |
|||
} |
|||
|
|||
protected virtual object? ConvertBack(object? value, Type targetType, object? parameter, |
|||
CultureInfo culture) |
|||
{ |
|||
return value; |
|||
} |
|||
} |
|||
|
|||
private static class StringConversionHelper |
|||
{ |
|||
private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; |
|||
private static readonly Type[] StringParameter = { typeof(string) }; |
|||
private static readonly Type[] StringFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; |
|||
|
|||
public static bool CanConvertFromString(Type type) |
|||
{ |
|||
var converter = TypeDescriptor.GetConverter(type); |
|||
|
|||
if (converter.CanConvertFrom(typeof(string))) |
|||
return true; |
|||
|
|||
return GetParseMethod(type, out _) != null; |
|||
} |
|||
|
|||
public static string? ToString(object o) |
|||
{ |
|||
var converter = TypeDescriptor.GetConverter(o); |
|||
|
|||
//CollectionConverter does not deliver any important information. It just displays "(Collection)".
|
|||
if (!converter.CanConvertTo(typeof(string)) || |
|||
converter.GetType() == typeof(CollectionConverter)) |
|||
return o.ToString(); |
|||
|
|||
return converter.ConvertToInvariantString(o); |
|||
} |
|||
|
|||
public static object? FromString(string str, Type type) |
|||
{ |
|||
var converter = TypeDescriptor.GetConverter(type); |
|||
|
|||
return converter.CanConvertFrom(typeof(string)) ? |
|||
converter.ConvertFrom(null, CultureInfo.InvariantCulture, str) : |
|||
InvokeParse(str, type); |
|||
} |
|||
|
|||
private static object? InvokeParse(string s, Type targetType) |
|||
{ |
|||
var m = GetParseMethod(targetType, out var hasFormat); |
|||
|
|||
if (m == null) |
|||
throw new InvalidOperationException(); |
|||
|
|||
return m.Invoke(null, |
|||
hasFormat ? |
|||
new object[] { s, CultureInfo.InvariantCulture } : |
|||
new object[] { s }); |
|||
} |
|||
|
|||
private static MethodInfo? GetParseMethod(Type type, out bool hasFormat) |
|||
{ |
|||
var m = type.GetMethod("Parse", PublicStatic, null, StringFormatProviderParameters, null); |
|||
|
|||
if (m != null) |
|||
{ |
|||
hasFormat = true; |
|||
|
|||
return m; |
|||
} |
|||
|
|||
hasFormat = false; |
|||
|
|||
return type.GetMethod("Parse", PublicStatic, null, StringParameter, null); |
|||
} |
|||
} |
|||
|
|||
private sealed class ValueToDecimalConverter : ValueConverter |
|||
{ |
|||
protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
return System.Convert.ToDecimal(value); |
|||
} |
|||
|
|||
protected override object? ConvertBack(object? value, Type targetType, object? parameter, |
|||
CultureInfo culture) |
|||
{ |
|||
return System.Convert.ChangeType(value, targetType); |
|||
} |
|||
} |
|||
|
|||
private sealed class TextToValueConverter : ValueConverter |
|||
{ |
|||
protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
return value is null ? null : StringConversionHelper.ToString(value); |
|||
} |
|||
|
|||
protected override object? ConvertBack(object? value, Type targetType, object? parameter, |
|||
CultureInfo culture) |
|||
{ |
|||
if (value is not string s) |
|||
return null; |
|||
|
|||
try |
|||
{ |
|||
return StringConversionHelper.FromString(s, targetType); |
|||
} |
|||
catch |
|||
{ |
|||
return BindingOperations.DoNothing; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
namespace Avalonia.Fonts.Inter |
|||
{ |
|||
public static class AppBuilderExtension |
|||
{ |
|||
public static AppBuilder WithInterFont(this AppBuilder appBuilder) |
|||
{ |
|||
return appBuilder.ConfigureFonts(fontManager => |
|||
{ |
|||
fontManager.AddFontCollection(new InterFontCollection()); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
using Avalonia.Media.Fonts; |
|||
|
|||
namespace Avalonia.Fonts.Inter |
|||
{ |
|||
public sealed class InterFontCollection : EmbeddedFontCollection |
|||
{ |
|||
public InterFontCollection() : base( |
|||
new Uri("fonts:Inter", UriKind.Absolute), |
|||
new Uri("avares://Avalonia.Fonts.Inter/Assets", UriKind.Absolute)) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -1,198 +0,0 @@ |
|||
using System.Collections.Concurrent; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Media; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
internal class SKTypefaceCollection |
|||
{ |
|||
private readonly ConcurrentDictionary<Typeface, SKTypeface> _typefaces = new(); |
|||
|
|||
public void AddTypeface(Typeface key, SKTypeface typeface) |
|||
{ |
|||
_typefaces.TryAdd(key, typeface); |
|||
} |
|||
|
|||
public SKTypeface? Get(Typeface typeface) |
|||
{ |
|||
return GetNearestMatch(typeface); |
|||
} |
|||
|
|||
private SKTypeface? GetNearestMatch(Typeface key) |
|||
{ |
|||
if (_typefaces.Count == 0) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (_typefaces.TryGetValue(key, out var typeface)) |
|||
{ |
|||
return typeface; |
|||
} |
|||
|
|||
if(key.Style != FontStyle.Normal) |
|||
{ |
|||
key = new Typeface(key.FontFamily, FontStyle.Normal, key.Weight, key.Stretch); |
|||
} |
|||
|
|||
if(key.Stretch != FontStretch.Normal) |
|||
{ |
|||
if(TryFindStretchFallback(key, out typeface)) |
|||
{ |
|||
return typeface; |
|||
} |
|||
|
|||
if(key.Weight != FontWeight.Normal) |
|||
{ |
|||
if (TryFindStretchFallback(new Typeface(key.FontFamily, key.Style, FontWeight.Normal, key.Stretch), out typeface)) |
|||
{ |
|||
return typeface; |
|||
} |
|||
} |
|||
|
|||
key = new Typeface(key.FontFamily, key.Style, key.Weight, FontStretch.Normal); |
|||
} |
|||
|
|||
if(TryFindWeightFallback(key, out typeface)) |
|||
{ |
|||
return typeface; |
|||
} |
|||
|
|||
if (TryFindStretchFallback(key, out typeface)) |
|||
{ |
|||
return typeface; |
|||
} |
|||
|
|||
//Nothing was found so we try some regular typeface.
|
|||
if (_typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface)) |
|||
{ |
|||
return typeface; |
|||
} |
|||
|
|||
SKTypeface? skTypeface = null; |
|||
|
|||
foreach(var pair in _typefaces) |
|||
{ |
|||
skTypeface = pair.Value; |
|||
|
|||
if (skTypeface.FamilyName.Contains(key.FontFamily.Name)) |
|||
{ |
|||
return skTypeface; |
|||
} |
|||
} |
|||
|
|||
return skTypeface; |
|||
} |
|||
|
|||
private bool TryFindStretchFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface) |
|||
{ |
|||
typeface = null; |
|||
var stretch = (int)key.Stretch; |
|||
|
|||
if (stretch < 5) |
|||
{ |
|||
for (var i = 0; stretch + i < 9; i++) |
|||
{ |
|||
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch + i)), out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
for (var i = 0; stretch - i > 1; i++) |
|||
{ |
|||
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch - i)), out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private bool TryFindWeightFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface) |
|||
{ |
|||
typeface = null; |
|||
var weight = (int)key.Weight; |
|||
|
|||
//If the target weight given is between 400 and 500 inclusive
|
|||
if (weight >= 400 && weight <= 500) |
|||
{ |
|||
//Look for available weights between the target and 500, in ascending order.
|
|||
for (var i = 0; weight + i <= 500; i += 50) |
|||
{ |
|||
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//If no match is found, look for available weights less than the target, in descending order.
|
|||
for (var i = 0; weight - i >= 100; i += 50) |
|||
{ |
|||
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//If no match is found, look for available weights greater than 500, in ascending order.
|
|||
for (var i = 0; weight + i <= 900; i += 50) |
|||
{ |
|||
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
|
|||
if (weight < 400) |
|||
{ |
|||
for (var i = 0; weight - i >= 100; i += 50) |
|||
{ |
|||
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//If no match is found, look for available weights less than the target, in descending order.
|
|||
for (var i = 0; weight + i <= 900; i += 50) |
|||
{ |
|||
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
//If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
|
|||
if (weight > 500) |
|||
{ |
|||
for (var i = 0; weight + i <= 900; i += 50) |
|||
{ |
|||
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//If no match is found, look for available weights less than the target, in descending order.
|
|||
for (var i = 0; weight - i >= 100; i += 50) |
|||
{ |
|||
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -1,73 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Fonts; |
|||
using Avalonia.Platform; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
internal static class SKTypefaceCollectionCache |
|||
{ |
|||
private static readonly ConcurrentDictionary<FontFamily, SKTypefaceCollection> s_cachedCollections; |
|||
|
|||
static SKTypefaceCollectionCache() |
|||
{ |
|||
s_cachedCollections = new ConcurrentDictionary<FontFamily, SKTypefaceCollection>(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the or add typeface collection.
|
|||
/// </summary>
|
|||
/// <param name="fontFamily">The font family.</param>
|
|||
/// <returns></returns>
|
|||
public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily) |
|||
{ |
|||
return s_cachedCollections.GetOrAdd(fontFamily, CreateCustomFontCollection); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates the custom font collection.
|
|||
/// </summary>
|
|||
/// <param name="fontFamily">The font family.</param>
|
|||
/// <returns></returns>
|
|||
private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily) |
|||
{ |
|||
var typeFaceCollection = new SKTypefaceCollection(); |
|||
|
|||
if (fontFamily.Key is not { } fontFamilyKey) |
|||
{ |
|||
return typeFaceCollection; |
|||
} |
|||
|
|||
var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamilyKey); |
|||
|
|||
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>(); |
|||
|
|||
foreach (var asset in fontAssets) |
|||
{ |
|||
var assetStream = assetLoader.Open(asset); |
|||
|
|||
if (assetStream == null) |
|||
throw new InvalidOperationException("Asset could not be loaded."); |
|||
|
|||
var typeface = SKTypeface.FromStream(assetStream); |
|||
|
|||
if (typeface == null) |
|||
throw new InvalidOperationException("Typeface could not be loaded."); |
|||
|
|||
if (!typeface.FamilyName.Contains(fontFamily.Name)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(), |
|||
(FontWeight)typeface.FontWeight, (FontStretch)typeface.FontWidth); |
|||
|
|||
typeFaceCollection.AddTypeface(key, typeface); |
|||
} |
|||
|
|||
return typeFaceCollection; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<IncludeBuildOutput>false</IncludeBuildOutput> |
|||
<PackageId>Avalonia.Generators</PackageId> |
|||
<DefineConstants>$(DefineConstants);XAMLX_INTERNAL</DefineConstants> |
|||
<IsPackable>true</IsPackable> |
|||
<IsRoslynComponent>true</IsRoslynComponent> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" /> |
|||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Compile Link="Compiler\XamlX\filename" Include="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/*.cs" /> |
|||
<Compile Remove="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/SreTypeSystem.cs" /> |
|||
<Compile Include="..\..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<None Include="Avalonia.Generators.props" Pack="true" PackagePath="buildTransitive/$(PackageId).props" /> |
|||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup Label="InternalsVisibleTo"> |
|||
<InternalsVisibleTo Include="Avalonia.Generators.Tests, PublicKey=$(AvaloniaPublicKey)" /> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\..\build\TrimmingEnable.props" /> |
|||
</Project> |
|||
@ -0,0 +1,22 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<PropertyGroup> |
|||
<AvaloniaNameGeneratorIsEnabled Condition="'$(AvaloniaNameGeneratorIsEnabled)' == ''">true</AvaloniaNameGeneratorIsEnabled> |
|||
<AvaloniaNameGeneratorBehavior Condition="'$(AvaloniaNameGeneratorBehavior)' == ''">InitializeComponent</AvaloniaNameGeneratorBehavior> |
|||
<AvaloniaNameGeneratorDefaultFieldModifier Condition="'$(AvaloniaNameGeneratorDefaultFieldModifier)' == ''">internal</AvaloniaNameGeneratorDefaultFieldModifier> |
|||
<AvaloniaNameGeneratorFilterByPath Condition="'$(AvaloniaNameGeneratorFilterByPath)' == ''">*</AvaloniaNameGeneratorFilterByPath> |
|||
<AvaloniaNameGeneratorFilterByNamespace Condition="'$(AvaloniaNameGeneratorFilterByNamespace)' == ''">*</AvaloniaNameGeneratorFilterByNamespace> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceItemGroup"/> |
|||
<CompilerVisibleProperty Include="AvaloniaNameGeneratorIsEnabled" /> |
|||
<CompilerVisibleProperty Include="AvaloniaNameGeneratorBehavior" /> |
|||
<CompilerVisibleProperty Include="AvaloniaNameGeneratorDefaultFieldModifier" /> |
|||
<CompilerVisibleProperty Include="AvaloniaNameGeneratorFilterByPath" /> |
|||
<CompilerVisibleProperty Include="AvaloniaNameGeneratorFilterByNamespace" /> |
|||
</ItemGroup> |
|||
<Target Name="_InjectAdditionalFiles" BeforeTargets="GenerateMSBuildEditorConfigFileShouldRun"> |
|||
<ItemGroup> |
|||
<AdditionalFiles Include="@(AvaloniaXaml)" SourceItemGroup="AvaloniaXaml" /> |
|||
</ItemGroup> |
|||
</Target> |
|||
</Project> |
|||
@ -0,0 +1,9 @@ |
|||
using System.Collections.Generic; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Generators.Common.Domain; |
|||
|
|||
internal interface ICodeGenerator |
|||
{ |
|||
string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names); |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Avalonia.Generators.Common.Domain; |
|||
|
|||
internal interface IGlobPattern |
|||
{ |
|||
bool Matches(string str); |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System.Collections.Generic; |
|||
using XamlX.Ast; |
|||
|
|||
namespace Avalonia.Generators.Common.Domain; |
|||
|
|||
internal enum NamedFieldModifier |
|||
{ |
|||
Public = 0, |
|||
Private = 1, |
|||
Internal = 2, |
|||
Protected = 3, |
|||
} |
|||
|
|||
internal interface INameResolver |
|||
{ |
|||
IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml); |
|||
} |
|||
|
|||
internal record ResolvedName(string TypeName, string Name, string FieldModifier); |
|||
@ -0,0 +1,11 @@ |
|||
using XamlX.Ast; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Generators.Common.Domain; |
|||
|
|||
internal interface IViewResolver |
|||
{ |
|||
ResolvedView ResolveView(string xaml); |
|||
} |
|||
|
|||
internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml); |
|||
@ -0,0 +1,18 @@ |
|||
using System.Text.RegularExpressions; |
|||
using Avalonia.Generators.Common.Domain; |
|||
|
|||
namespace Avalonia.Generators.Common; |
|||
|
|||
internal class GlobPattern : IGlobPattern |
|||
{ |
|||
private const RegexOptions GlobOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline; |
|||
private readonly Regex _regex; |
|||
|
|||
public GlobPattern(string pattern) |
|||
{ |
|||
var expression = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".") + "$"; |
|||
_regex = new Regex(expression, GlobOptions); |
|||
} |
|||
|
|||
public bool Matches(string str) => _regex.IsMatch(str); |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Generators.Common.Domain; |
|||
|
|||
namespace Avalonia.Generators.Common; |
|||
|
|||
internal class GlobPatternGroup : IGlobPattern |
|||
{ |
|||
private readonly GlobPattern[] _patterns; |
|||
|
|||
public GlobPatternGroup(IEnumerable<string> patterns) => |
|||
_patterns = patterns |
|||
.Select(pattern => new GlobPattern(pattern)) |
|||
.ToArray(); |
|||
|
|||
public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str)); |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using System.Linq; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Generators.Common; |
|||
|
|||
internal static class ResolverExtensions |
|||
{ |
|||
public static bool IsAvaloniaStyledElement(this IXamlType clrType) => |
|||
clrType.HasStyledElementBaseType() || |
|||
clrType.HasIStyledElementInterface(); |
|||
|
|||
private static bool HasStyledElementBaseType(this IXamlType clrType) |
|||
{ |
|||
// Check for the base type since IStyledElement interface is removed.
|
|||
// https://github.com/AvaloniaUI/Avalonia/pull/9553
|
|||
if (clrType.FullName == "Avalonia.StyledElement") |
|||
return true; |
|||
return clrType.BaseType != null && IsAvaloniaStyledElement(clrType.BaseType); |
|||
} |
|||
|
|||
private static bool HasIStyledElementInterface(this IXamlType clrType) => |
|||
clrType.Interfaces.Any(abstraction => |
|||
abstraction.IsInterface && |
|||
abstraction.FullName == "Avalonia.IStyledElement"); |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
using System.Collections.Generic; |
|||
using System.Collections.Immutable; |
|||
using System.Linq; |
|||
using Avalonia.Generators.Common.Domain; |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
|
|||
namespace Avalonia.Generators.Common; |
|||
|
|||
internal class XamlXNameResolver : INameResolver, IXamlAstVisitor |
|||
{ |
|||
private readonly List<ResolvedName> _items = new(); |
|||
private readonly string _defaultFieldModifier; |
|||
|
|||
public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal) |
|||
{ |
|||
_defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant(); |
|||
} |
|||
|
|||
public IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml) |
|||
{ |
|||
_items.Clear(); |
|||
xaml.Root.Visit(this); |
|||
xaml.Root.VisitChildren(this); |
|||
return _items; |
|||
} |
|||
|
|||
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) |
|||
{ |
|||
if (node is not XamlAstObjectNode objectNode) |
|||
return node; |
|||
|
|||
var clrType = objectNode.Type.GetClrType(); |
|||
if (!clrType.IsAvaloniaStyledElement()) |
|||
return node; |
|||
|
|||
foreach (var child in objectNode.Children) |
|||
{ |
|||
if (child is XamlAstXamlPropertyValueNode propertyValueNode && |
|||
propertyValueNode.Property is XamlAstNamePropertyReference namedProperty && |
|||
namedProperty.Name == "Name" && |
|||
propertyValueNode.Values.Count > 0 && |
|||
propertyValueNode.Values[0] is XamlAstTextNode text) |
|||
{ |
|||
var fieldModifier = TryGetFieldModifier(objectNode); |
|||
var typeName = $@"{clrType.Namespace}.{clrType.Name}"; |
|||
var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList(); |
|||
var genericTypeName = typeAgs.Count == 0 |
|||
? $"global::{typeName}" |
|||
: $@"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>"; |
|||
|
|||
var resolvedName = new ResolvedName(genericTypeName, text.Text, fieldModifier); |
|||
if (_items.Contains(resolvedName)) |
|||
continue; |
|||
_items.Add(resolvedName); |
|||
} |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
|
|||
void IXamlAstVisitor.Push(IXamlAstNode node) { } |
|||
|
|||
void IXamlAstVisitor.Pop() { } |
|||
|
|||
private string TryGetFieldModifier(XamlAstObjectNode objectNode) |
|||
{ |
|||
// We follow Xamarin.Forms API behavior in terms of x:FieldModifier here:
|
|||
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers
|
|||
// However, by default we use 'internal' field modifier here for generated
|
|||
// x:Name references for historical purposes and WPF compatibility.
|
|||
//
|
|||
var fieldModifierType = objectNode |
|||
.Children |
|||
.OfType<XamlAstXmlDirective>() |
|||
.Where(dir => dir.Name == "FieldModifier" && dir.Namespace == XamlNamespaces.Xaml2006) |
|||
.Select(dir => dir.Values[0]) |
|||
.OfType<XamlAstTextNode>() |
|||
.Select(txt => txt.Text) |
|||
.FirstOrDefault(); |
|||
|
|||
return fieldModifierType?.ToLowerInvariant() switch |
|||
{ |
|||
"private" => "private", |
|||
"public" => "public", |
|||
"protected" => "protected", |
|||
"internal" => "internal", |
|||
"notpublic" => "internal", |
|||
_ => _defaultFieldModifier |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Generators.Common.Domain; |
|||
using Avalonia.Generators.Compiler; |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Parsers; |
|||
|
|||
namespace Avalonia.Generators.Common; |
|||
|
|||
internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor |
|||
{ |
|||
private readonly RoslynTypeSystem _typeSystem; |
|||
private readonly MiniCompiler _compiler; |
|||
private readonly bool _checkTypeValidity; |
|||
private readonly Action<string> _onTypeInvalid; |
|||
private readonly Action<Exception> _onUnhandledError; |
|||
|
|||
private ResolvedView _resolvedClass; |
|||
private XamlDocument _xaml; |
|||
|
|||
public XamlXViewResolver( |
|||
RoslynTypeSystem typeSystem, |
|||
MiniCompiler compiler, |
|||
bool checkTypeValidity = false, |
|||
Action<string> onTypeInvalid = null, |
|||
Action<Exception> onUnhandledError = null) |
|||
{ |
|||
_checkTypeValidity = checkTypeValidity; |
|||
_onTypeInvalid = onTypeInvalid; |
|||
_onUnhandledError = onUnhandledError; |
|||
_typeSystem = typeSystem; |
|||
_compiler = compiler; |
|||
} |
|||
|
|||
public ResolvedView ResolveView(string xaml) |
|||
{ |
|||
try |
|||
{ |
|||
_resolvedClass = null; |
|||
_xaml = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string> |
|||
{ |
|||
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} |
|||
}); |
|||
|
|||
_compiler.Transform(_xaml); |
|||
_xaml.Root.Visit(this); |
|||
_xaml.Root.VisitChildren(this); |
|||
return _resolvedClass; |
|||
} |
|||
catch (Exception exception) |
|||
{ |
|||
_onUnhandledError?.Invoke(exception); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) |
|||
{ |
|||
if (node is not XamlAstObjectNode objectNode) |
|||
return node; |
|||
|
|||
var clrType = objectNode.Type.GetClrType(); |
|||
if (!clrType.IsAvaloniaStyledElement()) |
|||
return node; |
|||
|
|||
foreach (var child in objectNode.Children) |
|||
{ |
|||
if (child is XamlAstXmlDirective directive && |
|||
directive.Name == "Class" && |
|||
directive.Namespace == XamlNamespaces.Xaml2006 && |
|||
directive.Values[0] is XamlAstTextNode text) |
|||
{ |
|||
if (_checkTypeValidity) |
|||
{ |
|||
var existingType = _typeSystem.FindType(text.Text); |
|||
if (existingType == null) |
|||
{ |
|||
_onTypeInvalid?.Invoke(text.Text); |
|||
return node; |
|||
} |
|||
} |
|||
|
|||
var split = text.Text.Split('.'); |
|||
var nameSpace = string.Join(".", split.Take(split.Length - 1)); |
|||
var className = split.Last(); |
|||
|
|||
_resolvedClass = new ResolvedView(className, clrType, nameSpace, _xaml); |
|||
return node; |
|||
} |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
|
|||
void IXamlAstVisitor.Push(IXamlAstNode node) { } |
|||
|
|||
void IXamlAstVisitor.Pop() { } |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using XamlX.Ast; |
|||
using XamlX.Transform; |
|||
|
|||
namespace Avalonia.Generators.Compiler; |
|||
|
|||
internal class DataTemplateTransformer : IXamlAstTransformer |
|||
{ |
|||
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
|||
{ |
|||
if (node is XamlAstObjectNode objectNode && |
|||
objectNode.Type is XamlAstXmlTypeReference typeReference && |
|||
(typeReference.Name == "DataTemplate" || |
|||
typeReference.Name == "ControlTemplate")) |
|||
objectNode.Children.Clear(); |
|||
return node; |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using XamlX.Compiler; |
|||
using XamlX.Emit; |
|||
using XamlX.Transform; |
|||
using XamlX.Transform.Transformers; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Generators.Compiler; |
|||
|
|||
internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult> |
|||
{ |
|||
public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute"; |
|||
|
|||
public static MiniCompiler CreateDefault(RoslynTypeSystem typeSystem, params string[] additionalTypes) |
|||
{ |
|||
var mappings = new XamlLanguageTypeMappings(typeSystem); |
|||
foreach (var additionalType in additionalTypes) |
|||
mappings.XmlnsAttributes.Add(typeSystem.GetType(additionalType)); |
|||
|
|||
var configuration = new TransformerConfiguration( |
|||
typeSystem, |
|||
typeSystem.Assemblies.First(), |
|||
mappings); |
|||
return new MiniCompiler(configuration); |
|||
} |
|||
|
|||
private MiniCompiler(TransformerConfiguration configuration) |
|||
: base(configuration, new XamlLanguageEmitMappings<object, IXamlEmitResult>(), false) |
|||
{ |
|||
Transformers.Add(new NameDirectiveTransformer()); |
|||
Transformers.Add(new DataTemplateTransformer()); |
|||
Transformers.Add(new KnownDirectivesTransformer()); |
|||
Transformers.Add(new XamlIntrinsicsTransformer()); |
|||
Transformers.Add(new XArgumentsTransformer()); |
|||
Transformers.Add(new TypeReferenceResolver()); |
|||
} |
|||
|
|||
protected override XamlEmitContext<object, IXamlEmitResult> InitCodeGen( |
|||
IFileSource file, |
|||
Func<string, IXamlType, |
|||
IXamlTypeBuilder<object>> createSubType, |
|||
Func<string, IXamlType, IEnumerable<IXamlType>, |
|||
IXamlTypeBuilder<object>> createDelegateType, |
|||
object codeGen, |
|||
XamlRuntimeContext<object, IXamlEmitResult> context, |
|||
bool needContextLocal) => |
|||
throw new NotSupportedException(); |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Transform; |
|||
|
|||
namespace Avalonia.Generators.Compiler; |
|||
|
|||
internal class NameDirectiveTransformer : IXamlAstTransformer |
|||
{ |
|||
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
|||
{ |
|||
if (node is not XamlAstObjectNode objectNode) |
|||
return node; |
|||
|
|||
for (var index = 0; index < objectNode.Children.Count; index++) |
|||
{ |
|||
var child = objectNode.Children[index]; |
|||
if (child is XamlAstXmlDirective directive && |
|||
directive.Namespace == XamlNamespaces.Xaml2006 && |
|||
directive.Name == "Name") |
|||
objectNode.Children[index] = new XamlAstXamlPropertyValueNode( |
|||
directive, |
|||
new XamlAstNamePropertyReference(directive, objectNode.Type, "Name", objectNode.Type), |
|||
directive.Values); |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
} |
|||
@ -0,0 +1,276 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Microsoft.CodeAnalysis; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Generators.Compiler; |
|||
|
|||
internal class RoslynTypeSystem : IXamlTypeSystem |
|||
{ |
|||
private readonly List<IXamlAssembly> _assemblies = new(); |
|||
|
|||
public RoslynTypeSystem(CSharpCompilation compilation) |
|||
{ |
|||
_assemblies.Add(new RoslynAssembly(compilation.Assembly)); |
|||
|
|||
var assemblySymbols = compilation |
|||
.References |
|||
.Select(compilation.GetAssemblyOrModuleSymbol) |
|||
.OfType<IAssemblySymbol>() |
|||
.Select(assembly => new RoslynAssembly(assembly)) |
|||
.ToList(); |
|||
|
|||
_assemblies.AddRange(assemblySymbols); |
|||
} |
|||
|
|||
public IEnumerable<IXamlAssembly> Assemblies => _assemblies; |
|||
|
|||
public IXamlAssembly FindAssembly(string name) => |
|||
Assemblies |
|||
.FirstOrDefault(a => string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase)); |
|||
|
|||
public IXamlType FindType(string name) => |
|||
_assemblies |
|||
.Select(assembly => assembly.FindType(name)) |
|||
.FirstOrDefault(type => type != null); |
|||
|
|||
public IXamlType FindType(string name, string assembly) => |
|||
_assemblies |
|||
.Select(assemblyInstance => assemblyInstance.FindType(name)) |
|||
.FirstOrDefault(type => type != null); |
|||
} |
|||
|
|||
internal class RoslynAssembly : IXamlAssembly |
|||
{ |
|||
private readonly IAssemblySymbol _symbol; |
|||
|
|||
public RoslynAssembly(IAssemblySymbol symbol) => _symbol = symbol; |
|||
|
|||
public bool Equals(IXamlAssembly other) => |
|||
other is RoslynAssembly roslynAssembly && |
|||
SymbolEqualityComparer.Default.Equals(_symbol, roslynAssembly._symbol); |
|||
|
|||
public string Name => _symbol.Name; |
|||
|
|||
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes => |
|||
_symbol.GetAttributes() |
|||
.Select(data => new RoslynAttribute(data, this)) |
|||
.ToList(); |
|||
|
|||
public IXamlType FindType(string fullName) |
|||
{ |
|||
var type = _symbol.GetTypeByMetadataName(fullName); |
|||
return type is null ? null : new RoslynType(type, this); |
|||
} |
|||
} |
|||
|
|||
internal class RoslynAttribute : IXamlCustomAttribute |
|||
{ |
|||
private readonly AttributeData _data; |
|||
private readonly RoslynAssembly _assembly; |
|||
|
|||
public RoslynAttribute(AttributeData data, RoslynAssembly assembly) |
|||
{ |
|||
_data = data; |
|||
_assembly = assembly; |
|||
} |
|||
|
|||
public bool Equals(IXamlCustomAttribute other) => |
|||
other is RoslynAttribute attribute && |
|||
_data == attribute._data; |
|||
|
|||
public IXamlType Type => new RoslynType(_data.AttributeClass, _assembly); |
|||
|
|||
public List<object> Parameters => |
|||
_data.ConstructorArguments |
|||
.Select(argument => argument.Value) |
|||
.ToList(); |
|||
|
|||
public Dictionary<string, object> Properties => |
|||
_data.NamedArguments.ToDictionary( |
|||
pair => pair.Key, |
|||
pair => pair.Value.Value); |
|||
} |
|||
|
|||
internal class RoslynType : IXamlType |
|||
{ |
|||
private static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat( |
|||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, |
|||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | |
|||
SymbolDisplayGenericsOptions.IncludeTypeConstraints | |
|||
SymbolDisplayGenericsOptions.IncludeVariance); |
|||
|
|||
private readonly RoslynAssembly _assembly; |
|||
private readonly INamedTypeSymbol _symbol; |
|||
|
|||
public RoslynType(INamedTypeSymbol symbol, RoslynAssembly assembly) |
|||
{ |
|||
_symbol = symbol; |
|||
_assembly = assembly; |
|||
} |
|||
|
|||
public bool Equals(IXamlType other) => |
|||
other is RoslynType roslynType && |
|||
SymbolEqualityComparer.Default.Equals(_symbol, roslynType._symbol); |
|||
|
|||
public object Id => _symbol; |
|||
|
|||
public string Name => _symbol.Name; |
|||
|
|||
public string Namespace => _symbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat); |
|||
|
|||
public string FullName => $"{Namespace}.{Name}"; |
|||
|
|||
public IXamlAssembly Assembly => _assembly; |
|||
|
|||
public IReadOnlyList<IXamlProperty> Properties => |
|||
_symbol.GetMembers() |
|||
.Where(member => member.Kind == SymbolKind.Property) |
|||
.OfType<IPropertySymbol>() |
|||
.Select(property => new RoslynProperty(property, _assembly)) |
|||
.ToList(); |
|||
|
|||
public IReadOnlyList<IXamlEventInfo> Events { get; } = new List<IXamlEventInfo>(); |
|||
|
|||
public IReadOnlyList<IXamlField> Fields { get; } = new List<IXamlField>(); |
|||
|
|||
public IReadOnlyList<IXamlMethod> Methods { get; } = new List<IXamlMethod>(); |
|||
|
|||
public IReadOnlyList<IXamlConstructor> Constructors => |
|||
_symbol.Constructors |
|||
.Select(method => new RoslynConstructor(method, _assembly)) |
|||
.ToList(); |
|||
|
|||
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>(); |
|||
|
|||
public IReadOnlyList<IXamlType> GenericArguments { get; private set; } = new List<IXamlType>(); |
|||
|
|||
public bool IsAssignableFrom(IXamlType type) => type == this; |
|||
|
|||
public IXamlType MakeGenericType(IReadOnlyList<IXamlType> typeArguments) |
|||
{ |
|||
GenericArguments = typeArguments; |
|||
return this; |
|||
} |
|||
|
|||
public IXamlType GenericTypeDefinition => this; |
|||
|
|||
public bool IsArray => false; |
|||
|
|||
public IXamlType ArrayElementType { get; } = null; |
|||
|
|||
public IXamlType MakeArrayType(int dimensions) => null; |
|||
|
|||
public IXamlType BaseType => _symbol.BaseType == null ? null : new RoslynType(_symbol.BaseType, _assembly); |
|||
|
|||
public bool IsValueType { get; } = false; |
|||
|
|||
public bool IsEnum { get; } = false; |
|||
|
|||
public IReadOnlyList<IXamlType> Interfaces => |
|||
_symbol.AllInterfaces |
|||
.Select(abstraction => new RoslynType(abstraction, _assembly)) |
|||
.ToList(); |
|||
|
|||
public bool IsInterface => _symbol.IsAbstract; |
|||
|
|||
public IXamlType GetEnumUnderlyingType() => null; |
|||
|
|||
public IReadOnlyList<IXamlType> GenericParameters { get; } = new List<IXamlType>(); |
|||
} |
|||
|
|||
internal class RoslynConstructor : IXamlConstructor |
|||
{ |
|||
private readonly IMethodSymbol _symbol; |
|||
private readonly RoslynAssembly _assembly; |
|||
|
|||
public RoslynConstructor(IMethodSymbol symbol, RoslynAssembly assembly) |
|||
{ |
|||
_symbol = symbol; |
|||
_assembly = assembly; |
|||
} |
|||
|
|||
public bool Equals(IXamlConstructor other) => |
|||
other is RoslynConstructor roslynConstructor && |
|||
SymbolEqualityComparer.Default.Equals(_symbol, roslynConstructor._symbol); |
|||
|
|||
public bool IsPublic => true; |
|||
|
|||
public bool IsStatic => false; |
|||
|
|||
public IReadOnlyList<IXamlType> Parameters => |
|||
_symbol.Parameters |
|||
.Select(parameter => parameter.Type) |
|||
.OfType<INamedTypeSymbol>() |
|||
.Select(type => new RoslynType(type, _assembly)) |
|||
.ToList(); |
|||
} |
|||
|
|||
internal class RoslynProperty : IXamlProperty |
|||
{ |
|||
private readonly IPropertySymbol _symbol; |
|||
private readonly RoslynAssembly _assembly; |
|||
|
|||
public RoslynProperty(IPropertySymbol symbol, RoslynAssembly assembly) |
|||
{ |
|||
_symbol = symbol; |
|||
_assembly = assembly; |
|||
} |
|||
|
|||
public bool Equals(IXamlProperty other) => |
|||
other is RoslynProperty roslynProperty && |
|||
SymbolEqualityComparer.Default.Equals(_symbol, roslynProperty._symbol); |
|||
|
|||
public string Name => _symbol.Name; |
|||
|
|||
public IXamlType PropertyType => |
|||
_symbol.Type is INamedTypeSymbol namedTypeSymbol |
|||
? new RoslynType(namedTypeSymbol, _assembly) |
|||
: null; |
|||
|
|||
public IXamlMethod Getter => _symbol.GetMethod == null ? null : new RoslynMethod(_symbol.GetMethod, _assembly); |
|||
|
|||
public IXamlMethod Setter => _symbol.SetMethod == null ? null : new RoslynMethod(_symbol.SetMethod, _assembly); |
|||
|
|||
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>(); |
|||
|
|||
public IReadOnlyList<IXamlType> IndexerParameters { get; } = new List<IXamlType>(); |
|||
} |
|||
|
|||
internal class RoslynMethod : IXamlMethod |
|||
{ |
|||
private readonly IMethodSymbol _symbol; |
|||
private readonly RoslynAssembly _assembly; |
|||
|
|||
public RoslynMethod(IMethodSymbol symbol, RoslynAssembly assembly) |
|||
{ |
|||
_symbol = symbol; |
|||
_assembly = assembly; |
|||
} |
|||
|
|||
public bool Equals(IXamlMethod other) => |
|||
other is RoslynMethod roslynMethod && |
|||
SymbolEqualityComparer.Default.Equals(roslynMethod._symbol, _symbol); |
|||
|
|||
public string Name => _symbol.Name; |
|||
|
|||
public bool IsPublic => true; |
|||
|
|||
public bool IsStatic => false; |
|||
|
|||
public IXamlType ReturnType => new RoslynType((INamedTypeSymbol) _symbol.ReturnType, _assembly); |
|||
|
|||
public IReadOnlyList<IXamlType> Parameters => |
|||
_symbol.Parameters.Select(parameter => parameter.Type) |
|||
.OfType<INamedTypeSymbol>() |
|||
.Select(type => new RoslynType(type, _assembly)) |
|||
.ToList(); |
|||
|
|||
public IXamlType DeclaringType => new RoslynType((INamedTypeSymbol)_symbol.ReceiverType, _assembly); |
|||
|
|||
public IXamlMethod MakeGenericMethod(IReadOnlyList<IXamlType> typeArguments) => null; |
|||
|
|||
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>(); |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System; |
|||
using Microsoft.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Generators; |
|||
|
|||
internal static class GeneratorContextExtensions |
|||
{ |
|||
private const string UnhandledErrorDescriptorId = "AXN0002"; |
|||
private const string InvalidTypeDescriptorId = "AXN0001"; |
|||
|
|||
public static string GetMsBuildProperty( |
|||
this GeneratorExecutionContext context, |
|||
string name, |
|||
string defaultValue = "") |
|||
{ |
|||
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{name}", out var value); |
|||
return value ?? defaultValue; |
|||
} |
|||
|
|||
public static void ReportNameGeneratorUnhandledError(this GeneratorExecutionContext context, Exception error) => |
|||
context.Report(UnhandledErrorDescriptorId, |
|||
"Unhandled exception occured while generating typed Name references. " + |
|||
"Please file an issue: https://github.com/avaloniaui/Avalonia.Generators", |
|||
error.ToString()); |
|||
|
|||
public static void ReportNameGeneratorInvalidType(this GeneratorExecutionContext context, string typeName) => |
|||
context.Report(InvalidTypeDescriptorId, |
|||
$"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " + |
|||
$"The type '{typeName}' does not exist in the assembly."); |
|||
|
|||
private static void Report(this GeneratorExecutionContext context, string id, string title, string message = null) => |
|||
context.ReportDiagnostic( |
|||
Diagnostic.Create( |
|||
new DiagnosticDescriptor(id, title, message ?? title, "Usage", DiagnosticSeverity.Error, true), |
|||
Location.None)); |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
using System; |
|||
using Avalonia.Generators.Common.Domain; |
|||
using Avalonia.Generators.NameGenerator; |
|||
using Microsoft.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Generators; |
|||
|
|||
// When update these enum values, don't forget to update Avalonia.Generators.props.
|
|||
internal enum BuildProperties |
|||
{ |
|||
AvaloniaNameGeneratorIsEnabled = 0, |
|||
AvaloniaNameGeneratorBehavior = 1, |
|||
AvaloniaNameGeneratorDefaultFieldModifier = 2, |
|||
AvaloniaNameGeneratorFilterByPath = 3, |
|||
AvaloniaNameGeneratorFilterByNamespace = 4, |
|||
AvaloniaNameGeneratorViewFileNamingStrategy = 5, |
|||
|
|||
// TODO add other generators properties here.
|
|||
} |
|||
|
|||
internal class GeneratorOptions |
|||
{ |
|||
private readonly GeneratorExecutionContext _context; |
|||
|
|||
public GeneratorOptions(GeneratorExecutionContext context) => _context = context; |
|||
|
|||
public bool AvaloniaNameGeneratorIsEnabled => GetBoolProperty( |
|||
BuildProperties.AvaloniaNameGeneratorIsEnabled, |
|||
true); |
|||
|
|||
public Behavior AvaloniaNameGeneratorBehavior => GetEnumProperty( |
|||
BuildProperties.AvaloniaNameGeneratorBehavior, |
|||
Behavior.InitializeComponent); |
|||
|
|||
public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier => GetEnumProperty( |
|||
BuildProperties.AvaloniaNameGeneratorDefaultFieldModifier, |
|||
NamedFieldModifier.Internal); |
|||
|
|||
public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy => GetEnumProperty( |
|||
BuildProperties.AvaloniaNameGeneratorViewFileNamingStrategy, |
|||
ViewFileNamingStrategy.NamespaceAndClassName); |
|||
|
|||
public string[] AvaloniaNameGeneratorFilterByPath => GetStringArrayProperty( |
|||
BuildProperties.AvaloniaNameGeneratorFilterByPath, |
|||
"*"); |
|||
|
|||
public string[] AvaloniaNameGeneratorFilterByNamespace => GetStringArrayProperty( |
|||
BuildProperties.AvaloniaNameGeneratorFilterByNamespace, |
|||
"*"); |
|||
|
|||
private string[] GetStringArrayProperty(BuildProperties name, string defaultValue) |
|||
{ |
|||
var key = name.ToString(); |
|||
var value = _context.GetMsBuildProperty(key, defaultValue); |
|||
return value.Contains(";") ? value.Split(';') : new[] {value}; |
|||
} |
|||
|
|||
private TEnum GetEnumProperty<TEnum>(BuildProperties name, TEnum defaultValue) where TEnum : struct |
|||
{ |
|||
var key = name.ToString(); |
|||
var value = _context.GetMsBuildProperty(key, defaultValue.ToString()); |
|||
return Enum.TryParse(value, true, out TEnum behavior) ? behavior : defaultValue; |
|||
} |
|||
|
|||
private bool GetBoolProperty(BuildProperties name, bool defaultValue) |
|||
{ |
|||
var key = name.ToString(); |
|||
var value = _context.GetMsBuildProperty(key, defaultValue.ToString()); |
|||
return bool.TryParse(value, out var result) ? result : defaultValue; |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Generators.Common.Domain; |
|||
using Microsoft.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Generators.NameGenerator; |
|||
|
|||
internal class AvaloniaNameGenerator : INameGenerator |
|||
{ |
|||
private readonly ViewFileNamingStrategy _naming; |
|||
private readonly IGlobPattern _pathPattern; |
|||
private readonly IGlobPattern _namespacePattern; |
|||
private readonly IViewResolver _classes; |
|||
private readonly INameResolver _names; |
|||
private readonly ICodeGenerator _code; |
|||
|
|||
public AvaloniaNameGenerator( |
|||
ViewFileNamingStrategy naming, |
|||
IGlobPattern pathPattern, |
|||
IGlobPattern namespacePattern, |
|||
IViewResolver classes, |
|||
INameResolver names, |
|||
ICodeGenerator code) |
|||
{ |
|||
_naming = naming; |
|||
_pathPattern = pathPattern; |
|||
_namespacePattern = namespacePattern; |
|||
_classes = classes; |
|||
_names = names; |
|||
_code = code; |
|||
} |
|||
|
|||
public IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles) |
|||
{ |
|||
var resolveViews = |
|||
from file in additionalFiles |
|||
where (file.Path.EndsWith(".xaml") || |
|||
file.Path.EndsWith(".paml") || |
|||
file.Path.EndsWith(".axaml")) && |
|||
_pathPattern.Matches(file.Path) |
|||
let xaml = file.GetText()!.ToString() |
|||
let view = _classes.ResolveView(xaml) |
|||
where view != null && _namespacePattern.Matches(view.Namespace) |
|||
select view; |
|||
|
|||
var query = |
|||
from view in resolveViews |
|||
let names = _names.ResolveNames(view.Xaml) |
|||
let code = _code.GenerateCode(view.ClassName, view.Namespace, view.XamlType, names) |
|||
let fileName = ResolveViewFileName(view, _naming) |
|||
select new GeneratedPartialClass(fileName, code); |
|||
|
|||
return query.ToList(); |
|||
} |
|||
|
|||
private static string ResolveViewFileName(ResolvedView view, ViewFileNamingStrategy strategy) => strategy switch |
|||
{ |
|||
ViewFileNamingStrategy.ClassName => $"{view.ClassName}.g.cs", |
|||
ViewFileNamingStrategy.NamespaceAndClassName => $"{view.Namespace}.{view.ClassName}.g.cs", |
|||
_ => throw new ArgumentOutOfRangeException(nameof(strategy), strategy, "Unknown naming strategy!") |
|||
}; |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
using System; |
|||
using Avalonia.Generators.Common; |
|||
using Avalonia.Generators.Common.Domain; |
|||
using Avalonia.Generators.Compiler; |
|||
using Microsoft.CodeAnalysis; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
|
|||
namespace Avalonia.Generators.NameGenerator; |
|||
|
|||
[Generator] |
|||
public class AvaloniaNameSourceGenerator : ISourceGenerator |
|||
{ |
|||
public void Initialize(GeneratorInitializationContext context) { } |
|||
|
|||
public void Execute(GeneratorExecutionContext context) |
|||
{ |
|||
try |
|||
{ |
|||
var generator = CreateNameGenerator(context); |
|||
if (generator is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var partials = generator.GenerateNameReferences(context.AdditionalFiles); |
|||
foreach (var (fileName, content) in partials) context.AddSource(fileName, content); |
|||
} |
|||
catch (Exception exception) |
|||
{ |
|||
context.ReportNameGeneratorUnhandledError(exception); |
|||
} |
|||
} |
|||
|
|||
private static INameGenerator CreateNameGenerator(GeneratorExecutionContext context) |
|||
{ |
|||
var options = new GeneratorOptions(context); |
|||
if (!options.AvaloniaNameGeneratorIsEnabled) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var types = new RoslynTypeSystem((CSharpCompilation)context.Compilation); |
|||
ICodeGenerator generator = options.AvaloniaNameGeneratorBehavior switch { |
|||
Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(), |
|||
Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types), |
|||
_ => throw new ArgumentOutOfRangeException() |
|||
}; |
|||
|
|||
var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute); |
|||
return new AvaloniaNameGenerator( |
|||
options.AvaloniaNameGeneratorViewFileNamingStrategy, |
|||
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByPath), |
|||
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByNamespace), |
|||
new XamlXViewResolver(types, compiler, true, |
|||
type => context.ReportNameGeneratorInvalidType(type), |
|||
error => context.ReportNameGeneratorUnhandledError(error)), |
|||
new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier), |
|||
generator); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System.Collections.Generic; |
|||
using Microsoft.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Generators.NameGenerator; |
|||
|
|||
internal interface INameGenerator |
|||
{ |
|||
IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles); |
|||
} |
|||
|
|||
internal record GeneratedPartialClass(string FileName, string Content); |
|||
@ -0,0 +1,83 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.Generators.Common.Domain; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Generators.NameGenerator; |
|||
|
|||
internal class InitializeComponentCodeGenerator: ICodeGenerator |
|||
{ |
|||
private readonly bool _diagnosticsAreConnected; |
|||
private const string AttachDevToolsCodeBlock = @"
|
|||
#if DEBUG
|
|||
if (attachDevTools) |
|||
{ |
|||
this.AttachDevTools(); |
|||
} |
|||
#endif
|
|||
";
|
|||
private const string AttachDevToolsParameterDocumentation |
|||
= @" /// <param name=""attachDevTools"">Should the dev tools be attached.</param>
|
|||
";
|
|||
|
|||
public InitializeComponentCodeGenerator(IXamlTypeSystem types) |
|||
{ |
|||
_diagnosticsAreConnected = types.FindAssembly("Avalonia.Diagnostics") != null; |
|||
} |
|||
|
|||
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names) |
|||
{ |
|||
var properties = new List<string>(); |
|||
var initializations = new List<string>(); |
|||
foreach (var resolvedName in names) |
|||
{ |
|||
var (typeName, name, fieldModifier) = resolvedName; |
|||
properties.Add($" {fieldModifier} {typeName} {name};"); |
|||
initializations.Add($" {name} = this.FindNameScope()?.Find<{typeName}>(\"{name}\");"); |
|||
} |
|||
|
|||
var attachDevTools = _diagnosticsAreConnected && IsWindow(xamlType); |
|||
|
|||
return $@"// <auto-generated />
|
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace {nameSpace} |
|||
{{ |
|||
partial class {className} |
|||
{{ |
|||
{string.Join("\n", properties)} |
|||
|
|||
/// <summary>
|
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
|
|||
/// </summary>
|
|||
/// <param name=""loadXaml"">Should the XAML be loaded into the component.</param>
|
|||
{(attachDevTools ? AttachDevToolsParameterDocumentation : string.Empty)} |
|||
public void InitializeComponent(bool loadXaml = true{(attachDevTools ? ", bool attachDevTools = true" : string.Empty)}) |
|||
{{ |
|||
if (loadXaml) |
|||
{{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
}} |
|||
{(attachDevTools ? AttachDevToolsCodeBlock : string.Empty)} |
|||
{string.Join("\n", initializations)} |
|||
}} |
|||
}} |
|||
}} |
|||
";
|
|||
} |
|||
|
|||
private static bool IsWindow(IXamlType xamlType) |
|||
{ |
|||
var type = xamlType; |
|||
bool isWindow; |
|||
do |
|||
{ |
|||
isWindow = type.FullName == "Avalonia.Controls.Window"; |
|||
type = type.BaseType; |
|||
} while (!isWindow && type != null); |
|||
|
|||
return isWindow; |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Generators.Common.Domain; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Generators.NameGenerator; |
|||
|
|||
internal class OnlyPropertiesCodeGenerator : ICodeGenerator |
|||
{ |
|||
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names) |
|||
{ |
|||
var namedControls = names |
|||
.Select(info => " " + |
|||
$"{info.FieldModifier} {info.TypeName} {info.Name} => " + |
|||
$"this.FindNameScope()?.Find<{info.TypeName}>(\"{info.Name}\");") |
|||
.ToList(); |
|||
var lines = string.Join("\n", namedControls); |
|||
return $@"// <auto-generated />
|
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace {nameSpace} |
|||
{{ |
|||
partial class {className} |
|||
{{ |
|||
{lines} |
|||
}} |
|||
}} |
|||
";
|
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
namespace Avalonia.Generators.NameGenerator; |
|||
|
|||
internal enum Options |
|||
{ |
|||
Public = 0, |
|||
Private = 1, |
|||
Internal = 2, |
|||
Protected = 3, |
|||
} |
|||
|
|||
internal enum Behavior |
|||
{ |
|||
OnlyProperties = 0, |
|||
InitializeComponent = 1, |
|||
} |
|||
|
|||
internal enum ViewFileNamingStrategy |
|||
{ |
|||
ClassName = 0, |
|||
NamespaceAndClassName = 1, |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"profiles": { |
|||
"Profile 1": { |
|||
"commandName": "DebugRoslynComponent", |
|||
"targetProject": "..\\..\\..\\samples\\Generators.Sandbox\\Generators.Sandbox.csproj" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,209 @@ |
|||
[](https://www.nuget.org/packages/XamlNameReferenceGenerator) [](https://www.nuget.org/packages/XamlNameReferenceGenerator)    |
|||
|
|||
### C# `SourceGenerator` for Typed Avalonia `x:Name` References |
|||
|
|||
This is a [C# `SourceGenerator`](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) built for generating strongly-typed references to controls with `x:Name` (or just `Name`) attributes declared in XAML (or, in `.axaml`). The source generator will look for the `xaml` (or `axaml`) file with the same name as your partial C# class that is a subclass of `Avalonia.INamed` and parses the XAML markup, finds all XAML tags with `x:Name` attributes and generates the C# code. |
|||
|
|||
### Getting Started |
|||
|
|||
In order to get started, just install the NuGet package: |
|||
|
|||
``` |
|||
dotnet add package XamlNameReferenceGenerator |
|||
``` |
|||
|
|||
Or, if you are using [submodules](https://git-scm.com/docs/git-submodule), you can reference the generator as such: |
|||
|
|||
```xml |
|||
<ItemGroup> |
|||
<!-- Remember to ensure XAML files are included via <AdditionalFiles>, |
|||
otherwise C# source generator won't see XAML files. --> |
|||
<AdditionalFiles Include="**\*.xaml"/> |
|||
<ProjectReference Include="..\Avalonia.NameGenerator\Avalonia.NameGenerator.csproj" |
|||
OutputItemType="Analyzer" |
|||
ReferenceOutputAssembly="false" /> |
|||
</ItemGroup> |
|||
``` |
|||
|
|||
### Usage |
|||
|
|||
After installing the NuGet package, declare your view class as `partial`. Typed C# references to Avalonia controls declared in XAML files will be generated for classes referenced by the `x:Class` directive in XAML files. For example, for the following XAML markup: |
|||
|
|||
```xml |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Sample.App.SignUpView"> |
|||
<TextBox x:Name="UserNameTextBox" x:FieldModifier="public" /> |
|||
</Window> |
|||
``` |
|||
|
|||
A new C# partial class named `SignUpView` with a single `public` property named `UserNameTextBox` of type `TextBox` will be generated in the `Sample.App` namespace. We won't see the generated file, but we'll be able to access the generated property as shown below: |
|||
|
|||
```cs |
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
public partial class SignUpView : Window |
|||
{ |
|||
public SignUpView() |
|||
{ |
|||
// This method is generated. Call it before accessing any |
|||
// of the generated properties. The 'UserNameTextBox' |
|||
// property is also generated. |
|||
InitializeComponent(); |
|||
UserNameTextBox.Text = "Joseph"; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
<img src="https://hsto.org/getpro/habr/post_images/d9f/4aa/a1e/d9f4aaa1eb450f5dd2fca66631bc16a0.gif" /> |
|||
|
|||
### Why do I need this? |
|||
|
|||
The typed `x:Name` references might be useful if you decide to use e.g. [ReactiveUI code-behind bindings](https://www.reactiveui.net/docs/handbook/data-binding/): |
|||
|
|||
```cs |
|||
// UserNameValidation and PasswordValidation are auto generated. |
|||
public partial class SignUpView : ReactiveWindow<SignUpViewModel> |
|||
{ |
|||
public SignUpView() |
|||
{ |
|||
InitializeComponent(); |
|||
this.WhenActivated(disposables => |
|||
{ |
|||
this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text) |
|||
.DisposeWith(disposables); |
|||
this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text) |
|||
.DisposeWith(disposables); |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Advanced Usage |
|||
|
|||
> Never keep a method named `InitializeComponent` in your code-behind view class if you are using the generator with `AvaloniaNameGeneratorBehavior` set to `InitializeComponent` (this is the default value). The private `InitializeComponent` method declared in your code-behind class hides the `InitializeComponent` method generated by `Avalonia.NameGenerator`, see [Issue 69](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/69). If you wish to use your own `InitializeComponent` method (not the generated one), set `AvaloniaNameGeneratorBehavior` to `OnlyProperties`. |
|||
|
|||
The `x:Name` generator can be configured via MsBuild properties that you can put into your C# project file (`.csproj`). Using such options, you can configure the generator behavior, the default field modifier, namespace and path filters. The generator supports the following options: |
|||
|
|||
- `AvaloniaNameGeneratorBehavior` |
|||
Possible values: `OnlyProperties`, `InitializeComponent` |
|||
Default value: `InitializeComponent` |
|||
Determines if the generator should generate get-only properties, or the `InitializeComponent` method. |
|||
|
|||
- `AvaloniaNameGeneratorDefaultFieldModifier` |
|||
Possible values: `internal`, `public`, `private`, `protected` |
|||
Default value: `internal` |
|||
The default field modifier that should be used when there is no `x:FieldModifier` directive specified. |
|||
|
|||
- `AvaloniaNameGeneratorFilterByPath` |
|||
Posssible format: `glob_pattern`, `glob_pattern;glob_pattern` |
|||
Default value: `*` |
|||
The generator will process only XAML files with paths matching the specified glob pattern(s). |
|||
Example: `*/Views/*View.xaml`, `*View.axaml;*Control.axaml` |
|||
|
|||
- `AvaloniaNameGeneratorFilterByNamespace` |
|||
Posssible format: `glob_pattern`, `glob_pattern;glob_pattern` |
|||
Default value: `*` |
|||
The generator will process only XAML files with base classes' namespaces matching the specified glob pattern(s). |
|||
Example: `MyApp.Presentation.*`, `MyApp.Presentation.Views;MyApp.Presentation.Controls` |
|||
|
|||
- `AvaloniaNameGeneratorViewFileNamingStrategy` |
|||
Possible values: `ClassName`, `NamespaceAndClassName` |
|||
Default value: `NamespaceAndClassName` |
|||
Determines how the automatically generated view files should be [named](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/92). |
|||
|
|||
The default values are given by: |
|||
|
|||
```xml |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<AvaloniaNameGeneratorBehavior>InitializeComponent</AvaloniaNameGeneratorBehavior> |
|||
<AvaloniaNameGeneratorDefaultFieldModifier>internal</AvaloniaNameGeneratorDefaultFieldModifier> |
|||
<AvaloniaNameGeneratorFilterByPath>*</AvaloniaNameGeneratorFilterByPath> |
|||
<AvaloniaNameGeneratorFilterByNamespace>*</AvaloniaNameGeneratorFilterByNamespace> |
|||
<AvaloniaNameGeneratorViewFileNamingStrategy>NamespaceAndClassName</AvaloniaNameGeneratorViewFileNamingStrategy> |
|||
</PropertyGroup> |
|||
<!-- ... --> |
|||
</Project> |
|||
``` |
|||
|
|||
 |
|||
|
|||
### What do the generated sources look like? |
|||
|
|||
For [`SignUpView`](https://github.com/avaloniaui/Avalonia.NameGenerator/blob/main/src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml), we get the following generated output when the source generator is in the `InitializeComponent` mode: |
|||
|
|||
```cs |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox; |
|||
public global::Avalonia.Controls.TextBlock UserNameValidation; |
|||
private global::Avalonia.Controls.TextBox PasswordTextBox; |
|||
internal global::Avalonia.Controls.TextBlock PasswordValidation; |
|||
internal global::Avalonia.Controls.ListBox AwesomeListView; |
|||
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; |
|||
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation; |
|||
internal global::Avalonia.Controls.Button SignUpButton; |
|||
internal global::Avalonia.Controls.TextBlock CompoundValidation; |
|||
|
|||
public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
// This will be added only if you install Avalonia.Diagnostics. |
|||
#if DEBUG |
|||
if (attachDevTools) |
|||
{ |
|||
this.AttachDevTools(); |
|||
} |
|||
#endif |
|||
|
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox>("UserNameTextBox"); |
|||
UserNameValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation"); |
|||
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
PasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation"); |
|||
AwesomeListView = this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("AwesomeListView"); |
|||
ConfirmPasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
|||
ConfirmPasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation"); |
|||
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
CompoundValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation"); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
If you enable the `OnlyProperties` source generator mode, you get: |
|||
|
|||
```cs |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Avalonia.NameGenerator.Sandbox.Views |
|||
{ |
|||
partial class SignUpView |
|||
{ |
|||
internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox>("UserNameTextBox"); |
|||
public global::Avalonia.Controls.TextBlock UserNameValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation"); |
|||
private global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation"); |
|||
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
|||
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation"); |
|||
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation"); |
|||
} |
|||
} |
|||
``` |
|||
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<RootNamespace>Avalonia.Generators.Tests</RootNamespace> |
|||
<IsTestProject>true</IsTestProject> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" /> |
|||
<ProjectReference Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.csproj" /> |
|||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" /> |
|||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<EmbeddedResource Include="Views\*.xml" /> |
|||
<EmbeddedResource Include="OnlyProperties\GeneratedCode\*.txt" /> |
|||
<EmbeddedResource Include="InitializeComponent\GeneratedInitializeComponent\*.txt" /> |
|||
<EmbeddedResource Include="InitializeComponent\GeneratedDevTools\*.txt" /> |
|||
</ItemGroup> |
|||
<Import Project="..\..\build\UnitTests.NetCore.targets" /> |
|||
<Import Project="..\..\build\XUnit.props" /> |
|||
<Import Project="..\..\build\SharedVersion.props" /> |
|||
</Project> |
|||
@ -0,0 +1,31 @@ |
|||
using Avalonia.Generators.Common; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Generators.Tests; |
|||
|
|||
public class GlobPatternTests |
|||
{ |
|||
[Theory] |
|||
[InlineData("*", "anything", true)] |
|||
[InlineData("", "anything", false)] |
|||
[InlineData("Views/*", "Views/SignUpView.xaml", true)] |
|||
[InlineData("Views/*", "Extensions/SignUpView.xaml", false)] |
|||
[InlineData("*SignUpView*", "Extensions/SignUpView.xaml", true)] |
|||
[InlineData("*SignUpView.paml", "Extensions/SignUpView.xaml", false)] |
|||
[InlineData("*.xaml", "Extensions/SignUpView.xaml", true)] |
|||
public void Should_Match_Glob_Expressions(string pattern, string value, bool matches) |
|||
{ |
|||
Assert.Equal(matches, new GlobPattern(pattern).Matches(value)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("Views/SignUpView.xaml", true, new[] { "*.xaml", "Extensions/*" })] |
|||
[InlineData("Extensions/SignUpView.paml", true, new[] { "*.xaml", "Extensions/*" })] |
|||
[InlineData("Extensions/SignUpView.paml", false, new[] { "*.xaml", "Views/*" })] |
|||
[InlineData("anything", true, new[] { "*", "*" })] |
|||
[InlineData("anything", false, new[] { "", "" })] |
|||
public void Should_Match_Glob_Pattern_Groups(string value, bool matches, string[] patterns) |
|||
{ |
|||
Assert.Equal(matches, new GlobPatternGroup(patterns).Matches(value)); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue