diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 78b9cff039..acff8cc117 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,18 +1,18 @@ ## What does the pull request do? + -Give a bit of background on the PR here, together with links to with related issues etc. ## What is the current behavior? + -If the PR is a fix, describe the current incorrect behavior, otherwise delete this section. ## What is the updated/expected behavior with this PR? + -Describe how to test the PR. ## How was the solution implemented (if it's not obvious)? + -Include any information that might be of use to a reviewer here. ## Checklist @@ -21,12 +21,11 @@ Include any information that might be of use to a reviewer here. - [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Avaloniaui.net with user documentation ## Breaking changes + -List any breaking changes here. When the PR is merged please add an entry to https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes ## Fixed issues - -If the pull request fixes issue(s) list them like this: - + diff --git a/Avalonia.sln b/Avalonia.sln index 2f7560049c..d6472503fe 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -196,9 +196,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "nukebuild\_build. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Animation.UnitTests", "tests\Avalonia.Animation.UnitTests\Avalonia.Animation.UnitTests.csproj", "{AF227847-E65C-4BE9-BCE9-B551357788E0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -1819,6 +1821,30 @@ Global {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhone.Build.0 = Release|Any CPU {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhone.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhone.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.Build.0 = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhone.ActiveCfg = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhone.Build.0 = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1875,6 +1901,7 @@ Global {AF227847-E65C-4BE9-BCE9-B551357788E0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index afd1950859..65ba5e9756 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -109,7 +109,7 @@ public partial class Build if (!IsNuGetRelease) { // Use AssemblyVersion with Build as version - Version += "-build" + Environment.GetEnvironmentVariable("BUILD_BUILDID") + "-beta"; + Version += "-cibuild" + int.Parse(Environment.GetEnvironmentVariable("BUILD_BUILDID")).ToString("0000000") + "-beta"; } PublishTestResults = true; diff --git a/scripts/ReplaceNugetCache.ps1 b/scripts/ReplaceNugetCache.ps1 index 6de50f978d..70f5eaa40b 100644 --- a/scripts/ReplaceNugetCache.ps1 +++ b/scripts/ReplaceNugetCache.ps1 @@ -1,6 +1,5 @@ -copy ..\samples\ControlCatalog.Desktop\bin\Debug\net461\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\net461\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Gtk3.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 90b6aca505..7601b64ce9 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -36,32 +36,7 @@ namespace Avalonia public AvaloniaObject() { VerifyAccess(); - - void Notify(AvaloniaProperty property) - { - object value = property.IsDirect ? - ((IDirectPropertyAccessor)property).GetValue(this) : - ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); - - var e = new AvaloniaPropertyChangedEventArgs( - this, - property, - AvaloniaProperty.UnsetValue, - value, - BindingPriority.Unset); - - property.NotifyInitialized(e); - } - - foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegistered(this)) - { - Notify(property); - } - - foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType())) - { - Notify(property); - } + AvaloniaPropertyRegistry.Instance.NotifyInitialized(this); } /// @@ -645,7 +620,7 @@ namespace Avalonia /// /// The property. /// The default value. - internal object GetDefaultValue(AvaloniaProperty property) + private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) return aobj.GetValue(property); diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 4b0116a536..953132116c 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -21,7 +21,7 @@ namespace Avalonia /// public static readonly object UnsetValue = new Unset(); - private static int s_nextId = 1; + private static int s_nextId; private readonly Subject _initialized; private readonly Subject _changed; private readonly PropertyMetadata _defaultMetadata; diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index e29e7339ae..037e0dd72e 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using Avalonia.Data; namespace Avalonia { @@ -13,6 +14,8 @@ namespace Avalonia /// public class AvaloniaPropertyRegistry { + private readonly Dictionary _properties = + new Dictionary(); private readonly Dictionary> _registered = new Dictionary>(); private readonly Dictionary> _attached = @@ -21,6 +24,8 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _attachedCache = new Dictionary>(); + private readonly Dictionary>> _initializedCache = + new Dictionary>>(); /// /// Gets the instance @@ -28,6 +33,11 @@ namespace Avalonia public static AvaloniaPropertyRegistry Instance { get; } = new AvaloniaPropertyRegistry(); + /// + /// Gets a list of all registered properties. + /// + internal IReadOnlyCollection Properties => _properties.Values; + /// /// Gets all non-attached s registered on a type. /// @@ -148,6 +158,16 @@ namespace Avalonia return FindRegistered(o.GetType(), name); } + /// + /// Finds a registered property by Id. + /// + /// The property Id. + /// The registered property or null if no matching property found. + internal AvaloniaProperty FindRegistered(int id) + { + return id < _properties.Count ? _properties[id] : null; + } + /// /// Checks whether a is registered on a type. /// @@ -202,8 +222,14 @@ namespace Avalonia { inner.Add(property.Id, property); } - + + if (!_properties.ContainsKey(property.Id)) + { + _properties.Add(property.Id, property); + } + _registeredCache.Clear(); + _initializedCache.Clear(); } /// @@ -237,8 +263,59 @@ namespace Avalonia { inner.Add(property.Id, property); } - + _attachedCache.Clear(); + _initializedCache.Clear(); + } + + internal void NotifyInitialized(AvaloniaObject o) + { + Contract.Requires(o != null); + + var type = o.GetType(); + + void Notify(AvaloniaProperty property, object value) + { + var e = new AvaloniaPropertyChangedEventArgs( + o, + property, + AvaloniaProperty.UnsetValue, + value, + BindingPriority.Unset); + + property.NotifyInitialized(e); + } + + if (!_initializedCache.TryGetValue(type, out var items)) + { + var build = new Dictionary(); + + foreach (var property in GetRegistered(type)) + { + var value = !property.IsDirect ? + ((IStyledPropertyAccessor)property).GetDefaultValue(type) : + null; + build.Add(property, value); + } + + foreach (var property in GetRegisteredAttached(type)) + { + if (!build.ContainsKey(property)) + { + var value = ((IStyledPropertyAccessor)property).GetDefaultValue(type); + build.Add(property, value); + } + } + + items = build.ToList(); + _initializedCache.Add(type, items); + } + + foreach (var i in items) + { + var value = i.Key.IsDirect ? o.GetValue(i.Key) : i.Value; + Notify(i.Key, value); + } } } } diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index c4ffa839e0..f1717bde3b 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -177,7 +177,7 @@ namespace Avalonia.Data.Core protected override void Subscribed(IObserver observer, bool first) { - if (!first && _value != null && _value.TryGetTarget(out var val) == true) + if (!first && _value != null && _value.TryGetTarget(out var val)) { observer.OnNext(val); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index 14ca8ee79e..c41097c274 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; +using System.Reactive.Linq; +using System.Reflection; namespace Avalonia.Data.Core.Plugins { @@ -10,12 +13,19 @@ namespace Avalonia.Data.Core.Plugins /// public class ObservableStreamPlugin : IStreamPlugin { + static MethodInfo observableSelect; + /// /// Checks whether this plugin handles the specified value. /// /// A weak reference to the value. /// True if the plugin can handle the value; otherwise false. - public virtual bool Match(WeakReference reference) => reference.Target is IObservable; + public virtual bool Match(WeakReference reference) + { + return reference.Target.GetType().GetInterfaces().Any(x => + x.IsGenericType && + x.GetGenericTypeDefinition() == typeof(IObservable<>)); + } /// /// Starts producing output based on the specified value. @@ -26,7 +36,69 @@ namespace Avalonia.Data.Core.Plugins /// public virtual IObservable Start(WeakReference reference) { - return reference.Target as IObservable; + var target = reference.Target; + + // If the observable returns a reference type then we can cast it. + if (target is IObservable result) + { + return result; + }; + + // If the observable returns a value type then we need to call Observable.Select on it. + // First get the type of T in `IObservable`. + var sourceType = reference.Target.GetType().GetInterfaces().First(x => + x.IsGenericType && + x.GetGenericTypeDefinition() == typeof(IObservable<>)).GetGenericArguments()[0]; + + // Get the Observable.Select method. + var select = GetObservableSelect(sourceType); + + // Make a Box<> delegate of the correct type. + var funcType = typeof(Func<,>).MakeGenericType(sourceType, typeof(object)); + var box = GetType().GetMethod(nameof(Box), BindingFlags.Static | BindingFlags.NonPublic) + .MakeGenericMethod(sourceType) + .CreateDelegate(funcType); + + // Call Observable.Select(target, box); + return (IObservable)select.Invoke( + null, + new object[] { target, box }); + } + + private static MethodInfo GetObservableSelect(Type source) + { + return GetObservableSelect().MakeGenericMethod(source, typeof(object)); } + + private static MethodInfo GetObservableSelect() + { + if (observableSelect == null) + { + observableSelect = typeof(Observable).GetRuntimeMethods().First(x => + { + if (x.Name == nameof(Observable.Select) && + x.ContainsGenericParameters && + x.GetGenericArguments().Length == 2) + { + var parameters = x.GetParameters(); + + if (parameters.Length == 2 && + parameters[0].ParameterType.IsConstructedGenericType && + parameters[0].ParameterType.GetGenericTypeDefinition() == typeof(IObservable<>) && + parameters[1].ParameterType.IsConstructedGenericType && + parameters[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>)) + { + return true; + } + } + + return false; + }); + } + + return observableSelect; + } + + private static object Box(T value) => (object)value; } } diff --git a/src/Avalonia.Base/ISupportInitialize.cs b/src/Avalonia.Base/ISupportInitialize.cs deleted file mode 100644 index 04e3d72e6c..0000000000 --- a/src/Avalonia.Base/ISupportInitialize.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Avalonia -{ - /// - /// Specifies that this object supports a simple, transacted notification for batch - /// initialization. - /// - public interface ISupportInitialize - { - /// - /// Signals the object that initialization is starting. - /// - void BeginInit(); - - /// - /// Signals the object that initialization is complete. - /// - void EndInit(); - } -} diff --git a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs index 22e5c952bf..66024236da 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs @@ -4,6 +4,8 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; +using System.Xml.Linq; +using System.Linq; // ReSharper disable AssignNullToNotNullAttribute @@ -19,10 +21,20 @@ namespace Avalonia.Utilities { var ver = new BinaryReader(stream).ReadInt32(); if (ver > LastKnownVersion) - throw new Exception("Resources index format version is not known"); - var index = (AvaloniaResourcesIndex) - new DataContractSerializer(typeof(AvaloniaResourcesIndex)).ReadObject(stream); - return index.Entries; + throw new Exception("Resources index format version is not known"); + + var assetDoc = XDocument.Load(stream); + XNamespace assetNs = assetDoc.Root.Attribute("xmlns").Value; + List entries= + (from entry in assetDoc.Root.Element(assetNs + "Entries").Elements(assetNs + "AvaloniaResourcesIndexEntry") + select new AvaloniaResourcesIndexEntry + { + Path = entry.Element(assetNs + "Path").Value, + Offset = int.Parse(entry.Element(assetNs + "Offset").Value), + Size = int.Parse(entry.Element(assetNs + "Size").Value) + }).ToList(); + + return entries; } public static void Write(Stream stream, List entries) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index c5bf6ba0a8..22961a7568 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,13 +7,21 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { + private struct Entry + { + internal int PropertyId; + internal object Value; + } + private readonly AvaloniaObject _owner; - private readonly Dictionary _values = - new Dictionary(); + private Entry[] _entries; public ValueStore(AvaloniaObject owner) { _owner = owner; + + // The last item in the list is always int.MaxValue + _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } }; } public IDisposable AddBinding( @@ -23,7 +31,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (_values.TryGetValue(property, out var v)) + if (TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -31,13 +39,13 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _values[property] = priorityValue; + SetValueInternal(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - _values.Add(property, priorityValue); + AddValueInternal(property, priorityValue); } return priorityValue.Add(source, (int)priority); @@ -47,7 +55,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (_values.TryGetValue(property, out var v)) + if (TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -55,7 +63,7 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - _values[property] = Validate(property, value); + SetValueInternal(property, Validate(property, value)); Changed(property, priority, v, value); return; } @@ -63,7 +71,7 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _values[property] = priorityValue; + SetValueInternal(property, priorityValue); } } } @@ -76,14 +84,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - _values.Add(property, Validate(property, value)); + AddValueInternal(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - _values.Add(property, priorityValue); + AddValueInternal(property, priorityValue); } } @@ -100,6 +108,16 @@ namespace Avalonia _owner.PriorityValueChanged(property, priority, oldValue, newValue); } + public IDictionary GetSetValues() + { + var dict = new Dictionary(_entries.Length - 1); + for (int i = 0; i < _entries.Length - 1; ++i) + { + dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); + } + + return dict; + } public void LogError(AvaloniaProperty property, Exception e) { _owner.LogBindingError(property, e); @@ -111,7 +129,7 @@ namespace Avalonia { var result = AvaloniaProperty.UnsetValue; - if (_values.TryGetValue(property, out var value)) + if (TryGetValue(property, out var value)) { result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; } @@ -121,12 +139,12 @@ namespace Avalonia public bool IsAnimating(AvaloniaProperty property) { - return _values.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; + return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; } public bool IsSet(AvaloniaProperty property) { - if (_values.TryGetValue(property, out var value)) + if (TryGetValue(property, out var value)) { return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; } @@ -136,7 +154,7 @@ namespace Avalonia public void Revalidate(AvaloniaProperty property) { - if (_values.TryGetValue(property, out var value)) + if (TryGetValue(property, out var value)) { (value as PriorityValue)?.Revalidate(); } @@ -183,5 +201,103 @@ namespace Avalonia (_deferredSetter = new DeferredSetter()); } } + + private bool TryGetValue(AvaloniaProperty property, out object value) + { + (int index, bool found) = TryFindEntry(property.Id); + if (!found) + { + value = null; + return false; + } + + value = _entries[index].Value; + return true; + } + + private void AddValueInternal(AvaloniaProperty property, object value) + { + Entry[] entries = new Entry[_entries.Length + 1]; + + for (int i = 0; i < _entries.Length; ++i) + { + if (_entries[i].PropertyId > property.Id) + { + if (i > 0) + { + Array.Copy(_entries, 0, entries, 0, i); + } + + entries[i] = new Entry { PropertyId = property.Id, Value = value }; + Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); + break; + } + } + + _entries = entries; + } + + private void SetValueInternal(AvaloniaProperty property, object value) + { + _entries[TryFindEntry(property.Id).Item1].Value = value; + } + + private (int, bool) TryFindEntry(int propertyId) + { + if (_entries.Length <= 12) + { + // For small lists, we use an optimized linear search. Since the last item in the list + // is always int.MaxValue, we can skip a conditional branch in each iteration. + // By unrolling the loop, we can skip another unconditional branch in each iteration. + + if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId); + if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId); + if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId); + if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId); + if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId); + if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId); + if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId); + if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId); + if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId); + if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId); + if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId); + } + else + { + int low = 0; + int high = _entries.Length; + int id; + + while (high - low > 3) + { + int pivot = (high + low) / 2; + id = _entries[pivot].PropertyId; + + if (propertyId == id) + return (pivot, true); + + if (propertyId <= id) + high = pivot; + else + low = pivot + 1; + } + + do + { + id = _entries[low].PropertyId; + + if (id == propertyId) + return (low, true); + + if (id > propertyId) + break; + + ++low; + } + while (low < high); + } + + return (0, false); + } } } diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 1bc402bc2f..b054804c86 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1893,7 +1893,7 @@ namespace Avalonia.Controls { bool callTextChanged = false; // Update the Text dependency property - if ((userInitiated == null || userInitiated == true) && Text != value) + if ((userInitiated ?? true) && Text != value) { _ignoreTextPropertyChange++; Text = value; diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index d485924885..f572c67284 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -217,7 +217,7 @@ namespace Avalonia.Controls var e = new RoutedEventArgs(ClickEvent); RaiseEvent(e); - if (Command != null) + if (!e.Handled && Command?.CanExecute(CommandParameter) == true) { Command.Execute(CommandParameter); e.Handled = true; diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index a00d586233..a7ee027e70 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -1,6 +1,7 @@ // 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.ComponentModel; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index 93b33e0589..4f65a0aed4 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -56,6 +56,7 @@ namespace Avalonia.Controls private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; + private IDisposable _subscriptionsOnOpen; /// /// Initializes static members of the class. @@ -149,16 +150,12 @@ namespace Avalonia.Controls { if (e.Key == Key.Down) { - if (++SelectedIndex >= ItemCount) - SelectedIndex = 0; - + SelectNext(); e.Handled = true; } else if (e.Key == Key.Up) { - if (--SelectedIndex < 0) - SelectedIndex = ItemCount - 1; - + SelectPrev(); e.Handled = true; } } @@ -174,6 +171,32 @@ namespace Avalonia.Controls } } + /// + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + base.OnPointerWheelChanged(e); + + if (!e.Handled) + { + if (!IsDropDownOpen) + { + if (IsFocused) + { + if (e.Delta.Y < 0) + SelectNext(); + else + SelectPrev(); + + e.Handled = true; + } + } + else + { + e.Handled = true; + } + } + } + /// protected override void OnPointerPressed(PointerPressedEventArgs e) { @@ -223,6 +246,9 @@ namespace Avalonia.Controls private void PopupClosed(object sender, EventArgs e) { + _subscriptionsOnOpen?.Dispose(); + _subscriptionsOnOpen = null; + if (CanFocus(this)) { Focus(); @@ -232,6 +258,22 @@ namespace Avalonia.Controls private void PopupOpened(object sender, EventArgs e) { TryFocusSelectedItem(); + + _subscriptionsOnOpen?.Dispose(); + _subscriptionsOnOpen = null; + + var toplevel = this.GetVisualRoot() as TopLevel; + if (toplevel != null) + { + _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) => + { + //eat wheel scroll event outside dropdown popup while it's open + if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) + { + ev.Handled = true; + } + }, Interactivity.RoutingStrategies.Tunnel); + } } private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e) @@ -247,7 +289,7 @@ namespace Avalonia.Controls { var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); - if(container == null && SelectedItems.Count > 0) + if (container == null && SelectedItems.Count > 0) { ScrollIntoView(SelectedItems[0]); container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); @@ -307,5 +349,25 @@ namespace Avalonia.Controls } } } + + private void SelectNext() + { + int next = SelectedIndex + 1; + + if (next >= ItemCount) + next = 0; + + SelectedIndex = next; + } + + private void SelectPrev() + { + int prev = SelectedIndex - 1; + + if (prev < 0) + prev = ItemCount - 1; + + SelectedIndex = prev; + } } } diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 224af979ab..43beb923e5 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Platform; diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index 8b39cc03b8..c4f83ffd54 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Styling; namespace Avalonia.Controls.Embedding.Offscreen diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 72379e7b53..c696fe7975 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -99,5 +99,22 @@ namespace Avalonia.Controls return new Size(); } } + + /// + protected override Size ArrangeOverride(Size finalSize) + { + var source = Source; + + if (source != null) + { + var sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); + var result = Stretch.CalculateSize(finalSize, sourceSize); + return result; + } + else + { + return new Size(); + } + } } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index d74078c712..3dfeae52a4 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -64,6 +64,7 @@ namespace Avalonia.Controls static ItemsControl() { ItemsProperty.Changed.AddClassHandler(x => x.ItemsChanged); + ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); } /// @@ -73,7 +74,6 @@ namespace Avalonia.Controls { PseudoClasses.Add(":empty"); SubscribeToItems(_items); - ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); } /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 055d49fb0b..c2227b9dfc 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -287,7 +287,7 @@ namespace Avalonia.Controls /// The click event args. protected virtual void OnClick(RoutedEventArgs e) { - if (Command != null) + if (!e.Handled && Command?.CanExecute(CommandParameter) == true) { Command.Execute(CommandParameter); e.Handled = true; @@ -421,7 +421,7 @@ namespace Avalonia.Controls } /// - /// Called when the property changes. + /// Called when the property changes. /// /// The property change event. private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) diff --git a/src/Avalonia.Controls/PixelPointEventArgs.cs b/src/Avalonia.Controls/PixelPointEventArgs.cs index 55a3d5601f..2456d0aea4 100644 --- a/src/Avalonia.Controls/PixelPointEventArgs.cs +++ b/src/Avalonia.Controls/PixelPointEventArgs.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - /// The data. + /// The data. public PixelPointEventArgs(PixelPoint point) { Point = point; diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index c05c1672f8..30330ef9ac 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -285,7 +285,7 @@ namespace Avalonia.Controls.Presenters { scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable); - if (scrollable.IsLogicalScrollEnabled == true) + if (scrollable.IsLogicalScrollEnabled) { _logicalScrollSubscription = new CompositeDisposable( this.GetObservable(CanHorizontallyScrollProperty) diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index f77c43acd0..0387328a46 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -20,7 +20,10 @@ namespace Avalonia.Controls.Shapes AvaloniaProperty.Register(nameof(Stroke)); public static readonly StyledProperty> StrokeDashArrayProperty = - AvaloniaProperty.Register>("StrokeDashArray"); + AvaloniaProperty.Register>(nameof(StrokeDashArray)); + + public static readonly StyledProperty StrokeDashOffsetProperty = + AvaloniaProperty.Register(nameof(StrokeDashOffset)); public static readonly StyledProperty StrokeThicknessProperty = AvaloniaProperty.Register(nameof(StrokeThickness)); @@ -103,6 +106,12 @@ namespace Avalonia.Controls.Shapes get { return GetValue(StrokeDashArrayProperty); } set { SetValue(StrokeDashArrayProperty, value); } } + + public double StrokeDashOffset + { + get { return GetValue(StrokeDashOffsetProperty); } + set { SetValue(StrokeDashOffsetProperty, value); } + } public double StrokeThickness { @@ -124,7 +133,7 @@ namespace Avalonia.Controls.Shapes if (geometry != null) { - var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray), + var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeDashCap, StrokeStartLineCap, StrokeEndLineCap, StrokeJoin); context.DrawGeometry(Fill, pen, geometry); } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 5ca3647da7..32c40847c5 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -136,6 +136,11 @@ namespace Avalonia.Controls } } + /// + /// Fired when the window is opened. + /// + public event EventHandler Opened; + /// /// Fired when the window is closed. /// @@ -311,6 +316,12 @@ namespace Avalonia.Controls $"Control '{GetType().Name}' is a top level control and cannot be added as a child."); } + /// + /// Raises the event. + /// + /// The event args. + protected virtual void OnOpened(EventArgs e) => Opened?.Invoke(this, e); + /// /// Tries to get a service from an , logging a /// warning if not found. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 53f727900b..f5af6774b5 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -389,6 +389,7 @@ namespace Avalonia.Controls Renderer?.Start(); } SetWindowStartupLocation(Owner?.PlatformImpl); + OnOpened(EventArgs.Empty); } /// @@ -458,6 +459,7 @@ namespace Avalonia.Controls owner.Activate(); result.SetResult((TResult)(_dialogResult ?? default(TResult))); }); + OnOpened(EventArgs.Empty); } SetWindowStartupLocation(owner); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 2fba8619c6..363af05a0b 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -163,7 +164,7 @@ namespace Avalonia.Controls } /// - /// Shows the popup. + /// Shows the window. /// public virtual void Show() { @@ -181,6 +182,7 @@ namespace Avalonia.Controls } PlatformImpl?.Show(); Renderer?.Start(); + OnOpened(EventArgs.Empty); } finally { diff --git a/src/Avalonia.Native/ScreenImpl.cs b/src/Avalonia.Native/ScreenImpl.cs index c1edd6c846..0729de9b8e 100644 --- a/src/Avalonia.Native/ScreenImpl.cs +++ b/src/Avalonia.Native/ScreenImpl.cs @@ -41,7 +41,7 @@ namespace Avalonia.Native public void Dispose () { - _native.Dispose(); + _native?.Dispose(); _native = null; } } diff --git a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs index cf386a235e..e1db604e95 100644 --- a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs +++ b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs @@ -29,8 +29,8 @@ namespace Avalonia { var windowLoaded = Observable .FromEventPattern( - x => window.Initialized += x, - x => window.Initialized -= x) + x => window.Opened += x, + x => window.Opened -= x) .Select(args => true); var windowUnloaded = Observable .FromEventPattern( @@ -59,4 +59,4 @@ namespace Avalonia .DistinctUntilChanged(); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index 04c6f100a6..43e2ef93b6 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -16,7 +16,7 @@ namespace Avalonia public class ReactiveUserControl : UserControl, IViewFor where TViewModel : class { public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty - .Register, TViewModel>(nameof(ViewModel)); + .Register, TViewModel>(nameof(ViewModel)); /// /// Initializes a new instance of the class. @@ -41,4 +41,4 @@ namespace Avalonia set => ViewModel = (TViewModel)value; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs new file mode 100644 index 0000000000..726e086d9c --- /dev/null +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -0,0 +1,224 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; +using ReactiveUI; +using Splat; + +namespace Avalonia +{ + /// + /// This control hosts the View associated with ReactiveUI RoutingState, + /// and will display the View and wire up the ViewModel whenever a new + /// ViewModel is navigated to. Nested routing is also supported. + /// + /// + /// + /// ReactiveUI routing consists of an IScreen that contains current + /// RoutingState, several IRoutableViewModels, and a platform-specific + /// XAML control called RoutedViewHost. + /// + /// + /// RoutingState manages the ViewModel navigation stack and allows + /// ViewModels to navigate to other ViewModels. IScreen is the root of + /// a navigation stack; despite the name, its views don't have to occupy + /// the whole screen. RoutedViewHost monitors an instance of RoutingState, + /// responding to any changes in the navigation stack by creating and + /// embedding the appropriate view. + /// + /// + /// Place this control to a view containing your ViewModel that implements + /// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property. + /// + /// + /// + /// + /// + /// + /// ]]> + /// + /// + /// + /// See + /// ReactiveUI routing documentation website for more info. + /// + /// + public class RoutedViewHost : UserControl, IActivatable, IEnableLogger + { + /// + /// The router dependency property. + /// + public static readonly AvaloniaProperty RouterProperty = + AvaloniaProperty.Register(nameof(Router)); + + /// + /// The default content property. + /// + public static readonly AvaloniaProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + /// + /// Fade in animation property. + /// + public static readonly AvaloniaProperty FadeInAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); + + /// + /// Fade out animation property. + /// + public static readonly AvaloniaProperty FadeOutAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); + + /// + /// Initializes a new instance of the class. + /// + public RoutedViewHost() + { + this.WhenActivated(disposables => + { + this.WhenAnyObservable(x => x.Router.CurrentViewModel) + .DistinctUntilChanged() + .Subscribe(NavigateToViewModel) + .DisposeWith(disposables); + }); + } + + /// + /// Gets or sets the of the view model stack. + /// + public RoutingState Router + { + get => GetValue(RouterProperty); + set => SetValue(RouterProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Gets or sets the animation played when page appears. + /// + public IAnimation FadeInAnimation + { + get => GetValue(FadeInAnimationProperty); + set => SetValue(FadeInAnimationProperty, value); + } + + /// + /// Gets or sets the animation played when page disappears. + /// + public IAnimation FadeOutAnimation + { + get => GetValue(FadeOutAnimationProperty); + set => SetValue(FadeOutAnimationProperty, value); + } + + /// + /// Duplicates the Content property with a private setter. + /// + public new object Content + { + get => base.Content; + private set => base.Content = value; + } + + /// + /// Gets or sets the ReactiveUI view locator used by this router. + /// + public IViewLocator ViewLocator { get; set; } + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + /// + /// Thrown when ViewLocator is unable to find the appropriate view. + /// + private void NavigateToViewModel(IRoutableViewModel viewModel) + { + if (viewModel == null) + { + this.Log().Info("ViewModel is null, falling back to default content."); + UpdateContent(DefaultContent); + return; + } + + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var view = viewLocator.ResolveView(viewModel); + if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); + + this.Log().Info($"Ready to show {view} with autowired {viewModel}."); + view.ViewModel = viewModel; + UpdateContent(view); + } + + /// + /// Updates the content with transitions. + /// + /// New content to set. + private async void UpdateContent(object newContent) + { + if (FadeOutAnimation != null) + await FadeOutAnimation.RunAsync(this, Clock); + Content = newContent; + if (FadeInAnimation != null) + await FadeInAnimation.RunAsync(this, Clock); + } + + /// + /// Creates opacity animation for this routed view host. + /// + /// Opacity to start from. + /// Opacity to finish with. + /// Duration of the animation. + /// Animation object instance. + private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) + { + return new Avalonia.Animation.Animation + { + Duration = duration, + Children = + { + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = from + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = to + } + }, + Cue = new Cue(1d) + } + } + }; + } + } +} diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs index f6bb73129f..925fe10681 100644 --- a/src/Avalonia.Remote.Protocol/MetsysBson.cs +++ b/src/Avalonia.Remote.Protocol/MetsysBson.cs @@ -1190,10 +1190,6 @@ namespace Metsys.Bson object container = null; var property = typeHelper.FindProperty(name); var propertyType = property != null ? property.Type : _typeMap.ContainsKey(storageType) ? _typeMap[storageType] : typeof(object); - if (property == null && typeHelper.Expando == null) - { - throw new BsonException(string.Format("Deserialization failed: type {0} does not have a property named {1}", type.FullName, name)); - } if (property != null && property.Setter == null) { container = property.Getter(instance); @@ -1201,7 +1197,8 @@ namespace Metsys.Bson var value = isNull ? null : DeserializeValue(propertyType, storageType, container, options); if (property == null) { - ((IDictionary)typeHelper.Expando.Getter(instance))[name] = value; + if (typeHelper.Expando != null) + ((IDictionary)typeHelper.Expando.Getter(instance))[name] = value; } else if (container == null && value != null && !property.Ignored) { diff --git a/src/Avalonia.Remote.Protocol/TcpTransportBase.cs b/src/Avalonia.Remote.Protocol/TcpTransportBase.cs index 562dbdf8f9..d01265c9f4 100644 --- a/src/Avalonia.Remote.Protocol/TcpTransportBase.cs +++ b/src/Avalonia.Remote.Protocol/TcpTransportBase.cs @@ -46,7 +46,7 @@ namespace Avalonia.Remote.Protocol { try { - var cl = await server.AcceptTcpClientAsync(); + var cl = await server.AcceptTcpClientAsync().ConfigureAwait(false); AcceptNew(); await Task.Run(async () => { @@ -54,7 +54,7 @@ namespace Avalonia.Remote.Protocol var t = CreateTransport(_resolver, cl.GetStream(), () => tcs.TrySetResult(0)); cb(t); await tcs.Task; - }); + }).ConfigureAwait(false); } catch { @@ -69,7 +69,7 @@ namespace Avalonia.Remote.Protocol public async Task Connect(IPAddress address, int port) { var c = new TcpClient(); - await c.ConnectAsync(address, port); + await c.ConnectAsync(address, port).ConfigureAwait(false); return CreateTransport(_resolver, c.GetStream(), ((IDisposable)c).Dispose); } } diff --git a/src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs b/src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs index 1e821b7c24..d7919af9d9 100644 --- a/src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs +++ b/src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs @@ -64,7 +64,7 @@ namespace Avalonia.Remote.Protocol public Task Send(object data) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); lock (_lock) { if (!_workerIsAlive) @@ -79,8 +79,9 @@ namespace Avalonia.Remote.Protocol }); if (_signal != null) { - _signal.SetResult(0); + var signal = _signal; _signal = null; + signal.SetResult(0); } } return tcs.Task; @@ -98,4 +99,4 @@ namespace Avalonia.Remote.Protocol remove => _onException.Remove(value); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Styling/Styling/NotSelector.cs b/src/Avalonia.Styling/Styling/NotSelector.cs new file mode 100644 index 0000000000..bcf76620be --- /dev/null +++ b/src/Avalonia.Styling/Styling/NotSelector.cs @@ -0,0 +1,72 @@ +// 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.Reactive.Linq; + +namespace Avalonia.Styling +{ + /// + /// The `:not()` style selector. + /// + internal class NotSelector : Selector + { + private readonly Selector _previous; + private readonly Selector _argument; + private string _selectorString; + + /// + /// Initializes a new instance of the class. + /// + /// The previous selector. + /// The selector to be not-ed. + public NotSelector(Selector previous, Selector argument) + { + _previous = previous; + _argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument."); + } + + /// + public override bool InTemplate => _argument.InTemplate; + + /// + public override bool IsCombinator => false; + + /// + public override Type TargetType => _previous?.TargetType; + + /// + public override string ToString() + { + if (_selectorString == null) + { + _selectorString = ":not(" + _argument.ToString() + ")"; + } + + return _selectorString; + } + + protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + { + var innerResult = _argument.Match(control, subscribe); + + switch (innerResult.Result) + { + case SelectorMatchResult.AlwaysThisInstance: + return SelectorMatch.NeverThisInstance; + case SelectorMatchResult.AlwaysThisType: + return SelectorMatch.NeverThisType; + case SelectorMatchResult.NeverThisInstance: + return SelectorMatch.AlwaysThisInstance; + case SelectorMatchResult.NeverThisType: + return SelectorMatch.AlwaysThisType; + case SelectorMatchResult.Sometimes: + return new SelectorMatch(innerResult.Activator.Select(x => !x)); + default: + throw new InvalidOperationException("Invalid SelectorMatchResult."); + } + } + + protected override Selector MovePrevious() => _previous; + } +} diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index c91cc7af04..4284c7e798 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -94,6 +94,17 @@ namespace Avalonia.Styling } } + /// + /// Returns a selector which inverts the results of selector argument. + /// + /// The previous selector. + /// The selector to be not-ed. + /// The selector. + public static Selector Not(this Selector previous, Func argument) + { + return new NotSelector(previous, argument(null)); + } + /// /// Returns a selector which matches a type. /// diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 27fad58346..d799df7ac9 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -143,6 +143,7 @@ namespace Avalonia.Styling } controlSubscriptions.Add(subs); + controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs))); Subscriptions.Add(subs); } @@ -159,8 +160,9 @@ namespace Avalonia.Styling var sub = setter.Apply(this, control, null); subs.Add(sub); } - + controlSubscriptions.Add(subs); + controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs))); Subscriptions.Add(subs); return true; } @@ -223,7 +225,7 @@ namespace Avalonia.Styling { if (!_applied.TryGetValue(control, out var subscriptions)) { - subscriptions = new CompositeDisposable(2); + subscriptions = new CompositeDisposable(3); subscriptions.Add(control.StyleDetach.Subscribe(ControlDetach)); _applied.Add(control, subscriptions); } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 60e624948e..3bc5e92fb4 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -255,15 +255,19 @@ namespace Avalonia.Rendering } var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(GetContext); + using (scene) { - var overlay = DrawDirtyRects || DrawFps; - if (DrawDirtyRects) - _dirtyRectsDisplay.Tick(); - if (overlay) - RenderOverlay(scene.Item, GetContext()); - if (updated || forceComposite || overlay) - RenderComposite(scene.Item, GetContext()); + if (scene?.Item != null) + { + var overlay = DrawDirtyRects || DrawFps; + if (DrawDirtyRects) + _dirtyRectsDisplay.Tick(); + if (overlay) + RenderOverlay(scene.Item, GetContext()); + if (updated || forceComposite || overlay) + RenderComposite(scene.Item, GetContext()); + } } } finally diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 159c3cd0fa..2fb8e84a2e 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -236,7 +236,7 @@ namespace Avalonia.Rendering.SceneGraph { foreach (var operation in DrawOperations) { - if (operation.Item.HitTest(p) == true) + if (operation.Item.HitTest(p)) { return true; } diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index 087ba017ae..1629890568 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -7,7 +7,6 @@ - diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs new file mode 100644 index 0000000000..100996984b --- /dev/null +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -0,0 +1,263 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Platform.Interop; +// ReSharper disable IdentifierTypo +namespace Avalonia.X11.NativeDialogs +{ + + static unsafe class Glib + { + private const string GlibName = "libglib-2.0.so.0"; + private const string GObjectName = "libgobject-2.0.so.0"; + + [DllImport(GlibName)] + public static extern void g_slist_free(GSList* data); + + [DllImport(GObjectName)] + private static extern void g_object_ref(IntPtr instance); + + [DllImport(GObjectName)] + private static extern ulong g_signal_connect_object(IntPtr instance, Utf8Buffer signal, + IntPtr handler, IntPtr userData, int flags); + + [DllImport(GObjectName)] + private static extern void g_object_unref(IntPtr instance); + + [DllImport(GObjectName)] + private static extern ulong g_signal_handler_disconnect(IntPtr instance, ulong connectionId); + + private delegate bool timeout_callback(IntPtr data); + + [DllImport(GlibName)] + private static extern ulong g_timeout_add_full(int prio, uint interval, timeout_callback callback, IntPtr data, + IntPtr destroy); + + + class ConnectedSignal : IDisposable + { + private readonly IntPtr _instance; + private GCHandle _handle; + private readonly ulong _id; + + public ConnectedSignal(IntPtr instance, GCHandle handle, ulong id) + { + _instance = instance; + g_object_ref(instance); + _handle = handle; + _id = id; + } + + public void Dispose() + { + if (_handle.IsAllocated) + { + g_signal_handler_disconnect(_instance, _id); + g_object_unref(_instance); + _handle.Free(); + } + } + } + + public static IDisposable ConnectSignal(IntPtr obj, string name, T handler) + { + var handle = GCHandle.Alloc(handler); + var ptr = Marshal.GetFunctionPointerForDelegate((Delegate)(object)handler); + using (var utf = new Utf8Buffer(name)) + { + var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0); + if (id == 0) + throw new ArgumentException("Unable to connect to signal " + name); + return new ConnectedSignal(obj, handle, id); + } + } + + + static bool TimeoutHandler(IntPtr data) + { + var handle = GCHandle.FromIntPtr(data); + var cb = (Func)handle.Target; + if (!cb()) + { + handle.Free(); + return false; + } + + return true; + } + + private static readonly timeout_callback s_pinnedHandler; + + static Glib() + { + s_pinnedHandler = TimeoutHandler; + } + + static void AddTimeout(int priority, uint interval, Func callback) + { + var handle = GCHandle.Alloc(callback); + g_timeout_add_full(priority, interval, s_pinnedHandler, GCHandle.ToIntPtr(handle), IntPtr.Zero); + } + + public static Task RunOnGlibThread(Func action) + { + var tcs = new TaskCompletionSource(); + AddTimeout(0, 0, () => + { + + try + { + tcs.SetResult(action()); + } + catch (Exception e) + { + tcs.TrySetException(e); + } + + return false; + }); + return tcs.Task; + } + } + + [StructLayout(LayoutKind.Sequential)] + unsafe struct GSList + { + public readonly IntPtr Data; + public readonly GSList* Next; + } + + enum GtkFileChooserAction + { + Open, + Save, + SelectFolder, + } + + // ReSharper disable UnusedMember.Global + enum GtkResponseType + { + Help = -11, + Apply = -10, + No = -9, + Yes = -8, + Close = -7, + Cancel = -6, + Ok = -5, + DeleteEvent = -4, + Accept = -3, + Reject = -2, + None = -1, + } + // ReSharper restore UnusedMember.Global + + static unsafe class Gtk + { + private static IntPtr s_display; + private const string GdkName = "libgdk-3.so.0"; + private const string GtkName = "libgtk-3.so.0"; + + [DllImport(GtkName)] + static extern void gtk_main_iteration(); + + + [DllImport(GtkName)] + public static extern void gtk_window_set_modal(IntPtr window, bool modal); + + [DllImport(GtkName)] + public static extern void gtk_window_present(IntPtr gtkWindow); + + + public delegate bool signal_generic(IntPtr gtkWidget, IntPtr userData); + + public delegate bool signal_dialog_response(IntPtr gtkWidget, GtkResponseType response, IntPtr userData); + + [DllImport(GtkName)] + public static extern IntPtr gtk_file_chooser_dialog_new(Utf8Buffer title, IntPtr parent, + GtkFileChooserAction action, IntPtr ignore); + + [DllImport(GtkName)] + public static extern void gtk_file_chooser_set_select_multiple(IntPtr chooser, bool allow); + + [DllImport(GtkName)] + public static extern void + gtk_dialog_add_button(IntPtr raw, Utf8Buffer button_text, GtkResponseType response_id); + + [DllImport(GtkName)] + public static extern GSList* gtk_file_chooser_get_filenames(IntPtr chooser); + + [DllImport(GtkName)] + public static extern void gtk_file_chooser_set_filename(IntPtr chooser, Utf8Buffer file); + + [DllImport(GtkName)] + public static extern void gtk_widget_realize(IntPtr gtkWidget); + + [DllImport(GtkName)] + public static extern IntPtr gtk_widget_get_window(IntPtr gtkWidget); + + [DllImport(GtkName)] + public static extern void gtk_widget_hide(IntPtr gtkWidget); + + [DllImport(GtkName)] + static extern bool gtk_init_check(int argc, IntPtr argv); + + [DllImport(GdkName)] + static extern IntPtr gdk_x11_window_foreign_new_for_display(IntPtr display, IntPtr xid); + + [DllImport(GdkName)] + static extern IntPtr gdk_set_allowed_backends(Utf8Buffer backends); + + [DllImport(GdkName)] + static extern IntPtr gdk_display_get_default(); + + [DllImport(GtkName)] + static extern IntPtr gtk_application_new(Utf8Buffer appId, int flags); + + [DllImport(GdkName)] + public static extern void gdk_window_set_transient_for(IntPtr window, IntPtr parent); + + public static IntPtr GetForeignWindow(IntPtr xid) => gdk_x11_window_foreign_new_for_display(s_display, xid); + + public static Task StartGtk() + { + var tcs = new TaskCompletionSource(); + new Thread(() => + { + try + { + using (var backends = new Utf8Buffer("x11")) + gdk_set_allowed_backends(backends); + } + catch + { + //Ignore + } + + Environment.SetEnvironmentVariable("WAYLAND_DISPLAY", + "/proc/fake-display-to-prevent-wayland-initialization-by-gtk3"); + + if (!gtk_init_check(0, IntPtr.Zero)) + { + tcs.SetResult(false); + return; + } + + IntPtr app; + using (var utf = new Utf8Buffer($"avalonia.app.a{Guid.NewGuid():N}")) + app = gtk_application_new(utf, 0); + if (app == IntPtr.Zero) + { + tcs.SetResult(false); + return; + } + + s_display = gdk_display_get_default(); + tcs.SetResult(true); + while (true) + gtk_main_iteration(); + }) {Name = "GTK3THREAD", IsBackground = true}.Start(); + return tcs.Task; + } + } +} diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs new file mode 100644 index 0000000000..61047ef2a9 --- /dev/null +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.Platform.Interop; +using static Avalonia.X11.NativeDialogs.Glib; +using static Avalonia.X11.NativeDialogs.Gtk; +// ReSharper disable AccessToModifiedClosure +namespace Avalonia.X11.NativeDialogs +{ + class GtkSystemDialog : ISystemDialogImpl + { + private Task _initialized; + private unsafe Task ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action, + bool multiSelect, string initialFileName) + { + IntPtr dlg; + using (var name = new Utf8Buffer(title)) + dlg = gtk_file_chooser_dialog_new(name, IntPtr.Zero, action, IntPtr.Zero); + UpdateParent(dlg, parent); + if (multiSelect) + gtk_file_chooser_set_select_multiple(dlg, true); + + gtk_window_set_modal(dlg, true); + var tcs = new TaskCompletionSource(); + List disposables = null; + + void Dispose() + { + // ReSharper disable once PossibleNullReferenceException + foreach (var d in disposables) d.Dispose(); + disposables.Clear(); + } + + disposables = new List + { + ConnectSignal(dlg, "close", delegate + { + tcs.TrySetResult(null); + Dispose(); + return false; + }), + ConnectSignal(dlg, "response", (_, resp, __) => + { + string[] result = null; + if (resp == GtkResponseType.Accept) + { + var resultList = new List(); + var gs = gtk_file_chooser_get_filenames(dlg); + var cgs = gs; + while (cgs != null) + { + if (cgs->Data != IntPtr.Zero) + resultList.Add(Utf8Buffer.StringFromPtr(cgs->Data)); + cgs = cgs->Next; + } + g_slist_free(gs); + result = resultList.ToArray(); + } + + gtk_widget_hide(dlg); + Dispose(); + tcs.TrySetResult(result); + return false; + }) + }; + using (var open = new Utf8Buffer("Open")) + gtk_dialog_add_button(dlg, open, GtkResponseType.Accept); + using (var open = new Utf8Buffer("Cancel")) + gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel); + if (initialFileName != null) + using (var fn = new Utf8Buffer(initialFileName)) + gtk_file_chooser_set_filename(dlg, fn); + gtk_window_present(dlg); + return tcs.Task; + } + + public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + await EnsureInitialized(); + return await await RunOnGlibThread( + () => ShowDialog(dialog.Title, parent, + dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save, + (dialog as OpenFileDialog)?.AllowMultiple ?? false, + Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory, + string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName))); + } + + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + await EnsureInitialized(); + return await await RunOnGlibThread(async () => + { + var res = await ShowDialog(dialog.Title, parent, + GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory); + return res?.FirstOrDefault(); + }); + } + + async Task EnsureInitialized() + { + if (_initialized == null) _initialized = StartGtk(); + + if (!(await _initialized)) + throw new Exception("Unable to initialize GTK on separate thread"); + } + + void UpdateParent(IntPtr chooser, IWindowImpl parentWindow) + { + var xid = parentWindow.Handle.Handle; + gtk_widget_realize(chooser); + var window = gtk_widget_get_window(chooser); + var parent = GetForeignWindow(xid); + if (window != IntPtr.Zero && parent != IntPtr.Zero) + gdk_window_set_transient_for(window, parent); + } + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 323411e319..5a19187a22 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.Gtk3; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; @@ -10,6 +9,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.X11; using Avalonia.X11.Glx; +using Avalonia.X11.NativeDialogs; using static Avalonia.X11.XLib; namespace Avalonia.X11 { @@ -45,7 +45,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new X11Clipboard(this)) .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) - .Bind().ToConstant(new Gtk3ForeignX11SystemDialog()); + .Bind().ToConstant(new GtkSystemDialog()); X11Screens = Avalonia.X11.X11Screens.Init(this); Screens = new X11Screens(X11Screens); diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs index f2a0520c10..6bfc8779da 100644 --- a/src/Avalonia.X11/X11Screens.cs +++ b/src/Avalonia.X11/X11Screens.cs @@ -218,6 +218,7 @@ namespace Avalonia.X11 class X11Screen { + private const int FullHDWidth = 1920; public bool Primary { get; } public string Name { get; set; } public PixelRect Bounds { get; set; } @@ -247,6 +248,6 @@ namespace Avalonia.X11 } public static double GuessPixelDensity(double pixelWidth, double mmWidth) - => Math.Max(1, Math.Round(pixelWidth / mmWidth * 25.4 / 96)); + => pixelWidth <= FullHDWidth ? 1 : Math.Max(1, Math.Round(pixelWidth / mmWidth * 25.4 / 96)); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index a825deeae3..b99864b050 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -8,11 +8,14 @@ using Avalonia.Platform; using Portable.Xaml; using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Reflection; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Text; +using System.Xml.Linq; +using System.Linq; namespace Avalonia.Markup.Xaml { @@ -240,15 +243,21 @@ namespace Avalonia.Markup.Xaml { using (var xamlInfoStream = assetLocator.Open(xamlInfoUri)) { - var xamlInfo = (AvaloniaResourceXamlInfo)s_xamlInfoSerializer.ReadObject(xamlInfoStream); - if (xamlInfo.ClassToResourcePathIndex.TryGetValue(typeName, out var rv) == true) + var assetDoc = XDocument.Load(xamlInfoStream); + XNamespace assetNs = assetDoc.Root.Attribute("xmlns").Value; + XNamespace arrayNs = "http://schemas.microsoft.com/2003/10/Serialization/Arrays"; + Dictionary xamlInfo = + assetDoc.Root.Element(assetNs + "ClassToResourcePathIndex").Elements(arrayNs + "KeyValueOfstringstring") + .ToDictionary(entry =>entry.Element(arrayNs + "Key").Value, + entry => entry.Element(arrayNs + "Value").Value); + + if (xamlInfo.TryGetValue(typeName, out var rv)) { yield return new Uri($"avares://{asm}{rv}"); yield break; } } - } - + } yield return new Uri("resm:" + typeName + ".xaml?assembly=" + asm); yield return new Uri("resm:" + typeName + ".paml?assembly=" + asm); diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs index 5d1a98f6f8..9fa6c26c35 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs @@ -77,40 +77,15 @@ namespace Avalonia.Markup.Xaml.PortableXaml _delayedValuesHelper.ApplyAll(); } - protected internal override void OnAfterBeginInit(object value) - { - //not called for avalonia objects - //as it's called inly for - //Portable.Xaml.ComponentModel.ISupportInitialize - base.OnAfterBeginInit(value); - } - - protected internal override void OnAfterEndInit(object value) - { - //not called for avalonia objects - //as it's called inly for - //Portable.Xaml.ComponentModel.ISupportInitialize - base.OnAfterEndInit(value); - } - protected internal override void OnAfterProperties(object value) { _delayedValuesHelper.EndInit(value); base.OnAfterProperties(value); - - //AfterEndInit is not called as it supports only - //Portable.Xaml.ComponentModel.ISupportInitialize - //and we have Avalonia.ISupportInitialize so we need some hacks - HandleEndEdit(value); } protected internal override void OnBeforeProperties(object value) { - //OnAfterBeginInit is not called as it supports only - //Portable.Xaml.ComponentModel.ISupportInitialize - //and we have Avalonia.ISupportInitialize so we need some hacks - HandleBeginInit(value); if (value != null) _delayedValuesHelper.BeginInit(value); @@ -127,16 +102,6 @@ namespace Avalonia.Markup.Xaml.PortableXaml return base.OnSetValue(target, member, value); } - private void HandleBeginInit(object value) - { - (value as Avalonia.ISupportInitialize)?.BeginInit(); - } - - private void HandleEndEdit(object value) - { - (value as Avalonia.ISupportInitialize)?.EndInit(); - } - public override void WriteStartMember(XamlMember property) { foreach(var d in DesignDirectives) diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs index 2d6e046f51..9a493a85c0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs @@ -33,7 +33,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml private IRuntimeTypeProvider _avaloniaTypeProvider; - protected internal override XamlType GetXamlType(string xamlNamespace, string name, params XamlType[] typeArguments) + protected override XamlType GetXamlType(string xamlNamespace, string name, params XamlType[] typeArguments) { XamlType type = null; try diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github index 8abbe09592..ab55261737 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 8abbe09592668efb573ac4d5548ba2d7e464ba78 +Subproject commit ab5526173722b8988bc5ca3c03c8752ce89c0975 diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index f66d3e51fc..55c3aab81f 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Data.Core; using Avalonia.Utilities; @@ -32,6 +33,11 @@ namespace Avalonia.Markup.Parsers public static IEnumerable Parse(string s) { var r = new CharacterReader(s.AsSpan()); + return Parse(ref r, null); + } + + private static IEnumerable Parse(ref CharacterReader r, char? end) + { var state = State.Start; var selector = new List(); while (!r.End && state != State.End) @@ -43,7 +49,7 @@ namespace Avalonia.Markup.Parsers state = ParseStart(ref r); break; case State.Middle: - state = ParseMiddle(ref r); + state = ParseMiddle(ref r, end); break; case State.CanHaveType: state = ParseCanHaveType(ref r); @@ -107,7 +113,7 @@ namespace Avalonia.Markup.Parsers return State.TypeName; } - private static State ParseMiddle(ref CharacterReader r) + private static State ParseMiddle(ref CharacterReader r, char? end) { if (r.TakeIf(':')) { @@ -129,6 +135,10 @@ namespace Avalonia.Markup.Parsers { return State.Name; } + else if (end.HasValue && !r.End && r.Peek == end.Value) + { + return State.End; + } return State.TypeName; } @@ -151,16 +161,23 @@ namespace Avalonia.Markup.Parsers } const string IsKeyword = "is"; + const string NotKeyword = "not"; + if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('(')) { var syntax = ParseType(ref r, new IsSyntax()); - if (r.End || !r.TakeIf(')')) - { - throw new ExpressionParseException(r.Position, $"Expected ')', got {r.Peek}"); - } + Expect(ref r, ')'); return (State.CanHaveType, syntax); } + if (identifier.SequenceEqual(NotKeyword.AsSpan()) && r.TakeIf('(')) + { + var argument = Parse(ref r, ')'); + Expect(ref r, ')'); + + var syntax = new NotSyntax { Argument = argument }; + return (State.Middle, syntax); + } else { return ( @@ -282,6 +299,18 @@ namespace Avalonia.Markup.Parsers return syntax; } + private static void Expect(ref CharacterReader r, char c) + { + if (r.End) + { + throw new ExpressionParseException(r.Position, $"Expected '{c}', got end of selector."); + } + else if (!r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.Peek}'."); + } + } + public interface ISyntax { } @@ -376,5 +405,15 @@ namespace Avalonia.Markup.Parsers return obj is TemplateSyntax; } } + + public class NotSyntax : ISyntax + { + public IEnumerable Argument { get; set; } + + public override bool Equals(object obj) + { + return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument); + } + } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index bf5b396bec..8d1216e1dc 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -2,6 +2,7 @@ // 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.Globalization; using Avalonia.Styling; using Avalonia.Utilities; @@ -25,7 +26,7 @@ namespace Avalonia.Markup.Parsers /// public SelectorParser(Func typeResolver) { - this._typeResolver = typeResolver; + _typeResolver = typeResolver; } /// @@ -36,6 +37,11 @@ namespace Avalonia.Markup.Parsers public Selector Parse(string s) { var syntax = SelectorGrammar.Parse(s); + return Create(syntax); + } + + private Selector Create(IEnumerable syntax) + { var result = default(Selector); foreach (var i in syntax) @@ -97,6 +103,11 @@ namespace Avalonia.Markup.Parsers case SelectorGrammar.TemplateSyntax template: result = result.Template(); break; + case SelectorGrammar.NotSyntax not: + result = result.Not(x => Create(not.Argument)); + break; + default: + throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'."); } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs index c34e26ac5c..d11319114f 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs @@ -19,6 +19,19 @@ namespace Avalonia.Base.UnitTests p = AttachedOwner.AttachedProperty; } + [Fact] + public void Registered_Properties_Count_Reflects_Newly_Added_Attached_Property() + { + var registry = new AvaloniaPropertyRegistry(); + var metadata = new StyledPropertyMetadata(); + var property = new AttachedProperty("test", typeof(object), metadata, true); + registry.Register(typeof(object), property); + registry.RegisterAttached(typeof(AvaloniaPropertyRegistryTests), property); + property.AddOwner(); + + Assert.Equal(1, registry.Properties.Count); + } + [Fact] public void GetRegistered_Returns_Registered_Properties() { @@ -138,5 +151,9 @@ namespace Avalonia.Base.UnitTests private class AttachedOwner2 : AttachedOwner { } + + private class Class4 : AvaloniaObject + { + } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs index 701fdbce9c..4585181ab7 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs @@ -150,6 +150,26 @@ namespace Avalonia.Base.UnitTests.Data.Core } } + [Fact] + public void Should_Work_With_Value_Type() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var source = new BehaviorSubject(1); + var data = new { Foo = source }; + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); + var result = new List(); + + var sub = target.Subscribe(x => result.Add((int)x)); + source.OnNext(42); + sync.ExecutePostedCallbacks(); + + Assert.Equal(new[] { 1, 42 }, result); + + GC.KeepAlive(data); + } + } + private class Class1 : NotifyingBase { public Subject Next { get; } = new Subject(); diff --git a/tests/Avalonia.Controls.UnitTests/ImageTests.cs b/tests/Avalonia.Controls.UnitTests/ImageTests.cs index e92fc572b4..71d0d1e328 100644 --- a/tests/Avalonia.Controls.UnitTests/ImageTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ImageTests.cs @@ -61,5 +61,61 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(50, 50), target.DesiredSize); } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_No_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.None; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 100, 400)); + + Assert.Equal(new Size(50, 100), target.Bounds.Size); + } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_Fill_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.Fill; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 25, 100)); + + Assert.Equal(new Size(25, 100), target.Bounds.Size); + } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_Uniform_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.Uniform; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 25, 100)); + + Assert.Equal(new Size(25, 50), target.Bounds.Size); + } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_UniformToFill_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.UniformToFill; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 25, 100)); + + Assert.Equal(new Size(25, 100), target.Bounds.Size); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index bbe1d85acb..2df925301f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 51a4d21392..6d00409ae0 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -199,6 +199,22 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Showing_Should_Raise_Opened() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new TestWindowBase(); + var raised = false; + + target.Opened += (s, e) => raised = true; + + target.Show(); + + Assert.True(raised); + } + } + [Fact] public void Hiding_Should_Stop_Renderer() { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index c0b5342934..8221dadc86 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -228,18 +228,35 @@ namespace Avalonia.Controls.UnitTests [Fact] public void ShowDialog_Should_Start_Renderer() { - using (UnitTestApplication.Start(TestServices.StyledWindow)) { + var parent = Mock.Of(); var renderer = new Mock(); var target = new Window(CreateImpl(renderer)); - target.Show(); + target.ShowDialog(parent); renderer.Verify(x => x.Start(), Times.Once); } } + [Fact] + public void ShowDialog_Should_Raise_Opened() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = Mock.Of(); + var target = new Window(); + var raised = false; + + target.Opened += (s, e) => raised = true; + + target.ShowDialog(parent); + + Assert.True(raised); + } + } + [Fact] public void Hiding_Should_Stop_Renderer() { diff --git a/tests/Avalonia.DesignerSupport.Tests/Helpers.cs b/tests/Avalonia.DesignerSupport.Tests/Helpers.cs new file mode 100644 index 0000000000..223a86a9af --- /dev/null +++ b/tests/Avalonia.DesignerSupport.Tests/Helpers.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.DesignerSupport.Tests +{ + static class Helpers + { + public static void StructDiff(object parsed, object expected) => StructDiff(parsed, expected, "{root}"); + + static void StructDiff(object parsed, object expected, string path) + { + if (parsed == null && expected == null) + return; + if ((parsed == null && expected != null) || (parsed != null && expected == null)) + throw new Exception( + $"{path}: Null mismatch: {(parsed == null ? "null" : "not-null")} {(expected == null ? "null" : "not-null")}"); + + if (parsed.GetType() != expected.GetType()) + throw new Exception($"{path}: Type mismatch: {parsed.GetType()} {expected.GetType()}"); + + if (parsed is string || parsed.GetType().IsPrimitive) + { + if (!parsed.Equals(expected)) + throw new Exception($"{path}: Not equal {parsed} {expected}"); + } + else if (parsed is IDictionary dic) + { + var dic2 = (IDictionary) expected; + if (dic.Count != dic2.Count) + throw new Exception($"{path}: Dictionary count mismatch: {dic.Count} {dic2.Count}"); + + foreach (var k in dic.Keys.Cast().OrderBy(o => o.ToString())) + { + var v1 = dic[k]; + var v2 = dic2[k]; + StructDiff(v1, v2, path + "['" + k + "']"); + } + } + else if (parsed is IList col) + { + var col2 = (IList) expected; + if (col.Count != col2.Count) + throw new Exception($"{path}: Collection count mismatch: {col.Count} {col2.Count}"); + for (var c = 0; c < col.Count; c++) + StructDiff(col[c], col2[c], path + "[" + c + "]"); + } + else + { + foreach (var prop in parsed.GetType().GetProperties() + .Where(p => p.GetMethod != null && p.GetMethod.IsPublic)) + { + StructDiff(prop.GetValue(parsed), prop.GetValue(expected), path + "." + prop.Name); + } + } + + + + } + } +} diff --git a/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs b/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs new file mode 100644 index 0000000000..e5a477cc32 --- /dev/null +++ b/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Remote.Protocol; +using Avalonia.Remote.Protocol.Viewport; +using Xunit; + +namespace Avalonia.DesignerSupport.Tests +{ + public class RemoteProtocolTests : IDisposable + { + private readonly List _disposables = new List(); + private IAvaloniaRemoteTransportConnection _server; + private IAvaloniaRemoteTransportConnection _client; + private BlockingCollection _serverMessages = new BlockingCollection(); + private BlockingCollection _clientMessages = new BlockingCollection(); + private SynchronizationContext _originalContext; + + + class DisabledSyncContext : SynchronizationContext + { + public override void Post(SendOrPostCallback d, object state) + { + throw new InvalidCastException("Not allowed"); + } + + public override void Send(SendOrPostCallback d, object state) + { + throw new InvalidCastException("Not allowed"); + } + } + + void Init(IMessageTypeResolver clientResolver = null, IMessageTypeResolver serverResolver = null) + { + _originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(new DisabledSyncContext()); + var clientTransport = new BsonTcpTransport(clientResolver ?? new DefaultMessageTypeResolver()); + var serverTransport = new BsonTcpTransport(serverResolver ?? new DefaultMessageTypeResolver()); + + var tcpListener = new TcpListener(IPAddress.Loopback, 0); + tcpListener.Start(); + var port = ((IPEndPoint)tcpListener.LocalEndpoint).Port; + tcpListener.Stop(); + + var tcs = new TaskCompletionSource(); + serverTransport.Listen(IPAddress.Loopback, port, connected => + { + _server = connected; + tcs.SetResult(0); + }); + _client = clientTransport.Connect(IPAddress.Loopback, port).Result; + _disposables.Add(_client); + _client.OnMessage += (_, m) => _clientMessages.Add(m); + tcs.Task.Wait(); + _disposables.Add(_server); + _server.OnMessage += (_, m) => _serverMessages.Add(m); + + } + + object TakeServer() + { + var src = new CancellationTokenSource(200); + try + { + return _serverMessages.Take(src.Token); + } + finally + { + src.Dispose(); + } + + } + + [Fact] + void EntitiesAreProperlySerializedAndDeserialized() + { + Init(); + var rnd = new Random(); + _server.OnMessage += (_, message) => { }; + + + object GetRandomValue(Type t, string pathInfo) + { + if (t.IsArray) + { + var arr = Array.CreateInstance(t.GetElementType(), 1); + ((IList)arr)[0] = GetRandomValue(t.GetElementType(), pathInfo); + return arr; + } + + if (t == typeof(bool)) + return true; + if (t == typeof(int) || t == typeof(long)) + return rnd.Next(); + if (t == typeof(byte)) + return (byte)rnd.Next(255); + if (t == typeof(double)) + return rnd.NextDouble(); + if (t.IsEnum) + return ((IList)Enum.GetValues(t)).Cast().Last(); + if (t == typeof(string)) + return Guid.NewGuid().ToString(); + if (t == typeof(Guid)) + return Guid.NewGuid(); + throw new Exception($"Doesn't know how to fabricate a random value for {t}, path {pathInfo}"); + } + + foreach (var t in typeof(MeasureViewportMessage).Assembly.GetTypes().Where(t => + t.GetCustomAttribute(typeof(AvaloniaRemoteMessageGuidAttribute)) != null)) + { + var o = Activator.CreateInstance(t); + foreach (var p in t.GetProperties()) + p.SetValue(o, GetRandomValue(p.PropertyType, $"{t.FullName}.{p.Name}")); + + _client.Send(o).Wait(200); + var received = TakeServer(); + Helpers.StructDiff(received, o); + + } + + + } + + [Fact] + void RemoteProtocolShouldBeBackwardsCompatible() + { + Init(new DefaultMessageTypeResolver(typeof(ExtendedMeasureViewportMessage).Assembly)); + _client.Send(new ExtendedMeasureViewportMessage() + { + Width = 100, Height = 200, SomeNewProperty = 300, + SomeArrayProperty = new[]{1,2,3}, + SubObjectProperty = new ExtendedMeasureViewportMessage.SubObject() + { + Foo = 543 + } + }); + var received = (MeasureViewportMessage)TakeServer(); + Assert.Equal(100, received.Width); + Assert.Equal(200, received.Height); + + } + + public void Dispose() + { + _disposables.ForEach(d => d.Dispose()); + SynchronizationContext.SetSynchronizationContext(_originalContext); + } + } + + [AvaloniaRemoteMessageGuid("6E3C5310-E2B1-4C3D-8688-01183AA48C5B")] + public class ExtendedMeasureViewportMessage + { + public double Width { get; set; } + + public int SomeNewProperty { get; set; } + public int[] SomeArrayProperty { get; set; } + public class SubObject + { + public int Foo { get; set; } + } + public SubObject SubObjectProperty { get; set; } + public double Height { get; set; } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 88fe5a2a12..e3ce4b0968 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -200,6 +200,67 @@ namespace Avalonia.Markup.UnitTests.Parsers result); } + [Fact] + public void Not_OfType() + { + var result = SelectorGrammar.Parse(":not(Button)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NotSyntax + { + Argument = new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + }, + } + }, + result); + } + + [Fact] + public void OfType_Not_Class() + { + var result = SelectorGrammar.Parse("Button:not(.foo)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NotSyntax + { + Argument = new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + } + }, + result); + } + + [Fact] + public void Is_Descendent_Not_OfType_Class() + { + var result = SelectorGrammar.Parse(":is(Control) :not(Button.foo)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.IsSyntax { TypeName = "Control" }, + new SelectorGrammar.DescendantSyntax { }, + new SelectorGrammar.NotSyntax + { + Argument = new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + } + }, + result); + } + [Fact] public void Namespace_Alone_Fails() { @@ -223,5 +284,17 @@ namespace Avalonia.Markup.UnitTests.Parsers { Assert.Throws(() => SelectorGrammar.Parse(".%foo")); } + + [Fact] + public void Not_Without_Argument_Fails() + { + Assert.Throws(() => SelectorGrammar.Parse(":not()")); + } + + [Fact] + public void Not_Without_Closing_Parenthesis_Fails() + { + Assert.Throws(() => SelectorGrammar.Parse(":not(Button")); + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs index 3ecb2d9f37..104f46cbac 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs @@ -4,6 +4,7 @@ using Avalonia.Controls; using Avalonia.LogicalTree; using System.Collections.Generic; +using System.ComponentModel; namespace Avalonia.Markup.Xaml.UnitTests.Xaml { @@ -39,4 +40,4 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Order.Add($"EndInit {InitState}"); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index beaf7477d0..a84ce74a88 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -198,5 +198,33 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml ex.InnerException.Message); } } + + [Fact] + public void Style_Can_Use_Not_Selector() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var foo = window.FindControl("foo"); + var notFoo = window.FindControl("notFoo"); + + Assert.Null(foo.Background); + Assert.Equal(Colors.Red, ((ISolidColorBrush)notFoo.Background).Color); + } + } } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index b782311729..70a5504a7d 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -10,6 +10,7 @@ using ReactiveUI; using DynamicData; using Xunit; using Splat; +using Avalonia.Markup.Xaml; namespace Avalonia { @@ -70,12 +71,40 @@ namespace Avalonia public class ActivatableWindow : ReactiveWindow { - public ActivatableWindow() => this.WhenActivated(disposables => { }); + public ActivatableWindow() + { + InitializeComponent(); + Assert.IsType(Content); + this.WhenActivated(disposables => { }); + } + + private void InitializeComponent() + { + var loader = new AvaloniaXamlLoader(); + loader.Load(@" + + +", null, this); + } } public class ActivatableUserControl : ReactiveUserControl { - public ActivatableUserControl() => this.WhenActivated(disposables => { }); + public ActivatableUserControl() + { + InitializeComponent(); + Assert.IsType(Content); + this.WhenActivated(disposables => { }); + } + + private void InitializeComponent() + { + var loader = new AvaloniaXamlLoader(); + loader.Load(@" + + +", null, this); + } } public AvaloniaActivationForViewFetcherTest() @@ -183,4 +212,4 @@ namespace Avalonia Assert.False(viewModel.IsActivated); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs new file mode 100644 index 0000000000..c85b999af2 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -0,0 +1,104 @@ +using System; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Rendering; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Avalonia; +using ReactiveUI; +using DynamicData; +using Xunit; +using Splat; +using Avalonia.Markup.Xaml; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Reactive; + +namespace Avalonia +{ + public class RoutedViewHostTest + { + public class FirstRoutableViewModel : ReactiveObject, IRoutableViewModel + { + public string UrlPathSegment => "first"; + + public IScreen HostScreen { get; set; } + } + + public class FirstRoutableView : ReactiveUserControl { } + + public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel + { + public string UrlPathSegment => "second"; + + public IScreen HostScreen { get; set; } + } + + public class SecondRoutableView : ReactiveUserControl { } + + public class ScreenViewModel : ReactiveObject, IScreen + { + public RoutingState Router { get; } = new RoutingState(); + } + + public RoutedViewHostTest() + { + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor)); + } + + [Fact] + public void RoutedViewHostShouldStayInSyncWithRoutingState() + { + var screen = new ScreenViewModel(); + var defaultContent = new TextBlock(); + var host = new RoutedViewHost + { + Router = screen.Router, + DefaultContent = defaultContent, + FadeOutAnimation = null, + FadeInAnimation = null + }; + + var root = new TestRoot + { + Child = host + }; + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + screen.Router.Navigate + .Execute(new FirstRoutableViewModel()) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstRoutableView), host.Content.GetType()); + + screen.Router.Navigate + .Execute(new SecondRoutableViewModel()) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(SecondRoutableView), host.Content.GetType()); + + screen.Router.NavigateBack + .Execute(Unit.Default) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstRoutableView), host.Content.GetType()); + + screen.Router.NavigateBack + .Execute(Unit.Default) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs new file mode 100644 index 0000000000..2f3e2b8f34 --- /dev/null +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs @@ -0,0 +1,114 @@ +// 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.Reactive.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Xunit; + +namespace Avalonia.Styling.UnitTests +{ + public class SelectorTests_Not + { + [Fact] + public void Not_Selector_Should_Have_Correct_String_Representation() + { + var target = default(Selector).Not(x => x.Class("foo")); + + Assert.Equal(":not(.foo)", target.ToString()); + } + + [Fact] + public void Not_OfType_Matches_Control_Of_Incorrect_Type() + { + var control = new Control1(); + var target = default(Selector).Not(x => x.OfType()); + + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(control).Result); + } + + [Fact] + public void Not_OfType_Doesnt_Match_Control_Of_Correct_Type() + { + var control = new Control2(); + var target = default(Selector).Not(x => x.OfType()); + + Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result); + } + + [Fact] + public async Task Not_Class_Doesnt_Match_Control_With_Class() + { + var control = new Control1 + { + Classes = new Classes { "foo" }, + }; + + var target = default(Selector).Not(x => x.Class("foo")); + var match = target.Match(control); + + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + Assert.False(await match.Activator.Take(1)); + } + + [Fact] + public async Task Not_Class_Matches_Control_Without_Class() + { + var control = new Control1 + { + Classes = new Classes { "bar" }, + }; + + var target = default(Selector).Not(x => x.Class("foo")); + var match = target.Match(control); + + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + Assert.True(await match.Activator.Take(1)); + } + + [Fact] + public async Task OfType_Not_Class_Matches_Control_Without_Class() + { + var control = new Control1 + { + Classes = new Classes { "bar" }, + }; + + var target = default(Selector).OfType().Not(x => x.Class("foo")); + var match = target.Match(control); + + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + Assert.True(await match.Activator.Take(1)); + } + + [Fact] + public void OfType_Not_Class_Doesnt_Match_Control_Of_Wrong_Type() + { + var control = new Control2 + { + Classes = new Classes { "foo" }, + }; + + var target = default(Selector).OfType().Not(x => x.Class("foo")); + var match = target.Match(control); + + Assert.Equal(SelectorMatchResult.NeverThisType, match.Result); + } + + [Fact] + public void Returns_Correct_TargetType() + { + var target = default(Selector).OfType().Not(x => x.Class("foo")); + + Assert.Equal(typeof(Control1), target.TargetType); + } + + public class Control1 : TestControlBase + { + } + + public class Control2 : TestControlBase + { + } + } +} diff --git a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs index 4096dcf380..4970addd81 100644 --- a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs @@ -10,6 +10,7 @@ using Avalonia.UnitTests; using Xunit; using Avalonia.LogicalTree; using Avalonia.Controls; +using System.ComponentModel; namespace Avalonia.Styling.UnitTests {