Browse Source

Merge branch 'master' into fixes/886-incorrect-virtualized-items

pull/976/head
Steven Kirk 9 years ago
committed by GitHub
parent
commit
954bc7c764
  1. 5
      .ncrunch/Avalonia.Base.UnitTests.net461.v3.ncrunchproject
  2. 5
      .ncrunch/Avalonia.Controls.UnitTests.net461.v3.ncrunchproject
  3. 5
      .ncrunch/Avalonia.Input.UnitTests.net461.v3.ncrunchproject
  4. 5
      .ncrunch/Avalonia.Interactivity.UnitTests.net461.v3.ncrunchproject
  5. 5
      .ncrunch/Avalonia.Interactivity.UnitTests.netcoreapp1.1.v3.ncrunchproject
  6. 5
      .ncrunch/Avalonia.Layout.UnitTests.net461.v3.ncrunchproject
  7. 5
      .ncrunch/Avalonia.Markup.UnitTests.net461.v3.ncrunchproject
  8. 5
      .ncrunch/Avalonia.Markup.Xaml.UnitTests.net461.v3.ncrunchproject
  9. 5
      .ncrunch/Avalonia.Styling.UnitTests.net461.v3.ncrunchproject
  10. 5
      .ncrunch/Avalonia.UnitTests.net461.v3.ncrunchproject
  11. 5
      .ncrunch/Direct3DInteropSample.v3.ncrunchproject
  12. 2
      build/SkiaSharp.props
  13. 2
      docs/guidelines/build.md
  14. 21
      packages.cake
  15. 6
      src/Avalonia.Base/AvaloniaObject.cs
  16. 2
      src/Avalonia.Base/AvaloniaProperty.cs
  17. 2
      src/Avalonia.Base/PriorityValue.cs
  18. 237
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  19. 2
      src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
  20. 64
      src/Markup/Avalonia.Markup/DefaultValueConverter.cs
  21. 4
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  22. 2
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  23. 17
      tests/Avalonia.Markup.Xaml.UnitTests/Parsers/SelectorParserTests.cs

5
.ncrunch/Avalonia.Base.UnitTests.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.Controls.UnitTests.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.Input.UnitTests.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.Interactivity.UnitTests.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.Interactivity.UnitTests.netcoreapp1.1.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.Layout.UnitTests.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.Markup.UnitTests.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.Markup.Xaml.UnitTests.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.Styling.UnitTests.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.UnitTests.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Direct3DInteropSample.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

2
build/SkiaSharp.props

@ -1,5 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.56.1-beta" />
<PackageReference Include="SkiaSharp" Version="1.57.1" />
</ItemGroup>
</Project>

2
docs/guidelines/build.md

@ -2,7 +2,7 @@
## Windows
Avalonia requires at least Visual Studio 2015 to build on Windows.
Avalonia requires at least Visual Studio 2017 to build on Windows.
### Install GTK Sharp

21
packages.cake

