145 changed files with 3470 additions and 1738 deletions
@ -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="ClientSize" 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="FrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True" |
|||
Text="{Binding FrameSize, Mode=OneWay}"/> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="3">Position</Label> |
|||
<TextBox Name="Position" Grid.Column="1" Grid.Row="3" IsReadOnly="True"/> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="4">Owner Rect</Label> |
|||
<TextBox Name="OwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True"/> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="5">Screen Rect</Label> |
|||
<TextBox Name="ScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True"/> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="6">Scaling</Label> |
|||
<TextBox Name="Scaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True"/> |
|||
|
|||
<Label Grid.Column="0" Grid.Row="7">WindowState</Label> |
|||
<ComboBox Name="WindowState" 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="Order" 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> |
|||
|
|||
@ -1,5 +1,12 @@ |
|||
#!/usr/bin/env bash |
|||
|
|||
cd $(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) |
|||
dotnet restore -r osx-arm64 |
|||
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-arm64 -p:_AvaloniaUseExternalMSBuild=false |
|||
|
|||
arch="x64" |
|||
|
|||
if [[ $(uname -m) == 'arm64' ]]; then |
|||
arch="arm64" |
|||
fi |
|||
|
|||
dotnet restore -r osx-$arch |
|||
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-$arch -p:_AvaloniaUseExternalMSBuild=false |
|||
@ -0,0 +1,235 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Android.OS; |
|||
using Android.Views; |
|||
using AndroidX.AppCompat.App; |
|||
using AndroidX.Core.View; |
|||
using Avalonia.Android.Platform.SkiaPlatform; |
|||
using Avalonia.Controls.Platform; |
|||
using static Avalonia.Controls.Platform.IInsetsManager; |
|||
|
|||
namespace Avalonia.Android.Platform |
|||
{ |
|||
internal class AndroidInsetsManager : Java.Lang.Object, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener |
|||
{ |
|||
private readonly AvaloniaMainActivity _activity; |
|||
private readonly TopLevelImpl _topLevel; |
|||
private readonly InsetsAnimationCallback _callback; |
|||
private bool _displayEdgeToEdge; |
|||
private bool _usesLegacyLayouts; |
|||
private bool? _systemUiVisibility; |
|||
private SystemBarTheme? _statusBarTheme; |
|||
private bool? _isDefaultSystemBarLightTheme; |
|||
|
|||
public event EventHandler<SafeAreaChangedArgs> SafeAreaChanged; |
|||
|
|||
public bool DisplayEdgeToEdge |
|||
{ |
|||
get => _displayEdgeToEdge; |
|||
set |
|||
{ |
|||
_displayEdgeToEdge = value; |
|||
|
|||
if(Build.VERSION.SdkInt >= BuildVersionCodes.P) |
|||
{ |
|||
_activity.Window.Attributes.LayoutInDisplayCutoutMode = value ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default; |
|||
} |
|||
|
|||
WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value); |
|||
} |
|||
} |
|||
|
|||
public AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel) |
|||
{ |
|||
_activity = activity; |
|||
_topLevel = topLevel; |
|||
_callback = new InsetsAnimationCallback(WindowInsetsAnimationCompat.Callback.DispatchModeStop); |
|||
|
|||
_callback.InsetsManager = this; |
|||
|
|||
ViewCompat.SetOnApplyWindowInsetsListener(_activity.Window.DecorView, this); |
|||
|
|||
ViewCompat.SetWindowInsetsAnimationCallback(_activity.Window.DecorView, _callback); |
|||
|
|||
if(Build.VERSION.SdkInt < BuildVersionCodes.R) |
|||
{ |
|||
_usesLegacyLayouts = true; |
|||
_activity.Window.DecorView.ViewTreeObserver.AddOnGlobalLayoutListener(this); |
|||
} |
|||
|
|||
DisplayEdgeToEdge = false; |
|||
} |
|||
|
|||
public Thickness SafeAreaPadding |
|||
{ |
|||
get |
|||
{ |
|||
var insets = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); |
|||
|
|||
if (insets != null) |
|||
{ |
|||
var renderScaling = _topLevel.RenderScaling; |
|||
|
|||
var inset = insets.GetInsets( |
|||
(DisplayEdgeToEdge ? |
|||
WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | |
|||
WindowInsetsCompat.Type.DisplayCutout() : |
|||
0) | WindowInsetsCompat.Type.Ime()); |
|||
var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); |
|||
var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); |
|||
|
|||
return new Thickness(inset.Left / renderScaling, |
|||
inset.Top / renderScaling, |
|||
inset.Right / renderScaling, |
|||
(imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ? |
|||
imeInset.Bottom - navBarInset.Bottom : |
|||
inset.Bottom) / renderScaling); |
|||
} |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
|
|||
public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets) |
|||
{ |
|||
NotifySafeAreaChanged(SafeAreaPadding); |
|||
return insets; |
|||
} |
|||
|
|||
private void NotifySafeAreaChanged(Thickness safeAreaPadding) |
|||
{ |
|||
SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(safeAreaPadding)); |
|||
} |
|||
|
|||
public void OnGlobalLayout() |
|||
{ |
|||
NotifySafeAreaChanged(SafeAreaPadding); |
|||
} |
|||
|
|||
public SystemBarTheme? SystemBarTheme |
|||
{ |
|||
get |
|||
{ |
|||
try |
|||
{ |
|||
var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View); |
|||
|
|||
return compat.AppearanceLightStatusBars ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark; |
|||
} |
|||
catch (Exception _) |
|||
{ |
|||
return Controls.Platform.SystemBarTheme.Light; |
|||
} |
|||
} |
|||
set |
|||
{ |
|||
_statusBarTheme = value; |
|||
|
|||
var isDefault = _statusBarTheme == null; |
|||
|
|||
if (!_topLevel.View.IsShown) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View); |
|||
|
|||
if (_isDefaultSystemBarLightTheme == null) |
|||
{ |
|||
_isDefaultSystemBarLightTheme = compat.AppearanceLightStatusBars; |
|||
} |
|||
|
|||
if (value == null && _isDefaultSystemBarLightTheme != null) |
|||
{ |
|||
value = (bool)_isDefaultSystemBarLightTheme ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark; |
|||
} |
|||
|
|||
compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light; |
|||
compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light; |
|||
|
|||
AppCompatDelegate.DefaultNightMode = isDefault ? AppCompatDelegate.ModeNightFollowSystem : compat.AppearanceLightStatusBars ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes; |
|||
} |
|||
} |
|||
|
|||
public bool? IsSystemBarVisible |
|||
{ |
|||
get |
|||
{ |
|||
if(_activity.Window == null) |
|||
{ |
|||
return true; |
|||
} |
|||
var compat = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); |
|||
|
|||
return compat?.IsVisible(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); |
|||
} |
|||
set |
|||
{ |
|||
_systemUiVisibility = value; |
|||
|
|||
if (!_topLevel.View.IsShown) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var compat = WindowCompat.GetInsetsController(_activity.Window, _topLevel.View); |
|||
|
|||
if (value == null || value.Value) |
|||
{ |
|||
compat?.Show(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); |
|||
} |
|||
else |
|||
{ |
|||
compat?.Hide(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); |
|||
|
|||
if (compat != null) |
|||
{ |
|||
compat.SystemBarsBehavior = WindowInsetsControllerCompat.BehaviorShowTransientBarsBySwipe; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal void ApplyStatusBarState() |
|||
{ |
|||
IsSystemBarVisible = _systemUiVisibility; |
|||
SystemBarTheme = _statusBarTheme; |
|||
} |
|||
|
|||
private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback |
|||
{ |
|||
public InsetsAnimationCallback(int dispatchMode) : base(dispatchMode) |
|||
{ |
|||
} |
|||
|
|||
public AndroidInsetsManager InsetsManager { get; set; } |
|||
|
|||
public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList<WindowInsetsAnimationCompat> runningAnimations) |
|||
{ |
|||
foreach (var anim in runningAnimations) |
|||
{ |
|||
if ((anim.TypeMask & WindowInsetsCompat.Type.Ime()) != 0) |
|||
{ |
|||
var renderScaling = InsetsManager._topLevel.RenderScaling; |
|||
|
|||
var inset = insets.GetInsets((InsetsManager.DisplayEdgeToEdge ? WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime()); |
|||
var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); |
|||
var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); |
|||
|
|||
|
|||
var bottomPadding = (imeInset.Bottom > 0 && !InsetsManager.DisplayEdgeToEdge ? imeInset.Bottom - navBarInset.Bottom : inset.Bottom); |
|||
bottomPadding = (int)(bottomPadding * anim.InterpolatedFraction); |
|||
|
|||
var padding = new Thickness(inset.Left / renderScaling, |
|||
inset.Top / renderScaling, |
|||
inset.Right / renderScaling, |
|||
bottomPadding / renderScaling); |
|||
InsetsManager?.NotifySafeAreaChanged(padding); |
|||
break; |
|||
} |
|||
} |
|||
return insets; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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,34 @@ |
|||
using Avalonia.Automation.Peers; |
|||
using Avalonia.Controls.Primitives; |
|||
|
|||
namespace Avalonia.Controls.Automation.Peers |
|||
{ |
|||
public class LabelAutomationPeer : ControlAutomationPeer |
|||
{ |
|||
public LabelAutomationPeer(Label owner) : base(owner) |
|||
{ |
|||
} |
|||
|
|||
override protected string GetClassNameCore() |
|||
{ |
|||
return "Text"; |
|||
} |
|||
|
|||
override protected AutomationControlType GetAutomationControlTypeCore() |
|||
{ |
|||
return AutomationControlType.Text; |
|||
} |
|||
|
|||
override protected string? GetNameCore() |
|||
{ |
|||
var content = ((Label)Owner).Content as string; |
|||
|
|||
if (string.IsNullOrEmpty(content)) |
|||
{ |
|||
return base.GetNameCore(); |
|||
} |
|||
|
|||
return AccessText.RemoveAccessKeyMarker(content) ?? string.Empty; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
using System; |
|||
using Avalonia.Metadata; |
|||
|
|||
#nullable enable |
|||
namespace Avalonia.Controls.Platform |
|||
{ |
|||
[Unstable] |
|||
[NotClientImplementable] |
|||
public interface IInsetsManager |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets whether the system bars are visible.
|
|||
/// </summary>
|
|||
bool? IsSystemBarVisible { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets whether the window draws edge to edge. behind any visibile system bars.
|
|||
/// </summary>
|
|||
bool DisplayEdgeToEdge { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the current safe area padding.
|
|||
/// </summary>
|
|||
Thickness SafeAreaPadding { get; } |
|||
|
|||
/// <summary>
|
|||
/// Occurs when safe area for the current window changes.
|
|||
/// </summary>
|
|||
event EventHandler<SafeAreaChangedArgs>? SafeAreaChanged; |
|||
} |
|||
|
|||
public class SafeAreaChangedArgs : EventArgs |
|||
{ |
|||
public SafeAreaChangedArgs(Thickness safeArePadding) |
|||
{ |
|||
SafeAreaPadding = safeArePadding; |
|||
} |
|||
|
|||
/// <inheritdoc cref="IInsetsManager.GetSafeAreaPadding"/>
|
|||
public Thickness SafeAreaPadding { get; } |
|||
} |
|||
|
|||
public enum SystemBarTheme |
|||
{ |
|||
/// <summary>
|
|||
/// Light system bar theme, with light background and a dark foreground
|
|||
/// </summary>
|
|||
Light, |
|||
|
|||
/// <summary>
|
|||
/// Bark system bar theme, with dark background and a light foreground
|
|||
/// </summary>
|
|||
Dark |
|||
} |
|||
} |
|||
@ -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)) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue