Browse Source

Merge branch 'master' into devtools-property-editor

Luis von der Eltz 2 years ago
parent
commit
fece278c65

+ 5 - 0
packages/Avalonia/Avalonia.props

@@ -6,4 +6,9 @@
     <AvaloniaUseExternalMSBuild>false</AvaloniaUseExternalMSBuild>
   </PropertyGroup>
   <Import Project="$(MSBuildThisFileDirectory)\AvaloniaBuildTasks.props"/>
+
+  <!-- Allow loading the AvaloniaVS extension when referencing the Avalonia nuget package -->
+  <ItemGroup>
+    <ProjectCapability Include="Avalonia"/>
+  </ItemGroup>
 </Project>

+ 104 - 51
src/Avalonia.Base/Platform/AssetLoader.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
 using System.Reflection;
@@ -62,7 +63,7 @@ namespace Avalonia.Platform
         /// <returns>True if the asset could be found; otherwise false.</returns>
         public bool Exists(Uri uri, Uri? baseUri = null)
         {
-            return GetAsset(uri, baseUri) != null;
+            return TryGetAsset(uri, baseUri, out _);
         }
 
         /// <summary>
@@ -94,21 +95,27 @@ namespace Avalonia.Platform
         /// </exception>
         public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null)
         {
-            var asset = GetAsset(uri, baseUri);
-
-            if (asset == null)
+            if (TryGetAsset(uri, baseUri, out var assetDescriptor))
             {
-                throw new FileNotFoundException($"The resource {uri} could not be found.");
+                return (assetDescriptor.GetStream(), assetDescriptor.Assembly);
             }
 
-            return (asset.GetStream(), asset.Assembly);
+            throw new FileNotFoundException($"The resource {uri} could not be found.");
         }
 
         public Assembly? GetAssembly(Uri uri, Uri? baseUri)
         {
             if (!uri.IsAbsoluteUri && baseUri != null)
+            {
                 uri = new Uri(baseUri, uri);
-            return GetAssembly(uri)?.Assembly;
+            }
+
+            if (TryGetAssembly(uri, out var assemblyDescriptor))
+            {
+                return assemblyDescriptor.Assembly;
+            }
+
+            return null;
         }
 
         /// <summary>
@@ -121,99 +128,145 @@ namespace Avalonia.Platform
         {
             if (uri.IsAbsoluteResm())
             {
-                var assembly = GetAssembly(uri);
+                if (!TryGetAssembly(uri, out var assembly))
+                {
+                    assembly = _defaultResmAssembly;
+                }
 
                 return assembly?.Resources?
-                           .Where(x => x.Key.IndexOf(uri.GetUnescapeAbsolutePath(), StringComparison.Ordinal) >= 0)
-                           .Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ??
-                       Enumerable.Empty<Uri>();
+                        .Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath()))
+                        .Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ??
+                    Enumerable.Empty<Uri>();
             }
 
             uri = uri.EnsureAbsolute(baseUri);
+
             if (uri.IsAvares())
             {
-                var (asm, path) = GetResAsmAndPath(uri);
-                if (asm == null)
+                if (!TryGetResAsmAndPath(uri, out var assembly, out var path))
                 {
-                    throw new ArgumentException(
-                        "No default assembly, entry assembly or explicit assembly specified; " +
-                        "don't know where to look up for the resource, try specifying assembly explicitly.");
+                    return Enumerable.Empty<Uri>();
                 }
 
-                if (asm.AvaloniaResources == null)
+                if (assembly?.AvaloniaResources == null)
+                {
                     return Enumerable.Empty<Uri>();
+                }
 
-                if (path[path.Length - 1] != '/')
+                if (path.Length > 0 && path[path.Length - 1] != '/')
+                {
                     path += '/';
+                }
 
-                return asm.AvaloniaResources
+                return assembly.AvaloniaResources
                     .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal))
-                    .Select(x => new Uri($"avares://{asm.Name}{x.Key}"));
+                    .Select(x => new Uri($"avares://{assembly.Name}{x.Key}"));
             }
 
             return Enumerable.Empty<Uri>();
         }
-        
-        private IAssetDescriptor? GetAsset(Uri uri, Uri? baseUri)
-        {           
+
+        private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor)
+        {
+            assetDescriptor = null;
+
             if (uri.IsAbsoluteResm())
             {
-                var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly;
-
-                if (asm == null)
+                if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly))
                 {
-                    throw new ArgumentException(
-                        "No default assembly, entry assembly or explicit assembly specified; " +
-                        "don't know where to look up for the resource, try specifying assembly explicitly.");
+                    assembly = _defaultResmAssembly;
                 }
 
-                var resourceKey = uri.AbsolutePath;
-                IAssetDescriptor? rv = null;
-                asm.Resources?.TryGetValue(resourceKey, out rv);
-                return rv;
+                if (assembly?.Resources != null)
+                {
+                    var resourceKey = uri.AbsolutePath;
+
+                    if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor))
+                    {
+                        return true;
+                    }
+                }
             }
 
             uri = uri.EnsureAbsolute(baseUri);
 
             if (uri.IsAvares())
             {
-                var (asm, path) = GetResAsmAndPath(uri);
-                if (asm.AvaloniaResources == null)
-                    return null;
-                asm.AvaloniaResources.TryGetValue(path, out var desc);
-                return desc;
+                if (TryGetResAsmAndPath(uri, out var assembly, out var path))
+                {
+                    if (assembly.AvaloniaResources == null)
+                    {
+                        return false;
+                    }
+
+                    if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor))
+                    {
+                        return true;
+                    }
+                }
             }
 
-            throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri));
+            return false;
         }
 
-        private static (IAssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri)
+        private static bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path)
         {
-            var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority);
-            return (asm, uri.GetUnescapeAbsolutePath());
+            path = uri.GetUnescapeAbsolutePath();
+
+            if (TryLoadAssembly(uri.Authority, out assembly))
+            {
+                return true;
+            }
+
+            return false;
         }
-        
-        private static IAssemblyDescriptor? GetAssembly(Uri? uri)
+
+        private static bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly)
         {
+            assembly = null;
+
             if (uri != null)
             {
                 if (!uri.IsAbsoluteUri)
-                    return null;
-                if (uri.IsAvares())
-                    return GetResAsmAndPath(uri).asm;
+                {
+                    return false;
+                }
+
+                if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _))
+                {
+                    return true;
+                }
 
                 if (uri.IsResm())
                 {
                     var assemblyName = uri.GetAssemblyNameFromQuery();
-                    if (assemblyName.Length > 0)
-                        return s_assemblyDescriptorResolver.GetAssembly(assemblyName);
+
+                    if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly))
+                    {
+                        return true;
+                    }
                 }
             }
 
-            return null;
+            return false;
+        }
+
+        private static bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly)
+        {
+            assembly = null;
+
+            try
+            {
+                assembly = s_assemblyDescriptorResolver.GetAssembly(assemblyName);
+
+                return true;
+            }
+            catch (Exception) { }
+
+            return false;
         }
 #endif