@ -425,10 +425,7 @@ public class Packages
{
new NuSpecDependency() { Id = "Avalonia", Version = parameters.Version },
new NuSpecDependency() { Id = "SkiaSharp", Version = SkiaSharpVersion },
//netstandard1.3
new NuSpecDependency() { Id = "Avalonia", TargetFramework = "netstandard1.3", Version = parameters.Version },
new NuSpecDependency() { Id = "SkiaSharp", TargetFramework = "netstandard1.3", Version = SkiaSharpVersion },
new NuSpecDependency() { Id = "NETStandard.Library", TargetFramework = "netstandard1.3", Version = "1.6.0" }
new NuSpecDependency() { Id = "Avalonia.Skia.Linux.Natives", Version = SkiaSharpVersion }
},
Files = new []
{
@ -446,11 +443,17 @@ public class Packages
Id = "Avalonia.Desktop",
Dependencies = new []
{
new NuSpecDependency() { Id = "Avalonia.Win32", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Direct2D1", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Gtk", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Cairo", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Skia.Desktop", Version = parameters.Version }
//Full .NET
new NuSpecDependency() { Id = "Avalonia.Direct2D1", TargetFramework="net45", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Gtk", TargetFramework="net45", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Cairo", TargetFramework="net45", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Win32", TargetFramework="net45", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Skia.Desktop", TargetFramework="net45", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Gtk3", TargetFramework="net45", Version = parameters.Version },
//.NET Core
new NuSpecDependency() { Id = "Avalonia.Win32", TargetFramework="netcoreapp1.1", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Skia.Desktop", TargetFramework="netcoreapp1.1", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Gtk3", TargetFramework="netcoreapp1.1", Version = parameters.Version }
},
Files = new NuSpecContent[]
{

6
src/Avalonia.Base/AvaloniaObject.cs

@ -578,13 +578,13 @@ namespace Avalonia
if (notification == null)
{
return TypeUtilities.CastOrDefault(value, type);
return TypeUtilities.ConvertImplicitOrDefault(value, type);
}
else
{
if (notification.HasValue)
{
notification.SetValue(TypeUtilities.CastOrDefault(notification.Value, type));
notification.SetValue(TypeUtilities.ConvertImplicitOrDefault(notification.Value, type));
}
return notification;
@ -735,7 +735,7 @@ namespace Avalonia
ThrowNotRegistered(property);
}
if (!TypeUtilities.TryCast(property.PropertyType, value, out value))
if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value))
{
throw new ArgumentException(string.Format(
"Invalid value for Property '{0}': '{1}' ({2})",

2
src/Avalonia.Base/AvaloniaProperty.cs

@ -476,7 +476,7 @@ namespace Avalonia
/// <returns>True if the value is valid, otherwise false.</returns>
public bool IsValidValue(object value)
{
return TypeUtilities.TryCast(PropertyType, value, out value);
return TypeUtilities.TryConvertImplicit(PropertyType, value, out value);
}
/// <summary>

2
src/Avalonia.Base/PriorityValue.cs

@ -249,7 +249,7 @@ namespace Avalonia
value = (notification.HasValue) ? notification.Value : null;
}
if (TypeUtilities.TryCast(_valueType, value, out castValue))
if (TypeUtilities.TryConvertImplicit(_valueType, value, out castValue))
{
var old = _value;

237
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -14,17 +14,61 @@ namespace Avalonia.Utilities
/// </summary>
public static class TypeUtilities
{
private static readonly Dictionary<Type, List<Type>> Conversions = new Dictionary<Type, List<Type>>()
private static int[] Conversions =
{
{ typeof(decimal), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } },
{ typeof(double), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
{ typeof(float), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
{ typeof(ulong), new List<Type> { typeof(byte), typeof(ushort), typeof(uint), typeof(char) } },
{ typeof(long), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char) } },
{ typeof(uint), new List<Type> { typeof(byte), typeof(ushort), typeof(char) } },
{ typeof(int), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char) } },
{ typeof(ushort), new List<Type> { typeof(byte), typeof(char) } },
{ typeof(short), new List<Type> { typeof(byte) } }
0b101111111111101, // Boolean
0b100001111111110, // Char
0b101111111111111, // SByte
0b101111111111111, // Byte
0b101111111111111, // Int16
0b101111111111111, // UInt16
0b101111111111111, // Int32
0b101111111111111, // UInt32
0b101111111111111, // Int64
0b101111111111111, // UInt64
0b101111111111101, // Single
0b101111111111101, // Double
0b101111111111101, // Decimal
0b110000000000000, // DateTime
0b111111111111111, // String
};
private static int[] ImplicitConversions =
{
0b000000000000001, // Boolean
0b001110111100010, // Char
0b001110101010100, // SByte
0b001111111111000, // Byte
0b001110101010000, // Int16
0b001111111100000, // UInt16
0b001110101000000, // Int32
0b001111110000000, // UInt32
0b001110100000000, // Int64
0b001111000000000, // UInt64
0b000110000000000, // Single
0b000100000000000, // Double
0b001000000000000, // Decimal
0b010000000000000, // DateTime
0b100000000000000, // String
};
private static Type[] InbuiltTypes =
{
typeof(Boolean),
typeof(Char),
typeof(SByte),
typeof(Byte),
typeof(Int16),
typeof(UInt16),
typeof(Int32),
typeof(UInt32),
typeof(Int64),
typeof(UInt64),
typeof(Single),
typeof(Double),
typeof(Decimal),
typeof(DateTime),
typeof(String),
};
private static readonly Type[] NumericTypes = new[]
@ -54,49 +98,104 @@ namespace Avalonia.Utilities
}
/// <summary>
/// Try to cast a value to a type, using implicit conversions if possible.
/// Try to convert a value to a type by any means possible.
/// </summary>
/// <param name="to">The type to cast to.</param>
/// <param name="value">The value to cast.</param>
/// <param name="culture">The culture to use.</param>
/// <param name="result">If sucessful, contains the cast value.</param>
/// <returns>True if the cast was sucessful, otherwise false.</returns>
public static bool TryCast(Type to, object value, out object result)
public static bool TryConvert(Type to, object value, CultureInfo culture, out object result)
{
Contract.Requires<ArgumentNullException>(to != null);
if (value == null)
{
result = null;
return AcceptsNull(to);
}
var from = value.GetType();
if (value == AvaloniaProperty.UnsetValue)
{
result = value;
return true;
}
else if (to.GetTypeInfo().IsAssignableFrom(from.GetTypeInfo()))
var from = value.GetType();
var fromTypeInfo = from.GetTypeInfo();
var toTypeInfo = to.GetTypeInfo();
if (toTypeInfo.IsAssignableFrom(fromTypeInfo))
{
result = value;
return true;
}
else if (Conversions.ContainsKey(to) && Conversions[to].Contains(from))
if (to == typeof(string))
{
result = Convert.ChangeType(value, to);
result = Convert.ToString(value);
return true;
}
else
if (toTypeInfo.IsEnum && from == typeof(string))
{
if (Enum.IsDefined(to, (string)value))
{
result = Enum.Parse(to, (string)value);
return true;
}
}
if (!fromTypeInfo.IsEnum && toTypeInfo.IsEnum)
{
var cast = from.GetRuntimeMethods()
.FirstOrDefault(m => m.Name == "op_Implicit" && m.ReturnType == to);
result = null;
if (TryConvert(Enum.GetUnderlyingType(to), value, culture, out object enumValue))
{
result = Enum.ToObject(to, enumValue);
return true;
}
}
if (cast != null)
if (fromTypeInfo.IsEnum && IsNumeric(to))
{
try
{
result = cast.Invoke(null, new[] { value });
result = Convert.ChangeType((int)value, to, culture);
return true;
}
catch
{
result = null;
return false;
}
}
var convertableFrom = Array.IndexOf(InbuiltTypes, from);
var convertableTo = Array.IndexOf(InbuiltTypes, to);
if (convertableFrom != -1 && convertableTo != -1)
{
if ((Conversions[convertableFrom] & 1 << convertableTo) != 0)
{
try
{
result = Convert.ChangeType(value, to, culture);
return true;
}
catch
{
result = null;
return false;
}
}
}
var cast = from.GetRuntimeMethods()
.FirstOrDefault(m => (m.Name == "op_Implicit" || m.Name == "op_Explicit") && m.ReturnType == to);
if (cast != null)
{
result = cast.Invoke(null, new[] { value });
return true;
}
result = null;
@ -104,15 +203,14 @@ namespace Avalonia.Utilities
}
/// <summary>
/// Try to convert a value to a type, using <see cref="System.Convert"/> if possible,
/// otherwise using <see cref="TryCast(Type, object, out object)"/>.
/// Try to convert a value to a type using the implicit conversions allowed by the C#
/// language.
/// </summary>
/// <param name="to">The type to cast to.</param>
/// <param name="value">The value to cast.</param>
/// <param name="culture">The culture to use.</param>
/// <param name="result">If sucessful, contains the cast value.</param>
/// <returns>True if the cast was sucessful, otherwise false.</returns>
public static bool TryConvert(Type to, object value, CultureInfo culture, out object result)
public static bool TryConvertImplicit(Type to, object value, out object result)
{
if (value == null)
{
@ -120,54 +218,44 @@ namespace Avalonia.Utilities
return AcceptsNull(to);
}
var from = value.GetType();
if (value == AvaloniaProperty.UnsetValue)
{
result = value;
return true;
}
if (to.GetTypeInfo().IsAssignableFrom(from.GetTypeInfo()))
{
result = value;
return true;
}
var from = value.GetType();
var fromTypeInfo = from.GetTypeInfo();
var toTypeInfo = to.GetTypeInfo();
if (to == typeof(string))
if (toTypeInfo.IsAssignableFrom(fromTypeInfo))
{
result = Convert.ToString(value);
result = value;
return true;
}
if (to.GetTypeInfo().IsEnum && from == typeof(string))
{
if (Enum.IsDefined(to, (string)value))
{
result = Enum.Parse(to, (string)value);
return true;
}
}
bool containsFrom = Conversions.ContainsKey(from);
bool containsTo = Conversions.ContainsKey(to);
var convertableFrom = Array.IndexOf(InbuiltTypes, from);
var convertableTo = Array.IndexOf(InbuiltTypes, to);
if ((containsFrom && containsTo) || (from == typeof(string) && containsTo))
if (convertableFrom != -1 && convertableTo != -1)
{
try
{
result = Convert.ChangeType(value, to, culture);
return true;
}
catch
if ((ImplicitConversions[convertableFrom] & 1 << convertableTo) != 0)
{
result = null;
return false;
try
{
result = Convert.ChangeType(value, to, CultureInfo.InvariantCulture);
return true;
}
catch
{
result = null;
return false;
}
}
}
var cast = from.GetRuntimeMethods()
.FirstOrDefault(m => (m.Name == "op_Implicit" || m.Name == "op_Explicit") && m.ReturnType == to);
.FirstOrDefault(m => m.Name == "op_Implicit" && m.ReturnType == to);
if (cast != null)
{
@ -180,29 +268,28 @@ namespace Avalonia.Utilities
}
/// <summary>
/// Casts a value to a type, returning the default for that type if the value could not be
/// cast.
/// Convert a value to a type by any means possible, returning the default for that type
/// if the value could not be converted.
/// </summary>
/// <param name="value">The value to cast.</param>
/// <param name="type">The type to cast to..</param>
/// <param name="culture">The culture to use.</param>
/// <returns>A value of <paramref name="type"/>.</returns>
public static object CastOrDefault(object value, Type type)
public static object ConvertOrDefault(object value, Type type, CultureInfo culture)
{
var typeInfo = type.GetTypeInfo();
object result;
return TryConvert(type, value, culture, out object result) ? result : Default(type);
}
if (TypeUtilities.TryCast(type, value, out result))
{
return result;
}
else if (typeInfo.IsValueType)
{
return Activator.CreateInstance(type);
}
else
{
return null;
}
/// <summary>
/// Convert a value to a type using the implicit conversions allowed by the C# language or
/// return the default for the type if the value could not be converted.
/// </summary>
/// <param name="value">The value to cast.</param>
/// <param name="type">The type to cast to..</param>
/// <returns>A value of <paramref name="type"/>.</returns>
public static object ConvertImplicitOrDefault(object value, Type type)
{
return TryConvertImplicit(type, value, out object result) ? result : Default(type);
}
/// <summary>

2
src/Avalonia.Visuals/VisualTree/VisualExtensions.cs

@ -189,7 +189,7 @@ namespace Avalonia.VisualTree
{
Contract.Requires<ArgumentNullException>(visual != null);
return visual.VisualRoot as IRenderRoot ?? visual.VisualRoot;
return visual as IRenderRoot ?? visual.VisualRoot;
}
/// <summary>

64
src/Markup/Avalonia.Markup/DefaultValueConverter.cs

@ -3,10 +3,7 @@
using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Utilities;
namespace Avalonia.Markup
@ -32,32 +29,28 @@ namespace Avalonia.Markup
/// <returns>The converted value.</returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
object result;
if (value != null &&
(TypeUtilities.TryConvert(targetType, value, culture, out result) ||
TryConvertEnum(value, targetType, culture, out result)))
if (value == null)
{
return result;
return AvaloniaProperty.UnsetValue;
}
if (value != null)
if (TypeUtilities.TryConvert(targetType, value, culture, out object result))
{
string message;
return result;
}
if (TypeUtilities.IsNumeric(targetType))
{
message = $"'{value}' is not a valid number.";
}
else
{
message = $"Could not convert '{value}' to '{targetType.Name}'.";
}
string message;
return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error);
if (TypeUtilities.IsNumeric(targetType))
{
message = $"'{value}' is not a valid number.";
}
else
{
message = $"Could not convert '{value}' to '{targetType.Name}'.";
}
return AvaloniaProperty.UnsetValue;
return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error);
}
/// <summary>
@ -72,34 +65,5 @@ namespace Avalonia.Markup
{
return Convert(value, targetType, parameter, culture);
}
private bool TryConvertEnum(object value, Type targetType, CultureInfo cultur, out object result)
{
var valueTypeInfo = value.GetType().GetTypeInfo();
var targetTypeInfo = targetType.GetTypeInfo();
if (valueTypeInfo.IsEnum && !targetTypeInfo.IsEnum)
{
var enumValue = (int)value;
if (TypeUtilities.TryCast(targetType, enumValue, out result))
{
return true;
}
}
else if (!valueTypeInfo.IsEnum && targetTypeInfo.IsEnum)
{
object intValue;
if (TypeUtilities.TryCast(typeof(int), value, out intValue))
{
result = Enum.ToObject(targetType, intValue);
return true;
}
}
result = null;
return false;
}
}
}

4
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -285,7 +285,7 @@ namespace Avalonia.Skia
paint.StrokeCap = SKStrokeCap.Butt;
if (pen.LineJoin == PenLineJoin.Miter)
paint.StrokeJoin = SKStrokeJoin.Mitter;
paint.StrokeJoin = SKStrokeJoin.Miter;
else if (pen.LineJoin == PenLineJoin.Round)
paint.StrokeJoin = SKStrokeJoin.Round;
else
@ -397,7 +397,7 @@ namespace Avalonia.Skia
public void PopOpacityMask()
{
Canvas.SaveLayer(new SKPaint { XferMode = SKXferMode.DstIn });
Canvas.SaveLayer(new SKPaint { BlendMode = SKBlendMode.DstIn });
using (var paintWrapper = maskStack.Pop())
{
Canvas.DrawPaint(paintWrapper.Paint);

2
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -42,7 +42,7 @@ namespace Avalonia.Skia
_paint.Typeface = skiaTypeface;
_paint.TextSize = (float)(typeface?.FontSize ?? 12);
_paint.TextAlign = textAlignment.ToSKTextAlign();
_paint.XferMode = SKXferMode.Src;
_paint.BlendMode = SKBlendMode.Src;
_wrapping = wrapping;
_constraint = constraint;

17
tests/Avalonia.Markup.Xaml.UnitTests/Parsers/SelectorParserTests.cs

@ -0,0 +1,17 @@
using System;
using Avalonia.Controls;
using Avalonia.Markup.Xaml.Parsers;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Parsers
{
public class SelectorParserTests
{
[Fact]
public void Parses_Boolean_Property_Selector()
{
var target = new SelectorParser((type, ns) => typeof(TextBlock));
var result = target.Parse("TextBlock[IsPointerOver=True]");
}
}
}
Loading…
Cancel
Save