From c8363ddeb715efafb41e6f8eebf31b8d61bd0433 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 31 Jan 2019 11:05:19 +0100 Subject: [PATCH 01/45] Supply more detailed exception details. So we can display better errors in a designer. --- .../Remote/RemoteDesignerEntryPoint.cs | 12 +++++++++++- src/Avalonia.Remote.Protocol/DesignMessages.cs | 8 ++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 09196e4fb7..67a93f3c9c 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -8,6 +8,7 @@ using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Designer; using Avalonia.Remote.Protocol.Viewport; using Avalonia.Threading; +using Portable.Xaml; namespace Avalonia.DesignerSupport.Remote { @@ -204,9 +205,18 @@ namespace Avalonia.DesignerSupport.Remote } catch (Exception e) { + var xamlException = e as XamlException; + s_transport.Send(new UpdateXamlResultMessage { - Error = e.ToString() + Error = e.ToString(), + Exception = new ExceptionDetails + { + ExceptionType = e.GetType().FullName, + Message = e.Message.ToString(), + LineNumber = xamlException?.LineNumber, + LinePosition = xamlException?.LinePosition, + } }); } } diff --git a/src/Avalonia.Remote.Protocol/DesignMessages.cs b/src/Avalonia.Remote.Protocol/DesignMessages.cs index f70bcef6b3..5ff16c574d 100644 --- a/src/Avalonia.Remote.Protocol/DesignMessages.cs +++ b/src/Avalonia.Remote.Protocol/DesignMessages.cs @@ -15,6 +15,7 @@ namespace Avalonia.Remote.Protocol.Designer { public string Error { get; set; } public string Handle { get; set; } + public ExceptionDetails Exception { get; set; } } [AvaloniaRemoteMessageGuid("854887CF-2694-4EB6-B499-7461B6FB96C7")] @@ -23,4 +24,11 @@ namespace Avalonia.Remote.Protocol.Designer public string SessionId { get; set; } } + public class ExceptionDetails + { + public string ExceptionType { get; set; } + public string Message { get; set; } + public int? LineNumber { get; set; } + public int? LinePosition { get; set; } + } } From 8caa0d2924aa91b4cab6b1e0673d02e4c8c863f5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 31 Jan 2019 11:06:10 +0100 Subject: [PATCH 02/45] Strong name sign Avalonia.Remote.Protocol. It needs to be signed to be used in VS. --- .../Avalonia.Remote.Protocol.csproj | 2 ++ src/Avalonia.Remote.Protocol/Key.snk | Bin 0 -> 596 bytes 2 files changed, 2 insertions(+) create mode 100644 src/Avalonia.Remote.Protocol/Key.snk diff --git a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj index 871c9cd995..6684772bfe 100644 --- a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj +++ b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj @@ -2,6 +2,8 @@ netstandard2.0 AVALONIA_REMOTE_PROTOCOL;$(DefineConstants) + true + Key.snk diff --git a/src/Avalonia.Remote.Protocol/Key.snk b/src/Avalonia.Remote.Protocol/Key.snk new file mode 100644 index 0000000000000000000000000000000000000000..d6cbc36f52ccda288283effe3294eac82a5c7ef7 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097%7Y|vta8^3KR$Smx;K{fFS_SHi*Psw3 zJkx=j;*YrY^86pNz5vg5O~4U_MzBsH{`RYL9%T;+mhYMfmF$a-hma@g6C=)}fn}{@ zI6+_-6SrY={k8i>Zro$$X_F^INltv6z|DqkHoVCrb@d@)Ynl ziDFw_@9Kk?>cNGZ zkCmu~lKW60E{OV*o5~ku8U4_BVgA~yhp_&l8GDAo5|W{$&Z?`@AZ@_1&~jq@i>hZp zfr$zNo<&wCV%9Y16^ult`xUXBU{dwnHju#G_+DC2%24bQ6(nW*Ru#AksAm} zVrGJ%!vumsmQv4#B#c13373GWA)F8F&Jcl|4?#CU|8T13H{?NW1^lSD_)<`0%4AFb zT{Zy{*o=br4QtNUt*gZX42(6>YX3sMl5R$?_iRW)5tpm!JF-;s%CmTExp4_Y))=1A z5!R5C0%+Tm4TlE_P);~#r&{t`3qauN`OdjpTZIT$iHJqcKU?P+59wR{R01ve4D1B~ zFg7f2wt{GtEMYmt)ZZ)c>-o!;2RLaY_uLW_ zs%cQ6S|W66#6C;m>k<*}q>-#musb}vAeE695xEBO7zyW1c9{!H`NE?Kb$f6CtyJ66 iSF5$`(ApW(ItQ!B9xp{O{xF;%!Gd-<-CHQkxaEbh4H%~Y literal 0 HcmV?d00001 From 718c01f206c43214238f9cdf96979a733dc78f6c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 31 Jan 2019 11:05:19 +0100 Subject: [PATCH 03/45] Supply more detailed exception details. So we can display better errors in a designer. --- .../Remote/RemoteDesignerEntryPoint.cs | 12 +++++++++++- src/Avalonia.Remote.Protocol/DesignMessages.cs | 8 ++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 09196e4fb7..67a93f3c9c 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -8,6 +8,7 @@ using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Designer; using Avalonia.Remote.Protocol.Viewport; using Avalonia.Threading; +using Portable.Xaml; namespace Avalonia.DesignerSupport.Remote { @@ -204,9 +205,18 @@ namespace Avalonia.DesignerSupport.Remote } catch (Exception e) { + var xamlException = e as XamlException; + s_transport.Send(new UpdateXamlResultMessage { - Error = e.ToString() + Error = e.ToString(), + Exception = new ExceptionDetails + { + ExceptionType = e.GetType().FullName, + Message = e.Message.ToString(), + LineNumber = xamlException?.LineNumber, + LinePosition = xamlException?.LinePosition, + } }); } } diff --git a/src/Avalonia.Remote.Protocol/DesignMessages.cs b/src/Avalonia.Remote.Protocol/DesignMessages.cs index f70bcef6b3..5ff16c574d 100644 --- a/src/Avalonia.Remote.Protocol/DesignMessages.cs +++ b/src/Avalonia.Remote.Protocol/DesignMessages.cs @@ -15,6 +15,7 @@ namespace Avalonia.Remote.Protocol.Designer { public string Error { get; set; } public string Handle { get; set; } + public ExceptionDetails Exception { get; set; } } [AvaloniaRemoteMessageGuid("854887CF-2694-4EB6-B499-7461B6FB96C7")] @@ -23,4 +24,11 @@ namespace Avalonia.Remote.Protocol.Designer public string SessionId { get; set; } } + public class ExceptionDetails + { + public string ExceptionType { get; set; } + public string Message { get; set; } + public int? LineNumber { get; set; } + public int? LinePosition { get; set; } + } } From f32832878e9729a50dbaba071440722aa93a411c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 31 Jan 2019 11:06:10 +0100 Subject: [PATCH 04/45] Strong name sign Avalonia.Remote.Protocol. It needs to be signed to be used in VS. --- .../Avalonia.Remote.Protocol.csproj | 2 ++ src/Avalonia.Remote.Protocol/Key.snk | Bin 0 -> 596 bytes 2 files changed, 2 insertions(+) create mode 100644 src/Avalonia.Remote.Protocol/Key.snk diff --git a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj index 871c9cd995..6684772bfe 100644 --- a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj +++ b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj @@ -2,6 +2,8 @@ netstandard2.0 AVALONIA_REMOTE_PROTOCOL;$(DefineConstants) + true + Key.snk diff --git a/src/Avalonia.Remote.Protocol/Key.snk b/src/Avalonia.Remote.Protocol/Key.snk new file mode 100644 index 0000000000000000000000000000000000000000..d6cbc36f52ccda288283effe3294eac82a5c7ef7 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097%7Y|vta8^3KR$Smx;K{fFS_SHi*Psw3 zJkx=j;*YrY^86pNz5vg5O~4U_MzBsH{`RYL9%T;+mhYMfmF$a-hma@g6C=)}fn}{@ zI6+_-6SrY={k8i>Zro$$X_F^INltv6z|DqkHoVCrb@d@)Ynl ziDFw_@9Kk?>cNGZ zkCmu~lKW60E{OV*o5~ku8U4_BVgA~yhp_&l8GDAo5|W{$&Z?`@AZ@_1&~jq@i>hZp zfr$zNo<&wCV%9Y16^ult`xUXBU{dwnHju#G_+DC2%24bQ6(nW*Ru#AksAm} zVrGJ%!vumsmQv4#B#c13373GWA)F8F&Jcl|4?#CU|8T13H{?NW1^lSD_)<`0%4AFb zT{Zy{*o=br4QtNUt*gZX42(6>YX3sMl5R$?_iRW)5tpm!JF-;s%CmTExp4_Y))=1A z5!R5C0%+Tm4TlE_P);~#r&{t`3qauN`OdjpTZIT$iHJqcKU?P+59wR{R01ve4D1B~ zFg7f2wt{GtEMYmt)ZZ)c>-o!;2RLaY_uLW_ zs%cQ6S|W66#6C;m>k<*}q>-#musb}vAeE695xEBO7zyW1c9{!H`NE?Kb$f6CtyJ66 iSF5$`(ApW(ItQ!B9xp{O{xF;%!Gd-<-CHQkxaEbh4H%~Y literal 0 HcmV?d00001 From ba316b89333cf0ebffcdd940f0facd3756510990 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Feb 2019 14:51:44 +0100 Subject: [PATCH 05/45] Updated Portable.Xaml. --- .../Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github index ab55261737..b9f886b93a 160000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github @@ -1 +1 @@ -Subproject commit ab5526173722b8988bc5ca3c03c8752ce89c0975 +Subproject commit b9f886b93ab28dd69722e72ef8cb6c33889b3749 From 521c9a38414ca4a9e80c9619f23bf1da7460f083 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Feb 2019 18:17:02 +0100 Subject: [PATCH 06/45] Updated Portable.Xaml. To get better line/column info for errors. --- .../Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github index b9f886b93a..452ced4782 160000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github @@ -1 +1 @@ -Subproject commit b9f886b93ab28dd69722e72ef8cb6c33889b3749 +Subproject commit 452ced47823d963c1e2d0ad809d0327c9e9b5247 From 03e84dea7bf86d5133a9ed79891c24899e0d771a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 13 Feb 2019 13:11:53 +0100 Subject: [PATCH 07/45] Ignore missing events in the designer. Do this by registering a special type converter for EventInfo, and add in the plumbing for Portable.Xaml to use that type converter instead of its own (internal) `EventConverter`. --- .../DesignWindowLoader.cs | 5 ++ .../DesignerEventConverter.cs | 87 +++++++++++++++++++ .../AvaloniaMemberAttributeProvider.cs | 14 ++- .../PortableXaml/AvaloniaXamlType.cs | 2 +- 4 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.DesignerSupport/DesignerEventConverter.cs diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 8fee31408f..2df171295f 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -12,6 +12,11 @@ namespace Avalonia.DesignerSupport { public class DesignWindowLoader { + static DesignWindowLoader() + { + AvaloniaTypeConverters.Register(typeof(EventInfo), typeof(DesignerEventConverter)); + } + public static Window LoadDesignerWindow(string xaml, string assemblyPath, string xamlFileProjectPath) { Window window; diff --git a/src/Avalonia.DesignerSupport/DesignerEventConverter.cs b/src/Avalonia.DesignerSupport/DesignerEventConverter.cs new file mode 100644 index 0000000000..b420448698 --- /dev/null +++ b/src/Avalonia.DesignerSupport/DesignerEventConverter.cs @@ -0,0 +1,87 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Portable.Xaml; + +namespace Avalonia.DesignerSupport +{ + internal class DesignerEventConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + var text = value as string; + if (text != null) + { + var rootObjectProvider = context.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider; + var destinationTypeProvider = context.GetService(typeof(IDestinationTypeProvider)) as IDestinationTypeProvider; + if (rootObjectProvider != null && destinationTypeProvider != null) + { + var target = rootObjectProvider.RootObject; + var eventType = destinationTypeProvider.GetDestinationType(); + var eventParameters = eventType.GetRuntimeMethods().First(r => r.Name == "Invoke").GetParameters(); + // go in reverse to match System.Xaml behaviour + var methods = target.GetType().GetRuntimeMethods().Reverse(); + + // find based on exact match parameter types first + foreach (var method in methods) + { + if (method.Name != text) + continue; + var parameters = method.GetParameters(); + if (eventParameters.Length != parameters.Length) + continue; + if (parameters.Length == 0) + return method.CreateDelegate(eventType, target); + + for (int i = 0; i < parameters.Length; i++) + { + var param = parameters[i]; + var eventParam = eventParameters[i]; + if (param.ParameterType != eventParam.ParameterType) + break; + if (i == parameters.Length - 1) + return method.CreateDelegate(eventType, target); + } + } + + // EnhancedXaml: Find method with compatible base class parameters + foreach (var method in methods) + { + if (method.Name != text) + continue; + var parameters = method.GetParameters(); + if (parameters.Length == 0 || eventParameters.Length != parameters.Length) + continue; + + for (int i = 0; i < parameters.Length; i++) + { + var param = parameters[i]; + var eventParam = eventParameters[i]; + if (!param.ParameterType.GetTypeInfo().IsAssignableFrom(eventParam.ParameterType.GetTypeInfo())) + break; + if (i == parameters.Length - 1) + return method.CreateDelegate(eventType, target); + } + } + + // We want to ignore missing events in the designer, so if event handler + // wasn't found create an empty delegate. + var lambdaExpression = Expression.Lambda( + eventType, + Expression.Empty(), + eventParameters.Select(x => Expression.Parameter(x.ParameterType))); + return lambdaExpression.Compile(); + } + } + return base.ConvertFrom(context, culture, value); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs index e9f6ba6945..529cbab938 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs @@ -49,6 +49,18 @@ namespace Avalonia.Markup.Xaml.PortableXaml //Portable.Xaml is not searching for Type Converter result = new TypeConverterAttribute(typeof(SetterValueTypeConverter)); } + else if (attributeType == typeof(TypeConverterAttribute) && _info is EventInfo) + { + // If a type converter for `EventInfo` is registered, then use that to convert + // event handler values. This is used by the designer to override the lookup + // for event handlers with a null handler. + var eventConverter = AvaloniaTypeConverters.GetTypeConverter(typeof(EventInfo)); + + if (eventConverter != null) + { + result = new TypeConverterAttribute(eventConverter); + } + } if (result == null) { @@ -68,4 +80,4 @@ namespace Avalonia.Markup.Xaml.PortableXaml private readonly MemberInfo _info; } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs index 2194223cb7..10cf716912 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs @@ -385,4 +385,4 @@ namespace Avalonia.Markup.Xaml.PortableXaml { } } -} \ No newline at end of file +} From 9ed23bbf89158792812965b9f39afeab71314ef9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 13 Feb 2019 16:33:27 +0100 Subject: [PATCH 08/45] Handle null modifiers collection. --- .../Remote/Server/RemoteServerTopLevelImpl.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index e1767fca36..028a78aea4 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -61,6 +61,11 @@ namespace Avalonia.Controls.Remote.Server { var result = InputModifiers.None; + if (modifiers == null) + { + return result; + } + foreach(var modifier in modifiers) { switch (modifier) From 2b34a2789213796454d62a7706dc55f3ddb37a27 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 13 Feb 2019 19:36:33 +0100 Subject: [PATCH 09/45] Return a frame even for an empty control. If the user has a control with no content then we've still got a valid control so return a frame with size 0. --- .../Remote/Server/RemoteServerTopLevelImpl.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index 028a78aea4..3fa0b108ec 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -270,11 +270,15 @@ namespace Avalonia.Controls.Remote.Server var bpp = fmt == ProtocolPixelFormat.Rgb565 ? 2 : 4; var data = new byte[width * height * bpp]; var handle = GCHandle.Alloc(data, GCHandleType.Pinned); + try { - _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, (PixelFormat)fmt, - null); - Paint?.Invoke(new Rect(0, 0, width, height)); + if (width > 0 && height > 0) + { + _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, (PixelFormat)fmt, + null); + Paint?.Invoke(new Rect(0, 0, width, height)); + } } finally { @@ -306,8 +310,7 @@ namespace Avalonia.Controls.Remote.Server return; } - if (ClientSize.Width < 1 || ClientSize.Height < 1) - return; + var format = ProtocolPixelFormat.Rgba8888; foreach(var fmt in _supportedFormats) if (fmt <= ProtocolPixelFormat.MaxValue) From 81510372b4a8d445e083b83ac988b8fb772acfbf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 13 Feb 2019 21:01:26 +0100 Subject: [PATCH 10/45] Don't invalidate a disposed TopLevelImpl. --- .../Embedding/Offscreen/OffscreenTopLevelImpl.cs | 4 +++- .../Remote/Server/RemoteServerTopLevelImpl.cs | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index d328e1ee88..9c53dc0c10 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -11,11 +11,13 @@ namespace Avalonia.Controls.Embedding.Offscreen { private double _scaling = 1; private Size _clientSize; + public IInputRoot InputRoot { get; private set; } + public bool IsDisposed { get; private set; } public virtual void Dispose() { - //No-op + IsDisposed = true; } public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root); diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index 3fa0b108ec..6293cbfbfd 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -331,8 +331,11 @@ namespace Avalonia.Controls.Remote.Server public override void Invalidate(Rect rect) { - _invalidated = true; - Dispatcher.UIThread.Post(RenderIfNeeded); + if (!IsDisposed) + { + _invalidated = true; + Dispatcher.UIThread.Post(RenderIfNeeded); + } } public override IMouseDevice MouseDevice { get; } = new MouseDevice(); From e3e42e8ae08fb91c8f7a28a3ea858eff709639ef Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Feb 2019 22:24:45 +0100 Subject: [PATCH 11/45] Fix bad XAML in DesignWindowLoader message. --- src/Avalonia.DesignerSupport/DesignWindowLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 2df171295f..68feb2edb5 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -56,8 +56,8 @@ namespace Avalonia.DesignerSupport { new TextBlock {Text = "Styles can't be previewed without Design.PreviewWith. Add"}, new TextBlock {Text = ""}, - new TextBlock {Text = " "}, - new TextBlock {Text = ""}, + new TextBlock {Text = " "}, + new TextBlock {Text = ""}, new TextBlock {Text = "before setters in your first Style"} } }; From 8d9c23446a8d4e7cde8416c913104d41e282508a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Feb 2019 23:35:36 +0100 Subject: [PATCH 12/45] Make Styles inherit from AvaloniaObject. This way we can add attached properties (such as `Design.PreviewWith`) to `Styles`. --- src/Avalonia.Styling/Styling/Styles.cs | 79 +++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 51499b737a..288cf35d08 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; using Avalonia.Controls; @@ -12,16 +14,17 @@ namespace Avalonia.Styling /// /// A style that consists of a number of child styles. /// - public class Styles : AvaloniaList, IStyle, ISetStyleParent + public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetStyleParent { private IResourceNode _parent; private IResourceDictionary _resources; + private AvaloniaList _styles = new AvaloniaList(); private Dictionary> _cache; public Styles() { - ResetBehavior = ResetBehavior.Remove; - this.ForEachItem( + _styles.ResetBehavior = ResetBehavior.Remove; + _styles.ForEachItem( x => { if (x.ResourceParent == null && x is ISetStyleParent setParent) @@ -57,9 +60,18 @@ namespace Avalonia.Styling () => { }); } + public event NotifyCollectionChangedEventHandler CollectionChanged + { + add => _styles.CollectionChanged += value; + remove => _styles.CollectionChanged -= value; + } + /// public event EventHandler ResourcesChanged; + /// + public int Count => _styles.Count; + /// public bool HasResources => _resources?.Count > 0 || this.Any(x => x.HasResources); @@ -94,6 +106,19 @@ namespace Avalonia.Styling /// IResourceNode IResourceNode.ResourceParent => _parent; + /// + bool ICollection.IsReadOnly => false; + + /// + IStyle IReadOnlyList.this[int index] => _styles[index]; + + /// + public IStyle this[int index] + { + get => _styles[index]; + set => _styles[index] = value; + } + /// /// Attaches the style to a control if the style's selector matches. /// @@ -172,6 +197,54 @@ namespace Avalonia.Styling return false; } + /// + public void AddRange(IEnumerable items) => _styles.AddRange(items); + + /// + public void InsertRange(int index, IEnumerable items) => _styles.InsertRange(index, items); + + /// + public void Move(int oldIndex, int newIndex) => _styles.Move(oldIndex, newIndex); + + /// + public void MoveRange(int oldIndex, int count, int newIndex) => _styles.MoveRange(oldIndex, count, newIndex); + + /// + public void RemoveAll(IEnumerable items) => _styles.RemoveAll(items); + + /// + public void RemoveRange(int index, int count) => _styles.RemoveRange(index, count); + + /// + public int IndexOf(IStyle item) => _styles.IndexOf(item); + + /// + public void Insert(int index, IStyle item) => _styles.Insert(index, item); + + /// + public void RemoveAt(int index) => _styles.RemoveAt(index); + + /// + public void Add(IStyle item) => _styles.Add(item); + + /// + public void Clear() => _styles.Clear(); + + /// + public bool Contains(IStyle item) => _styles.Contains(item); + + /// + public void CopyTo(IStyle[] array, int arrayIndex) => _styles.CopyTo(array, arrayIndex); + + /// + public bool Remove(IStyle item) => _styles.Remove(item); + + /// + public IEnumerator GetEnumerator() => _styles.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); + /// void ISetStyleParent.SetParent(IResourceNode parent) { From c438f9301e8d8a53269d485cdb05f551457afef7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Feb 2019 00:13:57 +0100 Subject: [PATCH 13/45] Make DesignWindowLoader expect PreviewWith on Styles. --- samples/ControlCatalog/SideBar.xaml | 8 ++++++++ src/Avalonia.Controls/Design.cs | 6 +++--- src/Avalonia.DesignerSupport/DesignWindowLoader.cs | 8 ++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 625b344b8c..fea55bcb07 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -1,6 +1,14 @@ + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs index dc73bef07a..96e8b49f89 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { @@ -8,6 +9,7 @@ namespace ControlCatalog.Pages public ContextMenuPage() { this.InitializeComponent(); + DataContext = new ContextMenuPageViewModel(); } private void InitializeComponent() diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml.cs b/samples/ControlCatalog/Pages/MenuPage.xaml.cs index 5a07cae89f..0a77607719 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml.cs +++ b/samples/ControlCatalog/Pages/MenuPage.xaml.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Windows.Input; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; using ReactiveUI; namespace ControlCatalog.Pages @@ -13,51 +14,7 @@ namespace ControlCatalog.Pages public MenuPage() { this.InitializeComponent(); - var vm = new MenuPageViewModel(); - - vm.MenuItems = new[] - { - new MenuItemViewModel - { - Header = "_File", - Items = new[] - { - new MenuItemViewModel { Header = "_Open...", Command = vm.OpenCommand }, - new MenuItemViewModel { Header = "Save", Command = vm.SaveCommand }, - new MenuItemViewModel { Header = "-" }, - new MenuItemViewModel - { - Header = "Recent", - Items = new[] - { - new MenuItemViewModel - { - Header = "File1.txt", - Command = vm.OpenRecentCommand, - CommandParameter = @"c:\foo\File1.txt" - }, - new MenuItemViewModel - { - Header = "File2.txt", - Command = vm.OpenRecentCommand, - CommandParameter = @"c:\foo\File2.txt" - }, - } - }, - } - }, - new MenuItemViewModel - { - Header = "_Edit", - Items = new[] - { - new MenuItemViewModel { Header = "_Copy" }, - new MenuItemViewModel { Header = "_Paste" }, - } - } - }; - - DataContext = vm; + DataContext = new MenuPageViewModel(); } private void InitializeComponent() @@ -65,51 +22,4 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } } - - public class MenuPageViewModel - { - public MenuPageViewModel() - { - OpenCommand = ReactiveCommand.CreateFromTask(Open); - SaveCommand = ReactiveCommand.Create(Save); - OpenRecentCommand = ReactiveCommand.Create(OpenRecent); - } - - public IReadOnlyList MenuItems { get; set; } - public ReactiveCommand OpenCommand { get; } - public ReactiveCommand SaveCommand { get; } - public ReactiveCommand OpenRecentCommand { get; } - - public async Task Open() - { - var dialog = new OpenFileDialog(); - var result = await dialog.ShowAsync(App.Current.MainWindow); - - if (result != null) - { - foreach (var path in result) - { - System.Diagnostics.Debug.WriteLine($"Opened: {path}"); - } - } - } - - public void Save() - { - System.Diagnostics.Debug.WriteLine("Save"); - } - - public void OpenRecent(string path) - { - System.Diagnostics.Debug.WriteLine($"Open recent: {path}"); - } - } - - public class MenuItemViewModel - { - public string Header { get; set; } - public ICommand Command { get; set; } - public object CommandParameter { get; set; } - public IList Items { get; set; } - } } diff --git a/samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs new file mode 100644 index 0000000000..d34e9af017 --- /dev/null +++ b/samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Reactive; +using System.Threading.Tasks; +using Avalonia.Controls; +using ReactiveUI; + +namespace ControlCatalog.ViewModels +{ + public class ContextMenuPageViewModel + { + public ContextMenuPageViewModel() + { + OpenCommand = ReactiveCommand.CreateFromTask(Open); + SaveCommand = ReactiveCommand.Create(Save); + OpenRecentCommand = ReactiveCommand.Create(OpenRecent); + + MenuItems = new[] + { + new MenuItemViewModel { Header = "_Open...", Command = OpenCommand }, + new MenuItemViewModel { Header = "Save", Command = SaveCommand }, + new MenuItemViewModel { Header = "-" }, + new MenuItemViewModel + { + Header = "Recent", + Items = new[] + { + new MenuItemViewModel + { + Header = "File1.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File1.txt" + }, + new MenuItemViewModel + { + Header = "File2.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File2.txt" + }, + } + }, + }; + } + + public IReadOnlyList MenuItems { get; set; } + public ReactiveCommand OpenCommand { get; } + public ReactiveCommand SaveCommand { get; } + public ReactiveCommand OpenRecentCommand { get; } + + public async Task Open() + { + var dialog = new OpenFileDialog(); + var result = await dialog.ShowAsync(App.Current.MainWindow); + + if (result != null) + { + foreach (var path in result) + { + System.Diagnostics.Debug.WriteLine($"Opened: {path}"); + } + } + } + + public void Save() + { + System.Diagnostics.Debug.WriteLine("Save"); + } + + public void OpenRecent(string path) + { + System.Diagnostics.Debug.WriteLine($"Open recent: {path}"); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/MenuItemViewModel.cs b/samples/ControlCatalog/ViewModels/MenuItemViewModel.cs new file mode 100644 index 0000000000..1e4a764efc --- /dev/null +++ b/samples/ControlCatalog/ViewModels/MenuItemViewModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Windows.Input; + +namespace ControlCatalog.ViewModels +{ + public class MenuItemViewModel + { + public string Header { get; set; } + public ICommand Command { get; set; } + public object CommandParameter { get; set; } + public IList Items { get; set; } + } +} diff --git a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs new file mode 100644 index 0000000000..038f3574cc --- /dev/null +++ b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Reactive; +using System.Threading.Tasks; +using Avalonia.Controls; +using ReactiveUI; + +namespace ControlCatalog.ViewModels +{ + public class MenuPageViewModel + { + public MenuPageViewModel() + { + OpenCommand = ReactiveCommand.CreateFromTask(Open); + SaveCommand = ReactiveCommand.Create(Save); + OpenRecentCommand = ReactiveCommand.Create(OpenRecent); + + MenuItems = new[] + { + new MenuItemViewModel + { + Header = "_File", + Items = new[] + { + new MenuItemViewModel { Header = "_Open...", Command = OpenCommand }, + new MenuItemViewModel { Header = "Save", Command = SaveCommand }, + new MenuItemViewModel { Header = "-" }, + new MenuItemViewModel + { + Header = "Recent", + Items = new[] + { + new MenuItemViewModel + { + Header = "File1.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File1.txt" + }, + new MenuItemViewModel + { + Header = "File2.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File2.txt" + }, + } + }, + } + }, + new MenuItemViewModel + { + Header = "_Edit", + Items = new[] + { + new MenuItemViewModel { Header = "_Copy" }, + new MenuItemViewModel { Header = "_Paste" }, + } + } + }; + } + + public IReadOnlyList MenuItems { get; set; } + public ReactiveCommand OpenCommand { get; } + public ReactiveCommand SaveCommand { get; } + public ReactiveCommand OpenRecentCommand { get; } + + public async Task Open() + { + var dialog = new OpenFileDialog(); + var result = await dialog.ShowAsync(App.Current.MainWindow); + + if (result != null) + { + foreach (var path in result) + { + System.Diagnostics.Debug.WriteLine($"Opened: {path}"); + } + } + } + + public void Save() + { + System.Diagnostics.Debug.WriteLine("Save"); + } + + public void OpenRecent(string path) + { + System.Diagnostics.Debug.WriteLine($"Open recent: {path}"); + } + } +} From fbfd5d80d111256d9bb64e62e9dec32d84a281fc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 Feb 2019 21:59:19 +0100 Subject: [PATCH 21/45] Added MenuBase. And use it as base class for both `Menu` and `ContextMenu`. --- src/Avalonia.Controls/ContextMenu.cs | 85 +++--------- src/Avalonia.Controls/Menu.cs | 151 +-------------------- src/Avalonia.Controls/MenuBase.cs | 193 +++++++++++++++++++++++++++ 3 files changed, 216 insertions(+), 213 deletions(-) create mode 100644 src/Avalonia.Controls/MenuBase.cs diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index a69152c42b..e71934912b 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,34 +1,29 @@ using System; -using System.Reactive.Linq; -using System.Linq; using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; using Avalonia.Controls.Platform; -using System.Collections.Generic; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.LogicalTree; -using Avalonia.Controls.Primitives; namespace Avalonia.Controls { - public class ContextMenu : SelectingItemsControl, IMenu + /// + /// A control context menu. + /// + public class ContextMenu : MenuBase { - private readonly IMenuInteractionHandler _interaction; - private bool _isOpen; + private static readonly ITemplate DefaultPanel = + new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical }); private Popup _popup; - /// - /// Defines the property. - /// - public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect(nameof(IsOpen), o => o.IsOpen); - /// /// Initializes a new instance of the class. /// public ContextMenu() { - _interaction = AvaloniaLocator.Current.GetService() ?? - new DefaultMenuInteractionHandler(); } /// @@ -36,10 +31,8 @@ namespace Avalonia.Controls /// /// The menu interaction handler. public ContextMenu(IMenuInteractionHandler interactionHandler) + : base(interactionHandler) { - Contract.Requires(interactionHandler != null); - - _interaction = interactionHandler; } /// @@ -47,44 +40,10 @@ namespace Avalonia.Controls /// static ContextMenu() { + ItemsPanelProperty.OverrideDefaultValue(typeof(ContextMenu), DefaultPanel); ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); } - /// - /// Gets a value indicating whether the popup is open - /// - public bool IsOpen => _isOpen; - - /// - IMenuInteractionHandler IMenu.InteractionHandler => _interaction; - - /// - IMenuItem IMenuElement.SelectedItem - { - get - { - var index = SelectedIndex; - return (index != -1) ? - (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : - null; - } - set - { - SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); - } - } - - /// - IEnumerable IMenuElement.SubItems - { - get - { - return ItemContainerGenerator.Containers - .Select(x => x.ContainerControl) - .OfType(); - } - } - /// /// Occurs when the value of the /// @@ -121,7 +80,7 @@ namespace Avalonia.Controls /// /// Opens the menu. /// - public void Open() => Open(null); + public override void Open() => Open(null); /// /// Opens a context menu on the specified control. @@ -140,20 +99,18 @@ namespace Avalonia.Controls }; _popup.Closed += PopupClosed; - _interaction.Attach(this); } ((ISetLogicalParent)_popup).SetParent(control); _popup.Child = this; _popup.IsOpen = true; - - SetAndRaise(IsOpenProperty, ref _isOpen, true); + IsOpen = true; } /// /// Closes the menu. /// - public void Close() + public override void Close() { if (_popup != null && _popup.IsVisible) { @@ -161,8 +118,7 @@ namespace Avalonia.Controls } SelectedIndex = -1; - - SetAndRaise(IsOpenProperty, ref _isOpen, false); + IsOpen = false; } private void PopupClosed(object sender, EventArgs e) @@ -176,7 +132,7 @@ namespace Avalonia.Controls i.IsSubMenuOpen = false; } - contextMenu._isOpen = false; + contextMenu.IsOpen = false; contextMenu.SelectedIndex = -1; } } @@ -186,7 +142,7 @@ namespace Avalonia.Controls var control = (Control)sender; var contextMenu = control.ContextMenu; - if (control.ContextMenu._isOpen) + if (control.ContextMenu.IsOpen) { if (contextMenu.CancelClosing()) return; @@ -218,10 +174,5 @@ namespace Avalonia.Controls ContextMenuOpening?.Invoke(this, eventArgs); return eventArgs.Cancel; } - - bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) - { - throw new NotImplementedException(); - } } } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 00fca385a0..290e16d8c8 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -1,56 +1,26 @@ // 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.Collections.Generic; -using System.Linq; -using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.LogicalTree; namespace Avalonia.Controls { /// /// A top-level menu control. /// - public class Menu : SelectingItemsControl, IFocusScope, IMainMenu, IMenu + public class Menu : MenuBase, IFocusScope, IMainMenu { - /// - /// Defines the property. - /// - public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsOpen), - o => o.IsOpen); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent MenuOpenedEvent = - RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent MenuClosedEvent = - RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); - private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); - private readonly IMenuInteractionHandler _interaction; - private bool _isOpen; /// /// Initializes a new instance of the class. /// public Menu() { - _interaction = AvaloniaLocator.Current.GetService() ?? - new DefaultMenuInteractionHandler(); } /// @@ -58,82 +28,17 @@ namespace Avalonia.Controls /// /// The menu interaction handler. public Menu(IMenuInteractionHandler interactionHandler) + : base(interactionHandler) { - Contract.Requires(interactionHandler != null); - - _interaction = interactionHandler; } - /// - /// Initializes static members of the class. - /// static Menu() { ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); - MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); - } - - /// - /// Gets a value indicating whether the menu is open. - /// - public bool IsOpen - { - get { return _isOpen; } - private set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } - } - - /// - IMenuInteractionHandler IMenu.InteractionHandler => _interaction; - - /// - IMenuItem IMenuElement.SelectedItem - { - get - { - var index = SelectedIndex; - return (index != -1) ? - (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : - null; - } - set - { - SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); - } } /// - IEnumerable IMenuElement.SubItems - { - get - { - return ItemContainerGenerator.Containers - .Select(x => x.ContainerControl) - .OfType(); - } - } - - /// - /// Occurs when a is opened. - /// - public event EventHandler MenuOpened - { - add { AddHandler(MenuOpenedEvent, value); } - remove { RemoveHandler(MenuOpenedEvent, value); } - } - - /// - /// Occurs when a is closed. - /// - public event EventHandler MenuClosed - { - add { AddHandler(MenuClosedEvent, value); } - remove { RemoveHandler(MenuClosedEvent, value); } - } - - /// - /// Closes the menu. - /// - public void Close() + public override void Close() { if (IsOpen) { @@ -153,10 +58,8 @@ namespace Avalonia.Controls } } - /// - /// Opens the menu in response to the Alt/F10 key. - /// - public void Open() + /// + public override void Open() { if (!IsOpen) { @@ -170,15 +73,6 @@ namespace Avalonia.Controls } } - /// - bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); - - /// - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); - } - /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -190,41 +84,6 @@ namespace Avalonia.Controls { inputRoot.AccessKeyHandler.MainMenu = this; } - - _interaction.Attach(this); - } - - /// - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - _interaction.Detach(this); - } - - /// - protected override void OnKeyDown(KeyEventArgs e) - { - // Don't handle here: let the interaction handler handle it. - } - - /// - /// Called when a submenu opens somewhere in the menu. - /// - /// The event args. - protected virtual void OnSubmenuOpened(RoutedEventArgs e) - { - if (e.Source is MenuItem menuItem && menuItem.Parent == this) - { - foreach (var child in this.GetLogicalChildren().OfType()) - { - if (child != menuItem && child.IsSubMenuOpen) - { - child.IsSubMenuOpen = false; - } - } - } - - IsOpen = true; } } } diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs new file mode 100644 index 0000000000..836e3609c4 --- /dev/null +++ b/src/Avalonia.Controls/MenuBase.cs @@ -0,0 +1,193 @@ +// 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.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Generators; +using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; + +namespace Avalonia.Controls +{ + /// + /// Base class for menu controls. + /// + public abstract class MenuBase : SelectingItemsControl, IMenu + { + /// + /// Defines the property. + /// + public static readonly DirectProperty IsOpenProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsOpen), + o => o.IsOpen); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent MenuOpenedEvent = + RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent MenuClosedEvent = + RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); + + private bool _isOpen; + + /// + /// Initializes a new instance of the class. + /// + public MenuBase() + { + InteractionHandler = AvaloniaLocator.Current.GetService() ?? + new DefaultMenuInteractionHandler(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The menu interaction handler. + public MenuBase(IMenuInteractionHandler interactionHandler) + { + Contract.Requires(interactionHandler != null); + + InteractionHandler = interactionHandler; + } + + /// + /// Initializes static members of the class. + /// + static MenuBase() + { + MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); + } + + /// + /// Gets a value indicating whether the menu is open. + /// + public bool IsOpen + { + get { return _isOpen; } + protected set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } + } + + /// + IMenuInteractionHandler IMenu.InteractionHandler => InteractionHandler; + + /// + IMenuItem IMenuElement.SelectedItem + { + get + { + var index = SelectedIndex; + return (index != -1) ? + (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : + null; + } + set + { + SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); + } + } + + /// + IEnumerable IMenuElement.SubItems + { + get + { + return ItemContainerGenerator.Containers + .Select(x => x.ContainerControl) + .OfType(); + } + } + + /// + /// Gets the interaction handler for the menu. + /// + protected IMenuInteractionHandler InteractionHandler { get; } + + /// + /// Occurs when a is opened. + /// + public event EventHandler MenuOpened + { + add { AddHandler(MenuOpenedEvent, value); } + remove { RemoveHandler(MenuOpenedEvent, value); } + } + + /// + /// Occurs when a is closed. + /// + public event EventHandler MenuClosed + { + add { AddHandler(MenuClosedEvent, value); } + remove { RemoveHandler(MenuClosedEvent, value); } + } + + /// + /// Closes the menu. + /// + public abstract void Close(); + + /// + /// Opens the menu. + /// + public abstract void Open(); + + /// + bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); + + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + // Don't handle here: let the interaction handler handle it. + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + InteractionHandler.Attach(this); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + InteractionHandler.Detach(this); + } + + /// + /// Called when a submenu opens somewhere in the menu. + /// + /// The event args. + protected virtual void OnSubmenuOpened(RoutedEventArgs e) + { + if (e.Source is MenuItem menuItem && menuItem.Parent == this) + { + foreach (var child in this.GetLogicalChildren().OfType()) + { + if (child != menuItem && child.IsSubMenuOpen) + { + child.IsSubMenuOpen = false; + } + } + } + + IsOpen = true; + } + } +} From a5f07f78e95f46771e6a15434d40e7c19ea7102d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 Feb 2019 22:27:12 +0100 Subject: [PATCH 22/45] Fixed context menu styling. --- src/Avalonia.Controls/ContextMenu.cs | 6 ++++++ src/Avalonia.Themes.Default/ContextMenu.xaml | 22 ++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index e71934912b..92f195b79e 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using System.Linq; using System.Reactive.Linq; +using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -121,6 +122,11 @@ namespace Avalonia.Controls IsOpen = false; } + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new MenuItemContainerGenerator(this); + } + private void PopupClosed(object sender, EventArgs e) { var contextMenu = (sender as Popup)?.Child as ContextMenu; diff --git a/src/Avalonia.Themes.Default/ContextMenu.xaml b/src/Avalonia.Themes.Default/ContextMenu.xaml index bfa26d3528..53d7c5abb4 100644 --- a/src/Avalonia.Themes.Default/ContextMenu.xaml +++ b/src/Avalonia.Themes.Default/ContextMenu.xaml @@ -10,12 +10,22 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}"> - + + + + + + - \ No newline at end of file + From 4a5e11f6aaee79a3183aa7d703b60c4fa912a971 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 27 Feb 2019 09:53:54 +0100 Subject: [PATCH 23/45] Fix keyboard interaction for ContextMenu. --- src/Avalonia.Controls/ContextMenu.cs | 7 ++ src/Avalonia.Controls/Menu.cs | 2 +- src/Avalonia.Controls/MenuBase.cs | 5 +- .../Platform/DefaultMenuInteractionHandler.cs | 80 +++++++++++-------- src/Avalonia.Input/InputElement.cs | 2 +- .../DefaultMenuInteractionHandlerTests.cs | 71 +++++++++------- 6 files changed, 102 insertions(+), 65 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 92f195b79e..92293a32d6 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -24,6 +24,7 @@ namespace Avalonia.Controls /// Initializes a new instance of the class. /// public ContextMenu() + : this(new DefaultMenuInteractionHandler(true)) { } @@ -99,6 +100,7 @@ namespace Avalonia.Controls ObeyScreenEdges = true }; + _popup.Opened += PopupOpened; _popup.Closed += PopupClosed; } @@ -127,6 +129,11 @@ namespace Avalonia.Controls return new MenuItemContainerGenerator(this); } + private void PopupOpened(object sender, EventArgs e) + { + Focus(); + } + private void PopupClosed(object sender, EventArgs e) { var contextMenu = (sender as Popup)?.Child as ContextMenu; diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 290e16d8c8..b0fb3f2b3b 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls /// /// A top-level menu control. /// - public class Menu : MenuBase, IFocusScope, IMainMenu + public class Menu : MenuBase, IMainMenu { private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index 836e3609c4..d6eb40360b 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls /// /// Base class for menu controls. /// - public abstract class MenuBase : SelectingItemsControl, IMenu + public abstract class MenuBase : SelectingItemsControl, IFocusScope, IMenu { /// /// Defines the property. @@ -46,8 +46,7 @@ namespace Avalonia.Controls /// public MenuBase() { - InteractionHandler = AvaloniaLocator.Current.GetService() ?? - new DefaultMenuInteractionHandler(); + InteractionHandler = new DefaultMenuInteractionHandler(false); } /// diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 6b03e67897..942104d61b 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -13,18 +13,21 @@ namespace Avalonia.Controls.Platform /// public class DefaultMenuInteractionHandler : IMenuInteractionHandler { + private readonly bool _isContextMenu; private IDisposable _inputManagerSubscription; private IRenderRoot _root; - public DefaultMenuInteractionHandler() - : this(Input.InputManager.Instance, DefaultDelayRun) + public DefaultMenuInteractionHandler(bool isContextMenu) + : this(isContextMenu, Input.InputManager.Instance, DefaultDelayRun) { } public DefaultMenuInteractionHandler( + bool isContextMenu, IInputManager inputManager, Action delayRun) { + _isContextMenu = isContextMenu; InputManager = inputManager; DelayRun = delayRun; } @@ -59,7 +62,7 @@ namespace Avalonia.Controls.Platform window.Deactivated += WindowDeactivated; } - _inputManagerSubscription = InputManager.Process.Subscribe(RawInput); + _inputManagerSubscription = InputManager?.Process.Subscribe(RawInput); } public virtual void Detach(IMenu menu) @@ -125,23 +128,16 @@ namespace Avalonia.Controls.Platform protected internal virtual void KeyDown(object sender, KeyEventArgs e) { - var item = GetMenuItem(e.Source as IControl); - - if (item != null) - { - KeyDown(item, e); - } + KeyDown(GetMenuItem(e.Source as IControl), e); } protected internal virtual void KeyDown(IMenuItem item, KeyEventArgs e) { - Contract.Requires(item != null); - switch (e.Key) { case Key.Up: case Key.Down: - if (item.IsTopLevel) + if (item?.IsTopLevel == true) { if (item.HasSubMenu && !item.IsSubMenuOpen) { @@ -156,7 +152,7 @@ namespace Avalonia.Controls.Platform break; case Key.Left: - if (item.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) + if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) { parent.Close(); parent.Focus(); @@ -169,7 +165,7 @@ namespace Avalonia.Controls.Platform break; case Key.Right: - if (!item.IsTopLevel && item.HasSubMenu) + if (item != null && !item.IsTopLevel && item.HasSubMenu) { Open(item, true); e.Handled = true; @@ -181,47 +177,65 @@ namespace Avalonia.Controls.Platform break; case Key.Enter: - if (!item.HasSubMenu) + if (item != null) { - Click(item); - } - else - { - Open(item, true); - } + if (!item.HasSubMenu) + { + Click(item); + } + else + { + Open(item, true); + } - e.Handled = true; + e.Handled = true; + } break; case Key.Escape: - if (item.Parent != null) + if (item?.Parent != null) { item.Parent.Close(); item.Parent.Focus(); - e.Handled = true; } + else + { + Menu.Close(); + } + + e.Handled = true; break; default: var direction = e.Key.ToNavigationDirection(); - if (direction.HasValue && item.Parent?.MoveSelection(direction.Value, true) == true) + if (direction.HasValue) { - // If the the parent is an IMenu which successfully moved its selection, - // and the current menu is open then close the current menu and open the - // new menu. - if (item.IsSubMenuOpen && item.Parent is IMenu) + if (item == null && _isContextMenu) { - item.Close(); - Open(item.Parent.SelectedItem, true); + if (Menu.MoveSelection(direction.Value, true) == true) + { + e.Handled = true; + } + } + else if (item.Parent?.MoveSelection(direction.Value, true) == true) + { + // If the the parent is an IMenu which successfully moved its selection, + // and the current menu is open then close the current menu and open the + // new menu. + if (item.IsSubMenuOpen && item.Parent is IMenu) + { + item.Close(); + Open(item.Parent.SelectedItem, true); + } + e.Handled = true; } - e.Handled = true; } break; } - if (!e.Handled && item.Parent is IMenuItem parentItem) + if (!e.Handled && item?.Parent is IMenuItem parentItem) { KeyDown(parentItem, e); } diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index e7fea94d3d..07e04486ec 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -375,7 +375,7 @@ namespace Avalonia.Input /// public void Focus() { - FocusManager.Instance.Focus(this); + FocusManager.Instance?.Focus(this); } /// diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index e17279013d..87b235dce7 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Up_Opens_MenuItem_With_SubMenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var e = new KeyEventArgs { Key = Key.Up, Source = item }; @@ -27,7 +27,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Down_Opens_MenuItem_With_SubMenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var e = new KeyEventArgs { Key = Key.Down, Source = item }; @@ -41,7 +41,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Right_Selects_Next_MenuItem() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(x => x.MoveSelection(NavigationDirection.Right, true) == true); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); var e = new KeyEventArgs { Key = Key.Right, Source = item }; @@ -55,7 +55,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Left_Selects_Previous_MenuItem() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(x => x.MoveSelection(NavigationDirection.Left, true) == true); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); var e = new KeyEventArgs { Key = Key.Left, Source = item }; @@ -69,7 +69,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Enter_On_Item_With_No_SubMenu_Causes_Click() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); var e = new KeyEventArgs { Key = Key.Enter, Source = item }; @@ -84,7 +84,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Enter_On_Item_With_SubMenu_Opens_SubMenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var e = new KeyEventArgs { Key = Key.Enter, Source = item }; @@ -99,7 +99,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Escape_Closes_Parent_Menu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); var e = new KeyEventArgs { Key = Key.Escape, Source = item }; @@ -113,7 +113,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerEnter_Opens_Item_When_Old_Item_Is_Open() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = Mock.Of(x => x.IsSubMenuOpen == true && @@ -141,7 +141,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerLeave_Deselects_Item_When_Menu_Not_Open() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; @@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerLeave_Doesnt_Deselect_Item_When_Menu_Open() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; @@ -175,7 +175,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Up_Selects_Previous_MenuItem() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var item = Mock.Of(x => x.Parent == parentItem); var e = new KeyEventArgs { Key = Key.Up, Source = item }; @@ -189,7 +189,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Down_Selects_Next_MenuItem() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var item = Mock.Of(x => x.Parent == parentItem); var e = new KeyEventArgs { Key = Key.Down, Source = item }; @@ -203,7 +203,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Left_Closes_Parent_SubMenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var parentItem = Mock.Of(x => x.HasSubMenu == true && x.IsSubMenuOpen == true); var item = Mock.Of(x => x.Parent == parentItem); var e = new KeyEventArgs { Key = Key.Left, Source = item }; @@ -218,7 +218,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Right_With_SubMenu_Items_Opens_SubMenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); var e = new KeyEventArgs { Key = Key.Right, Source = item }; @@ -233,7 +233,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Right_On_TopLevel_Child_Navigates_TopLevel_Selection() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var parentItem = Mock.Of(x => x.IsSubMenuOpen == true && @@ -263,7 +263,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Enter_On_Item_With_No_SubMenu_Causes_Click() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); @@ -279,7 +279,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Enter_On_Item_With_SubMenu_Opens_SubMenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); var e = new KeyEventArgs { Key = Key.Enter, Source = item }; @@ -294,7 +294,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Escape_Closes_Parent_MenuItem() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var item = Mock.Of(x => x.Parent == parentItem); var e = new KeyEventArgs { Key = Key.Escape, Source = item }; @@ -309,7 +309,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerEnter_Selects_Item() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); @@ -325,7 +325,7 @@ namespace Avalonia.Controls.UnitTests.Platform public void PointerEnter_Opens_Submenu_After_Delay() { var timer = new TestTimer(); - var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); + var target = new DefaultMenuInteractionHandler(false, null, timer.RunOnce); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); @@ -344,7 +344,7 @@ namespace Avalonia.Controls.UnitTests.Platform public void PointerEnter_Closes_Sibling_Submenu_After_Delay() { var timer = new TestTimer(); - var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); + var target = new DefaultMenuInteractionHandler(false, null, timer.RunOnce); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); @@ -365,7 +365,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerLeave_Deselects_Item() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); @@ -381,7 +381,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerLeave_Doesnt_Deselect_Sibling() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); @@ -398,7 +398,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerLeave_Doesnt_Deselect_Item_If_Pointer_Over_Submenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsPointerOverSubMenu == true); @@ -413,7 +413,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerReleased_On_Item_With_No_SubMenu_Causes_Click() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); @@ -430,7 +430,7 @@ namespace Avalonia.Controls.UnitTests.Platform public void Selection_Is_Correct_When_Pointer_Temporarily_Exits_Item_To_Select_SubItem() { var timer = new TestTimer(); - var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); + var target = new DefaultMenuInteractionHandler(false, null, timer.RunOnce); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); @@ -467,7 +467,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerPressed_On_Item_With_SubMenu_Causes_Opens_Submenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); @@ -481,6 +481,23 @@ namespace Avalonia.Controls.UnitTests.Platform } } + public class ContextMenu + { + [Fact] + public void Down_Selects_Selects_First_MenuItem_When_No_Selection() + { + var target = new DefaultMenuInteractionHandler(true); + var contextMenu = Mock.Of(x => x.MoveSelection(NavigationDirection.Down, true) == true); + var e = new KeyEventArgs { Key = Key.Down, Source = contextMenu }; + + target.Attach(contextMenu); + target.KeyDown(contextMenu, e); + + Mock.Get(contextMenu).Verify(x => x.MoveSelection(NavigationDirection.Down, true)); + Assert.True(e.Handled); + } + } + private class TestTimer { private Action _action; From ce3138efda2f01d73584d3cfcc008443a00ded07 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 27 Feb 2019 15:31:51 +0100 Subject: [PATCH 24/45] Don't create a schema context each time. The change in the previous commit to create a new `AvaloniaXamlSchemaContext` for each instance of `AvaloniaXamlLoader` had a negative performance effect. Instead, keep two instances of the schema context around: one for normal loading and one for design-time loading. --- .../AvaloniaXamlLoader.cs | 11 ++--- .../PortableXaml/AvaloniaXamlSchemaContext.cs | 45 +++++++++++++++---- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 800afb3976..a1f8bf6cf6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -23,13 +23,7 @@ namespace Avalonia.Markup.Xaml /// public class AvaloniaXamlLoader { - private readonly AvaloniaXamlSchemaContext _context = AvaloniaXamlSchemaContext.Create(); - - public bool IsDesignMode - { - get => _context.IsDesignMode; - set => _context.IsDesignMode = value; - } + public bool IsDesignMode { get; set; } /// /// Initializes a new instance of the class. @@ -171,7 +165,8 @@ namespace Avalonia.Markup.Xaml LocalAssembly = localAssembly }; - var reader = new XamlXmlReader(stream, _context, readerSettings); + var context = IsDesignMode ? AvaloniaXamlSchemaContext.DesignInstance : AvaloniaXamlSchemaContext.Instance; + var reader = new XamlXmlReader(stream, context, readerSettings); object result = LoadFromReader( reader, diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs index 9a493a85c0..326db884a6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs @@ -1,21 +1,48 @@ -using Avalonia.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Avalonia.Data; using Avalonia.Markup.Xaml.Context; -using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Markup.Xaml.Styling; using Portable.Xaml; -using Portable.Xaml.ComponentModel; -using System.ComponentModel; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; namespace Avalonia.Markup.Xaml.PortableXaml { internal class AvaloniaXamlSchemaContext : XamlSchemaContext { - public bool IsDesignMode { get; set; } + private static AvaloniaXamlSchemaContext s_instance; + private static AvaloniaXamlSchemaContext s_designInstance; + + public static AvaloniaXamlSchemaContext Instance + { + get + { + if (s_instance == null) + { + s_instance = Create(); + } + + return s_instance; + } + } + + public static AvaloniaXamlSchemaContext DesignInstance + { + get + { + if (s_designInstance == null) + { + s_designInstance = Create(); + s_designInstance.IsDesignMode = true; + } + + return s_designInstance; + } + } + + public bool IsDesignMode { get; private set; } public static AvaloniaXamlSchemaContext Create(IRuntimeTypeProvider typeProvider = null) { return new AvaloniaXamlSchemaContext(typeProvider ?? new AvaloniaRuntimeTypeProvider()); From 48dccadfb0aed4b3d045a53067dd27b2b5c0aa4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Wed, 27 Feb 2019 23:35:11 +0000 Subject: [PATCH 25/45] Fixed argument exception constructor calls. Use nameof where possible. Fixed wrong check in RelativeSource.AncestorLevel. --- samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs | 2 +- src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs | 2 +- src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs | 4 ++-- src/Avalonia.Remote.Protocol/MetsysBson.cs | 2 +- src/Avalonia.Visuals/Visual.cs | 2 +- src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs | 2 +- src/Markup/Avalonia.Markup/Data/RelativeSource.cs | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs index 2ab6c26e68..df80931367 100644 --- a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs @@ -21,7 +21,7 @@ namespace BindingDemo.ViewModels } else { - throw new ArgumentOutOfRangeException("Value must be less than 10."); + throw new ArgumentOutOfRangeException(nameof(value), "Value must be less than 10."); } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index 135935498c..e48c671a13 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -21,7 +21,7 @@ namespace Avalonia.Data.Core.Plugins { if (method.GetParameters().Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8) { - var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(method)); + var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(methodName)); return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 18cef7d64e..f8ae5c9690 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -965,11 +965,11 @@ namespace Avalonia.Controls { if (value < Minimum) { - throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum)); + throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be greater than Minimum value of {0}", Minimum)); } else if (value > Maximum) { - throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum)); + throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be less than Maximum value of {0}", Maximum)); } } diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs index 925fe10681..5593112bd6 100644 --- a/src/Avalonia.Remote.Protocol/MetsysBson.cs +++ b/src/Avalonia.Remote.Protocol/MetsysBson.cs @@ -749,7 +749,7 @@ namespace Metsys.Bson if (memberExpression.Expression.NodeType != ExpressionType.Parameter && memberExpression.Expression.NodeType != ExpressionType.Convert) { - throw new ArgumentException(string.Format("Expression '{0}' must resolve to top-level member.", lambdaExpression), "lambdaExpression"); + throw new ArgumentException(string.Format("Expression '{0}' must resolve to top-level member.", lambdaExpression), nameof(lambdaExpression)); } return memberExpression.Member.Name; default: diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index bf282db72f..ab2f5307a0 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -551,7 +551,7 @@ namespace Avalonia { if (c == null) { - throw new ArgumentNullException("Cannot add null to VisualChildren."); + throw new ArgumentNullException(nameof(c), "Cannot add null to VisualChildren."); } if (c.VisualParent != null) diff --git a/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs b/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs index 44d887241c..878689442d 100644 --- a/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs +++ b/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs @@ -84,7 +84,7 @@ namespace Avalonia.Gtk3 public RenderOp(GtkWidget widget, ManagedCairoSurface surface, double factor, int width, int height) { _widget = widget; - _surface = surface ?? throw new ArgumentNullException(); + _surface = surface ?? throw new ArgumentNullException(nameof(surface)); _factor = factor; _width = width; _height = height; diff --git a/src/Markup/Avalonia.Markup/Data/RelativeSource.cs b/src/Markup/Avalonia.Markup/Data/RelativeSource.cs index 369bd8112c..ac974e3bda 100644 --- a/src/Markup/Avalonia.Markup/Data/RelativeSource.cs +++ b/src/Markup/Avalonia.Markup/Data/RelativeSource.cs @@ -85,9 +85,9 @@ namespace Avalonia.Data get { return _ancestorLevel; } set { - if (_ancestorLevel <= 0) + if (value <= 0) { - throw new ArgumentOutOfRangeException("AncestorLevel may not be set to less than 1."); + throw new ArgumentOutOfRangeException(nameof(value), "AncestorLevel may not be set to less than 1."); } _ancestorLevel = value; From a89b6128ddaa04f0c3fad3ffc0a92a7db795b1e2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 1 Mar 2019 10:19:29 +0300 Subject: [PATCH 26/45] Call Compact on Add --- src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs index a724878317..e948e55b96 100644 --- a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs +++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs @@ -132,6 +132,7 @@ namespace Avalonia.Utilities public void Add(EventHandler s) { + Compact(true); if (_count == _data.Length) { //Extend capacity @@ -174,7 +175,7 @@ namespace Avalonia.Utilities } } - void Compact() + void Compact(bool preventDestroy = false) { int empty = -1; for (int c = 0; c < _count; c++) @@ -193,7 +194,7 @@ namespace Avalonia.Utilities } if (empty != -1) _count = empty; - if (_count == 0) + if (_count == 0 && !preventDestroy) Destroy(); } From 1d8bf77ab164b228f0c27eacb8d42b0a37c9e35b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Mar 2019 16:33:50 +0100 Subject: [PATCH 27/45] Added failing test for #2335. --- .../Shapes/PathTests.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs new file mode 100644 index 0000000000..05224c2495 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs @@ -0,0 +1,16 @@ +using Avalonia.Controls.Shapes; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Shapes +{ + public class PathTests + { + [Fact] + public void Path_With_Null_Data_Does_Not_Throw_On_Measure() + { + var target = new Path(); + + target.Measure(Size.Infinity); + } + } +} From 7a738e18dd374d3dcc96ac5290fd805fcb5174f1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Mar 2019 16:44:38 +0100 Subject: [PATCH 28/45] Allow for null DefiningGeometry in Shape. Fixes #2335. --- src/Avalonia.Controls/Shapes/Shape.cs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 0387328a46..57dbeba1cc 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -195,7 +195,7 @@ namespace Avalonia.Controls.Shapes if (deferCalculateTransform) { _calculateTransformOnArrange = true; - return DefiningGeometry.Bounds.Size; + return DefiningGeometry?.Bounds.Size ?? Size.Empty; } else { @@ -217,17 +217,22 @@ namespace Avalonia.Controls.Shapes private Size CalculateShapeSizeAndSetTransform(Size availableSize) { - // This should probably use GetRenderBounds(strokeThickness) but then the calculations - // will multiply the stroke thickness as well, which isn't correct. - var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch); - - if (_transform != transform) + if (DefiningGeometry != null) { - _transform = transform; - _renderedGeometry = null; + // This should probably use GetRenderBounds(strokeThickness) but then the calculations + // will multiply the stroke thickness as well, which isn't correct. + var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch); + + if (_transform != transform) + { + _transform = transform; + _renderedGeometry = null; + } + + return size; } - return size; + return Size.Empty; } internal static (Size, Matrix) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch) From f322501852f15f85a05ea52076fcb9a80c07ecca Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Mar 2019 16:45:56 +0100 Subject: [PATCH 29/45] Don't draw null geometry in GeometryDrawing. And add a guard to `DrawingContext` to detect having a null `Geometry` passed, as we do for `DrawImage`, `DrawText` etc. Fixes #2053. --- src/Avalonia.Visuals/Media/DrawingContext.cs | 2 ++ src/Avalonia.Visuals/Media/GeometryDrawing.cs | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index e7d6df5a93..fd593db991 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -109,6 +109,8 @@ namespace Avalonia.Media /// The geometry. public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry) { + Contract.Requires(geometry != null); + if (brush != null || PenIsVisible(pen)) { PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl); diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs index a26a5341c8..ac0cc1c17d 100644 --- a/src/Avalonia.Visuals/Media/GeometryDrawing.cs +++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs @@ -31,7 +31,10 @@ public override void Draw(DrawingContext context) { - context.DrawGeometry(Brush, Pen, Geometry); + if (Geometry != null) + { + context.DrawGeometry(Brush, Pen, Geometry); + } } public override Rect GetBounds() @@ -41,4 +44,4 @@ return Geometry?.GetRenderBounds(pen) ?? new Rect(); } } -} \ No newline at end of file +} From 028d42c7b2ec5a114a65967744d6cfce8cebda40 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Mar 2019 18:07:48 +0100 Subject: [PATCH 30/45] Add additional failing test for #2053. --- .../Rendering/SceneGraph/DrawOperationTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 2060cc7170..6a1f08a384 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -61,6 +61,19 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Assert.Equal(1, bitmap.RefCount); } + [Fact] + public void HitTest_On_Geometry_Node_With_Zero_Transform_Does_Not_Throw() + { + var geometry = Mock.Of(); + var geometryNode = new GeometryNode( + new Matrix(), + Brushes.Black, + null, + geometry); + + geometryNode.HitTest(new Point()); + } + private class TestDrawOperation : DrawOperation { public TestDrawOperation(Rect bounds, Matrix transform, Pen pen) From 569e7d29fcad1e508bf63b536bd90bfcb0bde8b2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Mar 2019 18:08:34 +0100 Subject: [PATCH 31/45] Don't try to invert a non-invertible matrix. Fixes #2053. --- .../Rendering/SceneGraph/GeometryNode.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index 46d264e518..2d01b117d9 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -89,9 +89,14 @@ namespace Avalonia.Rendering.SceneGraph /// public override bool HitTest(Point p) { - p *= Transform.Invert(); - return (Brush != null && Geometry.FillContains(p)) || - (Pen != null && Geometry.StrokeContains(Pen, p)); + if (Transform.HasInverse) + { + p *= Transform.Invert(); + return (Brush != null && Geometry.FillContains(p)) || + (Pen != null && Geometry.StrokeContains(Pen, p)); + } + + return false; } } } From d42b22d5e81a6476432fa3b0430e2997d0ce2f2c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 2 Mar 2019 10:51:56 +0000 Subject: [PATCH 32/45] Add GCNotifier class. --- src/Avalonia.Base/Utilities/GCNotifier.cs | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/Avalonia.Base/Utilities/GCNotifier.cs diff --git a/src/Avalonia.Base/Utilities/GCNotifier.cs b/src/Avalonia.Base/Utilities/GCNotifier.cs new file mode 100644 index 0000000000..f42e78c76a --- /dev/null +++ b/src/Avalonia.Base/Utilities/GCNotifier.cs @@ -0,0 +1,29 @@ +// 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; + +namespace Avalonia.Utilities +{ + public class GCNotifier + { + public static event EventHandler GarbageCollected; + + static GCNotifier() + { + new GCNotifier(); + } + + ~GCNotifier() + { + if (Environment.HasShutdownStarted || AppDomain.CurrentDomain.IsFinalizingForUnload()) + { + return; + } + + new GCNotifier(); + + GarbageCollected?.Invoke(null, EventArgs.Empty); + } + } +} From d37615bea848e83ff19c9d4fd3904f8c677f48bf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Mar 2019 16:34:29 +0100 Subject: [PATCH 33/45] Added failing test for #1898. Plus a couple of other empty binding path tests. --- .../Data/BindingTests.cs | 33 +++++++++++++++++++ .../Parsers/ExpressionNodeBuilderTests.cs | 9 +++++ .../ExpressionNodeBuilderTests_Errors.cs | 7 ++++ 3 files changed, 49 insertions(+) diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 82b57224f0..baa1bca76a 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -273,6 +273,39 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(42, target.Value); } + [Fact] + public void Null_Path_Should_Bind_To_DataContext() + { + var target = new TextBlock { DataContext = "foo" }; + var binding = new Binding(); + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("foo", target.Text); + } + + [Fact] + public void Empty_Path_Should_Bind_To_DataContext() + { + var target = new TextBlock { DataContext = "foo" }; + var binding = new Binding { Path = string.Empty }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("foo", target.Text); + } + + [Fact] + public void Dot_Path_Should_Bind_To_DataContext() + { + var target = new TextBlock { DataContext = "foo" }; + var binding = new Binding { Path = "." }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("foo", target.Text); + } + /// /// Tests a problem discovered with ListBox with selection. /// diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs index 2d687ff4f7..7061915a52 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs @@ -36,6 +36,15 @@ namespace Avalonia.Markup.UnitTests.Parsers AssertIsProperty(result[0], "F0o"); } + [Fact] + public void Should_Build_Dot() + { + var result = ToList(ExpressionObserverBuilder.Parse(".")); + + Assert.Equal(1, result.Count); + Assert.IsType(result[0]); + } + [Fact] public void Should_Build_Property_Chain() { diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs index 347fc0a744..ac385b4cab 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs @@ -30,6 +30,13 @@ namespace Avalonia.Markup.UnitTests.Parsers () => ExpressionObserverBuilder.Parse("Foo.Bar.")); } + [Fact] + public void Expression_Cannot_Start_With_Period_Then_Token() + { + Assert.Throws( + () => ExpressionObserverBuilder.Parse(".Bar")); + } + [Fact] public void Expression_Cannot_Have_Empty_Indexer() { From e486d7f8ea1b2e607732ee38df62f4b5bfcbcbee Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Mar 2019 16:34:50 +0100 Subject: [PATCH 34/45] Allow `{Binding .}` syntax. Fixes #1898. --- .../Avalonia.Markup/Markup/Parsers/ExpressionParser.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index bf6ebe837b..a1350a8393 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -106,6 +106,11 @@ namespace Avalonia.Markup.Parsers { return State.Indexer; } + else if (ParseDot(ref r)) + { + nodes.Add(new EmptyExpressionNode()); + return State.End; + } else { var identifier = r.ParseIdentifier(); @@ -317,6 +322,11 @@ namespace Avalonia.Markup.Parsers return !r.End && r.TakeIf('#'); } + private static bool ParseDot(ref CharacterReader r) + { + return !r.End && r.TakeIf('.'); + } + private enum State { Start, From 82b3bbcc6b83646a4d26f42f9c0ccdbf28b70195 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Mar 2019 17:56:46 +0100 Subject: [PATCH 35/45] Added failing test for #1558. --- .../MouseDeviceTests.cs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs index 5853d1e82b..af034f541b 100644 --- a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs @@ -1,6 +1,7 @@ using Avalonia.Controls; using Avalonia.Input.Raw; using Avalonia.Interactivity; +using Avalonia.Media; using Avalonia.Rendering; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -184,6 +185,33 @@ namespace Avalonia.Input.UnitTests } } + + [Fact] + public void GetPosition_Should_Respect_Control_RenderTransform() + { + var renderer = new Mock(); + + using (TestApplication(renderer.Object)) + { + var inputManager = InputManager.Instance; + + var root = new TestRoot + { + MouseDevice = new MouseDevice(), + Child = new Border + { + Background = Brushes.Black, + RenderTransform = new TranslateTransform(10, 0), + } + }; + + SendMouseMove(inputManager, root, new Point(11, 11)); + + var result = root.MouseDevice.GetPosition(root.Child); + Assert.Equal(new Point(1, 11), result); + } + } + private void AddEnterLeaveHandlers( EventHandler handler, params IControl[] controls) @@ -195,14 +223,14 @@ namespace Avalonia.Input.UnitTests } } - private void SendMouseMove(IInputManager inputManager, TestRoot root) + private void SendMouseMove(IInputManager inputManager, TestRoot root, Point p = new Point()) { inputManager.ProcessInput(new RawMouseEventArgs( root.MouseDevice, 0, root, RawMouseEventType.Move, - new Point(), + p, InputModifiers.None)); } From 12cd666dc7bd2484ea64140dd18eeefea43a8b54 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Mar 2019 17:57:05 +0100 Subject: [PATCH 36/45] Respect RenderTransform in GetPosition. Fixes #1558. --- src/Avalonia.Input/MouseDevice.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 7945ee8ee4..d3e62ece6f 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -84,18 +84,14 @@ namespace Avalonia.Input { Contract.Requires(relativeTo != null); - Point p = default(Point); - IVisual v = relativeTo; - IVisual root = null; - - while (v != null) + if (relativeTo.VisualRoot == null) { - p += v.Bounds.Position; - root = v; - v = v.VisualParent; + throw new InvalidOperationException("Control is not attached to visual tree."); } - return root.PointToClient(Position) - p; + var rootPoint = relativeTo.VisualRoot.PointToClient(Position); + var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); + return rootPoint * transform.Value; } public void ProcessRawEvent(RawInputEventArgs e) From 6b2d9a020fa5fe1a50117a20b3414a4d218e326a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 2 Mar 2019 10:51:56 +0000 Subject: [PATCH 37/45] Revert "Add GCNotifier class." This reverts commit d42b22d5e81a6476432fa3b0430e2997d0ce2f2c. --- src/Avalonia.Base/Utilities/GCNotifier.cs | 29 ----------------------- 1 file changed, 29 deletions(-) delete mode 100644 src/Avalonia.Base/Utilities/GCNotifier.cs diff --git a/src/Avalonia.Base/Utilities/GCNotifier.cs b/src/Avalonia.Base/Utilities/GCNotifier.cs deleted file mode 100644 index f42e78c76a..0000000000 --- a/src/Avalonia.Base/Utilities/GCNotifier.cs +++ /dev/null @@ -1,29 +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; - -namespace Avalonia.Utilities -{ - public class GCNotifier - { - public static event EventHandler GarbageCollected; - - static GCNotifier() - { - new GCNotifier(); - } - - ~GCNotifier() - { - if (Environment.HasShutdownStarted || AppDomain.CurrentDomain.IsFinalizingForUnload()) - { - return; - } - - new GCNotifier(); - - GarbageCollected?.Invoke(null, EventArgs.Empty); - } - } -} From 5128f70f48c13a2decd86fc4a5c2dd2060f2cf42 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 2 Mar 2019 20:19:47 +0000 Subject: [PATCH 38/45] whitespace. --- src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs index e948e55b96..0ade1af249 100644 --- a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs +++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs @@ -121,7 +121,6 @@ namespace Avalonia.Utilities var del = new Action(OnEvent); _delegate = del.GetMethodInfo().CreateDelegate(_info.EventHandlerType, del.Target); _info.AddMethod.Invoke(target, new[] { _delegate }); - } void Destroy() From 89d969b367b4e1bd37982361602a74518e431ab9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Mar 2019 22:45:47 +0100 Subject: [PATCH 39/45] Added selector comma operator. The comma selector can be used to separate a number different selectors, all of which will be applied to the control with an OR. Fixes #1742 --- src/Avalonia.Styling/Styling/OrSelector.cs | 131 ++++++++++++++++++ src/Avalonia.Styling/Styling/Selectors.cs | 22 +++ .../Markup/Parsers/SelectorGrammar.cs | 30 ++-- .../Markup/Parsers/SelectorParser.cs | 20 +++ .../Parsers/SelectorGrammarTests.cs | 16 +++ .../Parsers/SelectorParserTests.cs | 7 + .../SelectorTests_Or.cs | 106 ++++++++++++++ 7 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 src/Avalonia.Styling/Styling/OrSelector.cs create mode 100644 tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs diff --git a/src/Avalonia.Styling/Styling/OrSelector.cs b/src/Avalonia.Styling/Styling/OrSelector.cs new file mode 100644 index 0000000000..58c5c778fb --- /dev/null +++ b/src/Avalonia.Styling/Styling/OrSelector.cs @@ -0,0 +1,131 @@ +// 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.Collections.Generic; + +namespace Avalonia.Styling +{ + /// + /// The OR style selector. + /// + internal class OrSelector : Selector + { + private readonly IReadOnlyList _selectors; + private string _selectorString; + private Type _targetType; + + /// + /// Initializes a new instance of the class. + /// + /// The selectors to OR. + public OrSelector(IReadOnlyList selectors) + { + Contract.Requires(selectors != null); + Contract.Requires(selectors.Count > 1); + + _selectors = selectors; + } + + /// + public override bool InTemplate => false; + + /// + public override bool IsCombinator => false; + + /// + public override Type TargetType + { + get + { + if (_targetType == null) + { + _targetType = EvaluateTargetType(); + } + + return _targetType; + } + } + + /// + public override string ToString() + { + if (_selectorString == null) + { + _selectorString = string.Join(", ", _selectors); + } + + return _selectorString; + } + + protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + { + var activators = new List>(); + var neverThisInstance = false; + + foreach (var selector in _selectors) + { + var match = selector.Match(control, subscribe); + + switch (match.Result) + { + case SelectorMatchResult.AlwaysThisType: + case SelectorMatchResult.AlwaysThisInstance: + return match; + case SelectorMatchResult.NeverThisInstance: + neverThisInstance = true; + break; + case SelectorMatchResult.Sometimes: + activators.Add(match.Activator); + break; + } + } + + if (activators.Count > 1) + { + return new SelectorMatch(StyleActivator.Or(activators)); + } + else if (activators.Count == 1) + { + return new SelectorMatch(activators[0]); + } + else if (neverThisInstance) + { + return SelectorMatch.NeverThisInstance; + } + else + { + return SelectorMatch.NeverThisType; + } + } + + protected override Selector MovePrevious() => null; + + private Type EvaluateTargetType() + { + var result = default(Type); + + foreach (var selector in _selectors) + { + if (selector.TargetType == null) + { + return null; + } + else if (result == null) + { + result = selector.TargetType; + } + else + { + while (!result.IsAssignableFrom(selector.TargetType)) + { + result = result.BaseType; + } + } + } + + return result; + } + } +} + diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index deb677e04c..3e7a30d389 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; +using System.Linq; namespace Avalonia.Styling { @@ -137,6 +139,26 @@ namespace Avalonia.Styling return previous.OfType(typeof(T)); } + /// + /// Returns a selector which ORs selectors. + /// + /// The selectors to be OR'd. + /// The selector. + public static Selector Or(params Selector[] selectors) + { + return new OrSelector(selectors); + } + + /// + /// Returns a selector which ORs selectors. + /// + /// The selectors to be OR'd. + /// The selector. + public static Selector Or(IReadOnlyList selectors) + { + return new OrSelector(selectors); + } + /// /// Returns a selector which matches a control with the specified property value. /// diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 55c3aab81f..e11e333a49 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -49,7 +49,7 @@ namespace Avalonia.Markup.Parsers state = ParseStart(ref r); break; case State.Middle: - state = ParseMiddle(ref r, end); + (state, syntax) = ParseMiddle(ref r, end); break; case State.CanHaveType: state = ParseCanHaveType(ref r); @@ -113,33 +113,37 @@ namespace Avalonia.Markup.Parsers return State.TypeName; } - private static State ParseMiddle(ref CharacterReader r, char? end) + private static (State, ISyntax) ParseMiddle(ref CharacterReader r, char? end) { if (r.TakeIf(':')) { - return State.Colon; + return (State.Colon, null); } else if (r.TakeIf('.')) { - return State.Class; + return (State.Class, null); } else if (r.TakeIf(char.IsWhiteSpace) || r.Peek == '>') { - return State.Traversal; + return (State.Traversal, null); } else if (r.TakeIf('/')) { - return State.Template; + return (State.Template, null); } else if (r.TakeIf('#')) { - return State.Name; + return (State.Name, null); + } + else if (r.TakeIf(',')) + { + return (State.Start, new CommaSyntax()); } else if (end.HasValue && !r.End && r.Peek == end.Value) { - return State.End; + return (State.End, null); } - return State.TypeName; + return (State.TypeName, null); } private static State ParseCanHaveType(ref CharacterReader r) @@ -415,5 +419,13 @@ namespace Avalonia.Markup.Parsers return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument); } } + + public class CommaSyntax : ISyntax + { + public override bool Equals(object obj) + { + return obj is CommaSyntax or; + } + } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index 8137ac3f48..493579d676 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -43,6 +43,7 @@ namespace Avalonia.Markup.Parsers private Selector Create(IEnumerable syntax) { var result = default(Selector); + var results = default(List); foreach (var i in syntax) { @@ -106,11 +107,30 @@ namespace Avalonia.Markup.Parsers case SelectorGrammar.NotSyntax not: result = result.Not(x => Create(not.Argument)); break; + case SelectorGrammar.CommaSyntax comma: + if (results == null) + { + results = new List(); + } + + results.Add(result); + result = null; + break; default: throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'."); } } + if (results != null) + { + if (result != null) + { + results.Add(result); + } + + result = results.Count > 1 ? Selectors.Or(results) : results[0]; + } + return result; } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index e3ce4b0968..216043aa20 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -261,6 +261,22 @@ namespace Avalonia.Markup.UnitTests.Parsers result); } + [Fact] + public void OfType_Comma_Is_Class() + { + var result = SelectorGrammar.Parse("TextBlock, :is(Button).foo"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "TextBlock" }, + new SelectorGrammar.CommaSyntax(), + new SelectorGrammar.IsSyntax { TypeName = "Button" }, + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + result); + } + [Fact] public void Namespace_Alone_Fails() { diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs index 1b1a96a7e2..1c0cba56c9 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs @@ -14,6 +14,13 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = target.Parse("TextBlock[IsPointerOver=True]"); } + [Fact] + public void Parses_Comma_Separated_Selectors() + { + var target = new SelectorParser((ns, type) => typeof(TextBlock)); + var result = target.Parse("TextBlock, TextBlock:foo"); + } + [Fact] public void Throws_If_OfType_Type_Not_Found() { diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs new file mode 100644 index 0000000000..521c73ce27 --- /dev/null +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs @@ -0,0 +1,106 @@ +// 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 Xunit; + +namespace Avalonia.Styling.UnitTests +{ + public class SelectorTests_Or + { + [Fact] + public void Or_Selector_Should_Have_Correct_String_Representation() + { + var target = Selectors.Or( + default(Selector).OfType().Class("foo"), + default(Selector).OfType().Class("bar")); + + Assert.Equal("Control1.foo, Control2.bar", target.ToString()); + } + + [Fact] + public void Or_Selector_Matches_Control_Of_Correct_Type() + { + var target = Selectors.Or( + default(Selector).OfType(), + default(Selector).OfType().Class("bar")); + var control = new Control1(); + + Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result); + } + + [Fact] + public void Or_Selector_Matches_Control_Of_Correct_Type_With_Class() + { + var target = Selectors.Or( + default(Selector).OfType(), + default(Selector).OfType().Class("bar")); + var control = new Control2(); + + Assert.Equal(SelectorMatchResult.Sometimes, target.Match(control).Result); + } + + [Fact] + public void Or_Selector_Doesnt_Match_Control_Of_Incorrect_Type() + { + var target = Selectors.Or( + default(Selector).OfType(), + default(Selector).OfType().Class("bar")); + var control = new Control3(); + + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(control).Result); + } + + [Fact] + public void Or_Selector_Doesnt_Match_Control_With_Incorrect_Name() + { + var target = Selectors.Or( + default(Selector).OfType().Name("foo"), + default(Selector).OfType().Name("foo")); + var control = new Control1 { Name = "bar" }; + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(control).Result); + } + + [Fact] + public void Returns_Correct_TargetType_When_Types_Same() + { + var target = Selectors.Or( + default(Selector).OfType().Class("foo"), + default(Selector).OfType().Class("bar")); + + Assert.Equal(typeof(Control1), target.TargetType); + } + + [Fact] + public void Returns_Common_TargetType() + { + var target = Selectors.Or( + default(Selector).OfType().Class("foo"), + default(Selector).OfType().Class("bar")); + + Assert.Equal(typeof(TestControlBase), target.TargetType); + } + + [Fact] + public void Returns_Null_TargetType_When_A_Selector_Has_No_TargetType() + { + var target = Selectors.Or( + default(Selector).OfType().Class("foo"), + default(Selector).Class("bar")); + + Assert.Equal(null, target.TargetType); + } + + public class Control1 : TestControlBase + { + } + + public class Control2 : TestControlBase + { + } + + public class Control3 : TestControlBase + { + } + } +} From 36da749d38c9ac3c963585863d22667e692de89c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Mar 2019 22:55:18 +0100 Subject: [PATCH 40/45] Use comma operator for Separator style. --- src/Avalonia.Themes.Default/Separator.xaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Avalonia.Themes.Default/Separator.xaml b/src/Avalonia.Themes.Default/Separator.xaml index 6312a14df5..cf0db16ee6 100644 --- a/src/Avalonia.Themes.Default/Separator.xaml +++ b/src/Avalonia.Themes.Default/Separator.xaml @@ -11,13 +11,7 @@ - - -