-        
+
         public static void RegisterResUriParsers()
         {
             if (!UriParser.IsKnownScheme("avares"))

+ 1 - 1
src/Avalonia.Base/PropertyStore/ValueStore.cs

@@ -924,7 +924,7 @@ namespace Avalonia.PropertyStore
                 {
                     _effectiveValues.GetKeyValue(i, out var key, out var e);
 
-                    if (e.Priority == BindingPriority.Unset)
+                    if (e.Priority == BindingPriority.Unset && !e.IsOverridenCurrentValue)
                     {
                         RemoveEffectiveValue(key, i);
                         e.DisposeAndRaiseUnset(this, key);

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Rendering.SceneGraph
             Matrix transform,
             IImmutableBrush foreground,
             IRef<IGlyphRunImpl> glyphRun)
-            : base(new Rect(glyphRun.Item.Size), transform, foreground)
+            : base(new Rect(glyphRun.Item.BaselineOrigin, glyphRun.Item.Size), transform, foreground)
         {
             GlyphRun = glyphRun.Clone();
         }

+ 24 - 11
src/Avalonia.Controls/Panel.cs

@@ -1,13 +1,12 @@
 using System;
-using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.ComponentModel;
 using System.Linq;
+using Avalonia.Controls.Presenters;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Metadata;
 using Avalonia.Reactive;
-using Avalonia.Styling;
 
 namespace Avalonia.Controls
 {
@@ -59,6 +58,11 @@ namespace Avalonia.Controls
             set { SetValue(BackgroundProperty, value); }
         }
 
+        /// <summary>
+        /// Gets whether the <see cref="Panel"/> hosts the items created by an <see cref="ItemsPresenter"/>.
+        /// </summary>
+        public bool IsItemsHost { get; internal set; }
+
         event EventHandler<ChildIndexChangedEventArgs>? IChildIndexProvider.ChildIndexChanged
         {
             add
@@ -129,24 +133,29 @@ namespace Avalonia.Controls
         /// <param name="e">The event args.</param>
         protected virtual void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e)
         {
-            List<Control> controls;
-
             switch (e.Action)
             {
                 case NotifyCollectionChangedAction.Add:
-                    controls = e.NewItems!.OfType<Control>().ToList();
-                    LogicalChildren.InsertRange(e.NewStartingIndex, controls);
+                    if (!IsItemsHost)
+                    {
+                        LogicalChildren.InsertRange(e.NewStartingIndex, e.NewItems!.OfType<Control>().ToList());
+                    }
                     VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems!.OfType<Visual>());
                     break;
 
                 case NotifyCollectionChangedAction.Move:
-                    LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex);
-                    VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems.Count, e.NewStartingIndex);
+                    if (!IsItemsHost)
+                    {
+                        LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex);
+                    }
+                    VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex);
                     break;
 
                 case NotifyCollectionChangedAction.Remove:
-                    controls = e.OldItems!.OfType<Control>().ToList();
-                    LogicalChildren.RemoveAll(controls);
+                    if (!IsItemsHost)
+                    {
+                        LogicalChildren.RemoveAll(e.OldItems!.OfType<Control>().ToList());
+                    }
                     VisualChildren.RemoveAll(e.OldItems!.OfType<Visual>());
                     break;
 
@@ -155,7 +164,10 @@ namespace Avalonia.Controls
                     {
                         var index = i + e.OldStartingIndex;
                         var child = (Control)e.NewItems![i]!;
-                        LogicalChildren[index] = child;
+                        if (!IsItemsHost)
+                        {
+                            LogicalChildren[index] = child;
+                        }
                         VisualChildren[index] = child;
                     }
                     break;
@@ -200,6 +212,7 @@ namespace Avalonia.Controls
             return child is Control control ? Children.IndexOf(control) : -1;
         }
 
+        /// <inheritdoc />
         public bool TryGetTotalCount(out int count)
         {
             count = Children.Count;

+ 1 - 0
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@@ -167,6 +167,7 @@ namespace Avalonia.Controls.Presenters
 
                 Panel = ItemsPanel.Build();
                 Panel.SetValue(TemplatedParentProperty, TemplatedParent);
+                Panel.IsItemsHost = true;
                 _scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo;
                 LogicalChildren.Add(Panel);
                 VisualChildren.Add(Panel);

+ 1 - 1
src/Windows/Avalonia.Win32/Input/KeyInterop.cs

@@ -4,7 +4,7 @@ using Avalonia.Win32.Interop;
 
 namespace Avalonia.Win32.Input
 {
-    static class KeyInterop
+    public static class KeyInterop
     {
         private static readonly Dictionary<Key, int> s_virtualKeyFromKey = new Dictionary<Key, int>
         {

+ 9 - 4
src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs

@@ -396,7 +396,7 @@ public partial class AvaloniaPropertyAnalyzer
                             hostTypeRef = new(_avaloniaObjectType, Location.None); // assume that an attached property applies everywhere until we find its registration
                         }
 
-                        var result =  new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef };
+                        var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef };
 
                         // assume that the property is owned by its containing type at the point of assignment, until we find its registration
                         result.SetAssignment(s, new(s.ContainingType, Location.None));
@@ -570,7 +570,7 @@ public partial class AvaloniaPropertyAnalyzer
 
             if (_allGetSetMethods.Contains(originalMethod))
             {
-                if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && 
+                if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } &&
                     GetReferencedProperty(invocation.Arguments[0]) is { } refProp &&
                     refProp.description.AssignedTo.TryGetValue(refProp.storageSymbol, out var ownerType) &&
                     !DerivesFrom(context.ContainingSymbol.ContainingType, ownerType.Type) &&
@@ -694,9 +694,14 @@ public partial class AvaloniaPropertyAnalyzer
 
                 void VerifyAccessor(IMethodSymbol? method, string verb, string methodName)
                 {
-                    if (method == null)
+                    if (method is null)
                     {
-                        context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName));
+                        if (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Public ||
+                            (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Protected
+                                && avaloniaPropertyStorage.ContainingSymbol.DeclaredAccessibility == Accessibility.Public))
+                        {
+                            context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName));
+                        }
                     }
                     else if (method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility)
                     {

+ 12 - 1
tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs

@@ -9,7 +9,7 @@ namespace Avalonia.Base.UnitTests;
 
 public class AssetLoaderTests : IDisposable
 {
-    public class MockAssembly : Assembly {}
+    public class MockAssembly : Assembly { }
 
     private const string AssemblyNameWithWhitespace = "Awesome Library";
 
@@ -50,6 +50,17 @@ public class AssetLoaderTests : IDisposable
         Assert.Equal(AssemblyNameWithNonAscii, assemblyActual?.FullName);
     }
 
+    [Fact]
+    public void Invalid_AssemblyName_Should_Yield_Empty_Enumerable()
+    {
+        var uri = new Uri($"avares://InvalidAssembly");
+        var loader = new AssetLoader();
+
+        var assemblyActual = loader.GetAssets(uri, null);
+
+        Assert.Empty(assemblyActual);
+    }
+
     private static IAssemblyDescriptor CreateAssemblyDescriptor(string assemblyName)
     {
         var assembly = Mock.Of<MockAssembly>();

+ 80 - 1
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs

@@ -1,6 +1,9 @@
 using System;
+using Avalonia.Controls;
 using Avalonia.Data;
 using Avalonia.Diagnostics;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
 using Xunit;
 using Observable = Avalonia.Reactive.Observable;
 
@@ -275,6 +278,79 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("style", target.Foo);
         }
 
+        [Fact]
+        public void SetCurrent_Value_Persists_When_Toggling_Style_1()
+        {
+            var target = new Class1();
+            var root = new TestRoot(target)
+            {
+                Styles =
+                {
+                    new Style(x => x.OfType<Class1>().Class("foo"))
+                    {
+                        Setters = { new Setter(Class1.BarProperty, "bar") },
+                    }
+                }
+            };
+
+            root.LayoutManager.ExecuteInitialLayoutPass();
+
+            target.SetCurrentValue(Class1.FooProperty, "current");
+
+            Assert.Equal("current", target.Foo);
+            Assert.Equal("bardefault", target.Bar);
+
+            target.Classes.Add("foo");
+
+            Assert.Equal("current", target.Foo);
+            Assert.Equal("bar", target.Bar);
+
+            target.Classes.Remove("foo");
+
+            Assert.Equal("current", target.Foo);
+            Assert.Equal("bardefault", target.Bar);
+        }
+
+        [Fact]
+        public void SetCurrent_Value_Persists_When_Toggling_Style_2()
+        {
+            var target = new Class1();
+            var root = new TestRoot(target)
+            {
+                Styles =
+                {
+                    new Style(x => x.OfType<Class1>().Class("foo"))
+                    {
+                        Setters = 
+                        { 
+                            new Setter(Class1.BarProperty, "bar"),
+                            new Setter(Class1.InheritedProperty, "inherited"),
+                        },
+                    }
+                }
+            };
+
+            root.LayoutManager.ExecuteInitialLayoutPass();
+
+            target.SetCurrentValue(Class1.FooProperty, "current");
+
+            Assert.Equal("current", target.Foo);
+            Assert.Equal("bardefault", target.Bar);
+            Assert.Equal("inheriteddefault", target.Inherited);
+
+            target.Classes.Add("foo");
+
+            Assert.Equal("current", target.Foo);
+            Assert.Equal("bar", target.Bar);
+            Assert.Equal("inherited", target.Inherited);
+
+            target.Classes.Remove("foo");
+
+            Assert.Equal("current", target.Foo);
+            Assert.Equal("bardefault", target.Bar);
+            Assert.Equal("inheriteddefault", target.Inherited);
+        }
+
         private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property)
         {
             return target.GetDiagnostic(property).Priority;
@@ -285,16 +361,19 @@ namespace Avalonia.Base.UnitTests
             return target.GetDiagnostic(property).IsOverriddenCurrentValue;
         }
 
-        private class Class1 : AvaloniaObject
+        private class Class1 : Control
         {
             public static readonly StyledProperty<string> FooProperty =
                 AvaloniaProperty.Register<Class1, string>(nameof(Foo), "foodefault");
+            public static readonly StyledProperty<string> BarProperty =
+                AvaloniaProperty.Register<Class1, string>(nameof(Bar), "bardefault");
             public static readonly StyledProperty<string> InheritedProperty =
                 AvaloniaProperty.Register<Class1, string>(nameof(Inherited), "inheriteddefault", inherits: true);
             public static readonly StyledProperty<double> CoercedProperty =
                 AvaloniaProperty.Register<Class1, double>(nameof(Coerced), coerce: Coerce);
 
             public string Foo => GetValue(FooProperty);
+            public string Bar => GetValue(BarProperty);
             public string Inherited => GetValue(InheritedProperty);
             public double Coerced => GetValue(CoercedProperty);
             public double CoerceMax { get; set; } = 100;

+ 14 - 1
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@@ -73,6 +73,19 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(target, target.Presenter.Panel.TemplatedParent);
         }
 
+        [Fact]
+        public void Panel_Should_Have_ItemsHost_Set_To_True()
+        {
+            var target = new ItemsControl();
+
+            target.Template = GetTemplate();
+            target.Items = new[] { "Foo" };
+            target.ApplyTemplate();
+            target.Presenter!.ApplyTemplate();
+
+            Assert.True(target.Presenter.Panel!.IsItemsHost);
+        }
+
         [Fact]
         public void Container_Should_Have_TemplatedParent_Set_To_Null()
         {
@@ -634,7 +647,7 @@ namespace Avalonia.Controls.UnitTests
             target.ApplyTemplate();
             target.Presenter.ApplyTemplate();
 
-            var item = target.Presenter.Panel.LogicalChildren[0];
+            var item = target.LogicalChildren[0];
             Assert.Null(NameScope.GetNameScope((TextBlock)item));
         }
 

+ 15 - 0
tests/Avalonia.Controls.UnitTests/PanelTests.cs

@@ -141,5 +141,20 @@ namespace Avalonia.Controls.UnitTests
             var panel = new Panel();
             Assert.Throws<ArgumentNullException>(() => panel.Children.Add(null!));
         }
+
+        [Fact]
+        public void Adding_Control_To_Items_Host_Panel_Should_Not_Affect_Logical_Children()
+        {
+            var child = new Control();
+            var realParent = new ContentControl { Content = child };
+            var panel = new Panel { IsItemsHost = true };
+
+            panel.Children.Add(child);
+
+            Assert.Empty(panel.LogicalChildren);
+            Assert.Same(child.Parent, realParent);
+            Assert.Same(child.GetLogicalParent(), realParent);
+            Assert.Same(child.GetVisualParent(), panel);
+        }
     }
 }

+ 1 - 1
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@@ -327,7 +327,7 @@ namespace Avalonia.Controls.UnitTests
 
             ApplyTemplate(target);
 
-            var logicalChildren = target.ItemsPresenterPart.Panel.GetLogicalChildren();
+            var logicalChildren = target.GetLogicalChildren();
 
             var result = logicalChildren
                 .OfType<TabItem>()

+ 1 - 1
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -1178,7 +1178,7 @@ namespace Avalonia.Controls.UnitTests
             target.ApplyTemplate();
             target.Presenter.ApplyTemplate();
 
-            var item = target.Presenter.Panel.LogicalChildren[0];
+            var item = target.LogicalChildren[0];
             Assert.Null(NameScope.GetNameScope((TreeViewItem)item));
         }