37 changed files with 249 additions and 444 deletions
@ -0,0 +1,84 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Collections; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml.Converters; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Avalonia.Styling; |
||||
|
using Avalonia.Controls.Templates; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Maintains a repository of <see cref="TypeConverter"/>s for XAML parsing on top of those
|
||||
|
/// maintained by <see cref="TypeDescriptor"/>.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The default method of defining type converters using <see cref="TypeConverterAttribute"/>
|
||||
|
/// isn't powerful enough for our purposes:
|
||||
|
///
|
||||
|
/// - It doesn't handle non-constructed generic types (such as <see cref="AvaloniaList{T}"/>)
|
||||
|
/// - Type converters which require XAML features cannot be defined in non-XAML assemblies and
|
||||
|
/// so can't be referenced using <see cref="TypeConverterAttribute"/>
|
||||
|
/// - Many types have a static `Parse(string)` method which can be used implicitly; this class
|
||||
|
/// detects such methods and auto-creates a type converter
|
||||
|
/// </remarks>
|
||||
|
public static class AvaloniaTypeConverters |
||||
|
{ |
||||
|
private static Dictionary<Type, Type> _converters = new Dictionary<Type, Type>() |
||||
|
{ |
||||
|
{ typeof(AvaloniaList<>), typeof(AvaloniaListConverter<>) }, |
||||
|
{ typeof(AvaloniaProperty), typeof(AvaloniaPropertyTypeConverter) }, |
||||
|
{ typeof(IBitmap), typeof(BitmapTypeConverter) }, |
||||
|
{ typeof(IList<Point>), typeof(PointsListTypeConverter) }, |
||||
|
{ typeof(IMemberSelector), typeof(MemberSelectorTypeConverter) }, |
||||
|
{ typeof(Selector), typeof(SelectorTypeConverter) }, |
||||
|
{ typeof(TimeSpan), typeof(TimeSpanTypeConverter) }, |
||||
|
{ typeof(WindowIcon), typeof(IconTypeConverter) }, |
||||
|
}; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Tries to lookup a <see cref="TypeConverter"/> for a type.
|
||||
|
/// </summary>
|
||||
|
/// <param name="type">The type.</param>
|
||||
|
/// <returns>The type converter.</returns>
|
||||
|
public static Type GetTypeConverter(Type type) |
||||
|
{ |
||||
|
if (_converters.TryGetValue(type, out var result)) |
||||
|
{ |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
// Converters for non-constructed generic types can't be specified using
|
||||
|
// TypeConverterAttribute. Allow them to be registered here and handle them sanely.
|
||||
|
if (type.IsConstructedGenericType && |
||||
|
_converters.TryGetValue(type.GetGenericTypeDefinition(), out result)) |
||||
|
{ |
||||
|
return result?.MakeGenericType(type.GetGenericArguments()); |
||||
|
} |
||||
|
|
||||
|
// If the type isn't a primitive or a type that XAML already handles, but has a static
|
||||
|
// Parse method, use that
|
||||
|
if (!type.IsPrimitive && |
||||
|
type != typeof(DateTime) && |
||||
|
type != typeof(Uri) && |
||||
|
ParseTypeConverter.HasParseMethod(type)) |
||||
|
{ |
||||
|
result = typeof(ParseTypeConverter<>).MakeGenericType(type); |
||||
|
_converters.Add(type, result); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
_converters.Add(type, null); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Registers a type converter for a type.
|
||||
|
/// </summary>
|
||||
|
/// <param name="type">The type. Maybe be a non-constructed generic type.</param>
|
||||
|
/// <param name="converterType">The converter type. Maybe be a non-constructed generic type.</param>
|
||||
|
public static void Register(Type type, Type converterType) => _converters[type] = converterType; |
||||
|
} |
||||
|
} |
||||
@ -1,22 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
using Avalonia.Controls; |
|
||||
using System.ComponentModel; |
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
public class ClassesTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return new Classes(((string)value).Split(' ')); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,26 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
using Avalonia.Media; |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
|
|
||||
|
|
||||
public class ColorTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return Color.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -1,24 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
using Avalonia.Controls; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class ColumnDefinitionsTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return new ColumnDefinitions((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,19 +0,0 @@ |
|||||
using System; |
|
||||
using System.ComponentModel; |
|
||||
using System.Globalization; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
public class CornerRadiusTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return CornerRadius.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,25 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
using Avalonia.Input; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class CursorTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
var cursor = (StandardCursorType)Enum.Parse(typeof(StandardCursorType), ((string)value).Trim(), true); |
|
||||
return new Cursor(cursor); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,20 +0,0 @@ |
|||||
using System; |
|
||||
using System.ComponentModel; |
|
||||
using System.Globalization; |
|
||||
using Avalonia.Media; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
public class FontFamilyTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return FontFamily.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,24 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
using Avalonia.Media; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class GeometryTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return StreamGeometry.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,24 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
using Avalonia.Controls; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class GridLengthTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return GridLength.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,24 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
using Avalonia.Input; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class KeyGestureConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return KeyGesture.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class MatrixTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return Matrix.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,79 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.Globalization; |
||||
|
using System.Reflection; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.Converters |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Base class for type converters which call a static Parse method.
|
||||
|
/// </summary>
|
||||
|
public abstract class ParseTypeConverter : TypeConverter |
||||
|
{ |
||||
|
protected const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; |
||||
|
protected static readonly Type[] StringParameter = new[] { typeof(string) }; |
||||
|
protected static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) }; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Checks whether a type has a suitable Parse method.
|
||||
|
/// </summary>
|
||||
|
/// <param name="type">The type.</param>
|
||||
|
/// <returns>True if the type has a suitable parse method, otherwise false.</returns>
|
||||
|
public static bool HasParseMethod(Type type) |
||||
|
{ |
||||
|
return type.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null) != null || |
||||
|
type.GetMethod("Parse", PublicStatic, null, StringParameter, null) != null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// A type converter which calls a static Parse method.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The type with the Parse method.</typeparam>
|
||||
|
public class ParseTypeConverter<T> : ParseTypeConverter |
||||
|
{ |
||||
|
private static Func<string, T> _parse; |
||||
|
private static Func<string, IFormatProvider, T> _parseWithFormat; |
||||
|
|
||||
|
static ParseTypeConverter() |
||||
|
{ |
||||
|
var method = typeof(T).GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); |
||||
|
|
||||
|
if (method != null) |
||||
|
{ |
||||
|
_parseWithFormat = (Func<string, IFormatProvider, T>)method |
||||
|
.CreateDelegate(typeof(Func<string, IFormatProvider, T>)); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
method = typeof(T).GetMethod("Parse", PublicStatic, null, StringParameter, null); |
||||
|
|
||||
|
if (method != null) |
||||
|
{ |
||||
|
_parse = (Func<string, T>)method.CreateDelegate(typeof(Func<string, T>)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
||||
|
{ |
||||
|
return sourceType == typeof(string); |
||||
|
} |
||||
|
|
||||
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
||||
|
{ |
||||
|
if (value != null) |
||||
|
{ |
||||
|
if (_parse != null) |
||||
|
{ |
||||
|
return _parse(value.ToString()); |
||||
|
} |
||||
|
else if (_parseWithFormat != null) |
||||
|
{ |
||||
|
return _parseWithFormat(value.ToString(), culture); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,23 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class PointTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return Point.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class RectTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return Rect.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class RelativePointTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return RelativePoint.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class RelativeRectTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return RelativeRect.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,24 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
using Avalonia.Controls; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class RowDefinitionsTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return new RowDefinitions((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class SizeTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return Size.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Globalization; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.Converters |
|
||||
{ |
|
||||
using System.ComponentModel; |
|
||||
|
|
||||
public class ThicknessTypeConverter : TypeConverter |
|
||||
{ |
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|
||||
{ |
|
||||
return sourceType == typeof(string); |
|
||||
} |
|
||||
|
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|
||||
{ |
|
||||
return Thickness.Parse((string)value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue