From 88dd17083add2b1134334ad8a16ca5f5d3f20702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Wed, 10 Feb 2021 01:49:23 +0000 Subject: [PATCH 01/47] Use deferred renderer on Android. --- .../Avalonia.Android/ActivityTracker.cs | 47 ------------ .../Avalonia.Android/AndroidPlatform.cs | 8 +- .../Avalonia.Android/AvaloniaActivity.cs | 3 +- src/Android/Avalonia.Android/AvaloniaView.cs | 27 ++++++- .../Avalonia.Android/ChoreographerTimer.cs | 74 +++++++++++++++++++ .../OpenGL/GlPlatformSurface.cs | 6 +- .../Avalonia.Android/OpenGL/GlRenderTarget.cs | 13 +++- .../Platform/SkiaPlatform/TopLevelImpl.cs | 8 +- 8 files changed, 122 insertions(+), 64 deletions(-) delete mode 100644 src/Android/Avalonia.Android/ActivityTracker.cs create mode 100644 src/Android/Avalonia.Android/ChoreographerTimer.cs diff --git a/src/Android/Avalonia.Android/ActivityTracker.cs b/src/Android/Avalonia.Android/ActivityTracker.cs deleted file mode 100644 index 2ad1d9e361..0000000000 --- a/src/Android/Avalonia.Android/ActivityTracker.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Android.App; -using Android.OS; - -namespace Avalonia.Android -{ - internal class ActivityTracker : Java.Lang.Object, global::Android.App.Application.IActivityLifecycleCallbacks - { - public static Activity Current { get; private set; } - public void OnActivityCreated(Activity activity, Bundle savedInstanceState) - { - Current = activity; - } - - public void OnActivityDestroyed(Activity activity) - { - if (Current == activity) - Current = null; - } - - public void OnActivityPaused(Activity activity) - { - if (Current == activity) - Current = null; - } - - public void OnActivityResumed(Activity activity) - { - Current = activity; - } - - public void OnActivitySaveInstanceState(Activity activity, Bundle outState) - { - Current = activity; - } - - public void OnActivityStarted(Activity activity) - { - Current = activity; - } - - public void OnActivityStopped(Activity activity) - { - if (Current == activity) - Current = null; - } - } -} \ No newline at end of file diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index e0ceb0c8b7..043af6a8df 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -32,6 +32,7 @@ namespace Avalonia.Android class AndroidPlatform : IPlatformSettings, IWindowingPlatform { public static readonly AndroidPlatform Instance = new AndroidPlatform(); + public static AndroidPlatformOptions Options { get; private set; } public Size DoubleClickSize => new Size(4, 4); public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200); public double RenderScalingFactor => _scalingFactor; @@ -46,6 +47,8 @@ namespace Avalonia.Android public static void Initialize(Type appType, AndroidPlatformOptions options) { + Options = options; + AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToTransient() @@ -55,14 +58,12 @@ namespace Avalonia.Android .Bind().ToTransient() .Bind().ToConstant(Instance) .Bind().ToSingleton() - .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToConstant(new ChoreographerTimer()) .Bind().ToConstant(new RenderLoop()) .Bind().ToSingleton() .Bind().ToConstant(new AssetLoader(appType.Assembly)); SkiaPlatform.Initialize(); - ((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext) - .RegisterActivityLifecycleCallbacks(new ActivityTracker()); if (options.UseGpu) { @@ -83,6 +84,7 @@ namespace Avalonia.Android public sealed class AndroidPlatformOptions { + public bool UseDeferredRendering { get; set; } = true; public bool UseGpu { get; set; } = true; } } diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs index d3696aa41d..52b68f8e2f 100644 --- a/src/Android/Avalonia.Android/AvaloniaActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs @@ -13,9 +13,8 @@ namespace Avalonia.Android protected override void OnCreate(Bundle savedInstanceState) { - RequestWindowFeature(WindowFeatures.NoTitle); View = new AvaloniaView(this); - if(_content != null) + if (_content != null) View.Content = _content; SetContentView(View); TakeKeyEvents(true); diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 72732a1f95..a60a17d08e 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -1,11 +1,12 @@ using System; using Android.Content; +using Android.Runtime; using Android.Views; using Android.Widget; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; using Avalonia.Controls.Embedding; -using Avalonia.Platform; +using Avalonia.Rendering; namespace Avalonia.Android { @@ -33,6 +34,30 @@ namespace Avalonia.Android return _view.View.DispatchKeyEvent(e); } + public override void OnVisibilityAggregated(bool isVisible) + { + base.OnVisibilityAggregated(isVisible); + OnVisibilityChanged(isVisible); + } + + protected override void OnVisibilityChanged(View changedView, [GeneratedEnum] ViewStates visibility) + { + base.OnVisibilityChanged(changedView, visibility); + OnVisibilityChanged(visibility == ViewStates.Visible); + } + + private void OnVisibilityChanged(bool isVisible) + { + if (isVisible) + { + _root.Renderer.Start(); + } + else + { + _root.Renderer.Stop(); + } + } + class ViewImpl : TopLevelImpl { public ViewImpl(Context context) : base(context) diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs new file mode 100644 index 0000000000..12961fec83 --- /dev/null +++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs @@ -0,0 +1,74 @@ +using System; +using System.Threading.Tasks; + +using Android.OS; +using Android.Views; + +using Avalonia.Rendering; + +using Java.Lang; + +namespace Avalonia.Android +{ + internal sealed class ChoreographerTimer : Java.Lang.Object, IRenderTimer, Choreographer.IFrameCallback + { + private readonly object _lock = new object(); + + private readonly Thread _thread; + private readonly TaskCompletionSource _choreographer = new TaskCompletionSource(); + + private Action _tick; + private int _count; + + public ChoreographerTimer() + { + _thread = new Thread(Loop); + _thread.Start(); + } + + public event Action Tick + { + add + { + lock (_lock) + { + _tick += value; + _count++; + + if (_count == 1) + { + _choreographer.Task.Result.PostFrameCallback(this); + } + } + } + remove + { + lock (_lock) + { + _tick -= value; + _count--; + } + } + } + + private void Loop() + { + Looper.Prepare(); + _choreographer.SetResult(Choreographer.Instance); + Looper.Loop(); + } + + public void DoFrame(long frameTimeNanos) + { + _tick?.Invoke(TimeSpan.FromTicks(frameTimeNanos / 100)); + + lock (_lock) + { + if (_count > 0) + { + Choreographer.Instance.PostFrameCallback(this); + } + } + } + } +} diff --git a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs index 4f4c03fe77..a9710039f8 100644 --- a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs +++ b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs @@ -1,6 +1,4 @@ -using System.Linq; - -using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; namespace Avalonia.Android.OpenGL @@ -17,7 +15,7 @@ namespace Avalonia.Android.OpenGL } public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() => - new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle)); + new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle), _info.Handle); public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info) { diff --git a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs index 75bbd15e3e..f9071d9b27 100644 --- a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs +++ b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs @@ -1,23 +1,30 @@ -using Avalonia.OpenGL.Egl; +using System; + +using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; namespace Avalonia.Android.OpenGL { - internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase + internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo { private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; private readonly EglSurface _surface; + private readonly IntPtr _handle; public GlRenderTarget( EglPlatformOpenGlInterface egl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, - EglSurface surface) + EglSurface surface, + IntPtr handle) : base(egl) { _info = info; _surface = surface; + _handle = handle; } + public bool IsCorrupted => _handle != _info.Handle; + public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info); } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index a8c7f7af9b..a71b574198 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -98,10 +98,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IEnumerable Surfaces => new object[] { _gl, _framebuffer }; - public IRenderer CreateRenderer(IRenderRoot root) - { - return new ImmediateRenderer(root); - } + public IRenderer CreateRenderer(IRenderRoot root) => + AndroidPlatform.Options.UseDeferredRendering + ? new DeferredRenderer(root, AvaloniaLocator.Current.GetService()) { RenderOnlyOnRenderThread = true } + : new ImmediateRenderer(root); public virtual void Hide() { From b9a2f76cf0bcbb49e9bab5a50f00de8789c2e9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Wed, 10 Feb 2021 23:58:51 +0000 Subject: [PATCH 02/47] Added timer subscriptions for views. --- src/Android/Avalonia.Android/AvaloniaView.cs | 8 +++++ .../Avalonia.Android/ChoreographerTimer.cs | 29 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index a60a17d08e..8de3657283 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -15,6 +15,8 @@ namespace Avalonia.Android private readonly EmbeddableControlRoot _root; private readonly ViewImpl _view; + private IDisposable? _timerSubscription; + public AvaloniaView(Context context) : base(context) { _view = new ViewImpl(context); @@ -50,11 +52,17 @@ namespace Avalonia.Android { if (isVisible) { + if (AvaloniaLocator.Current.GetService() is ChoreographerTimer timer) + { + _timerSubscription = timer.SubscribeView(this); + } + _root.Renderer.Start(); } else { _root.Renderer.Stop(); + _timerSubscription?.Dispose(); } } diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs index 12961fec83..1d898261a3 100644 --- a/src/Android/Avalonia.Android/ChoreographerTimer.cs +++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Reactive.Disposables; using System.Threading.Tasks; using Android.OS; @@ -17,6 +19,8 @@ namespace Avalonia.Android private readonly Thread _thread; private readonly TaskCompletionSource _choreographer = new TaskCompletionSource(); + private readonly ISet _views = new HashSet(); + private Action _tick; private int _count; @@ -51,6 +55,29 @@ namespace Avalonia.Android } } + internal IDisposable SubscribeView(AvaloniaView view) + { + lock (_lock) + { + _views.Add(view); + + if (_views.Count == 1) + { + _choreographer.Task.Result.PostFrameCallback(this); + } + } + + return Disposable.Create( + () => + { + lock (_lock) + { + _views.Remove(view); + } + } + ); + } + private void Loop() { Looper.Prepare(); @@ -64,7 +91,7 @@ namespace Avalonia.Android lock (_lock) { - if (_count > 0) + if (_count > 0 && _views.Count > 0) { Choreographer.Instance.PostFrameCallback(this); } From e1bfe10f434e61fc4eaa36b8b2ed39c7cf6a0464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Thu, 11 Feb 2021 00:02:10 +0000 Subject: [PATCH 03/47] Fixed CursorFactory. --- src/Android/Avalonia.Android/AndroidPlatform.cs | 2 +- src/Android/Avalonia.Android/CursorFactory.cs | 17 +++++++++++++---- .../Platform/SkiaPlatform/TopLevelImpl.cs | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 043af6a8df..e0ed9b1fda 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -51,7 +51,7 @@ namespace Avalonia.Android AvaloniaLocator.CurrentMutable .Bind().ToTransient() - .Bind().ToTransient() + .Bind().ToTransient() .Bind().ToSingleton() .Bind().ToConstant(Instance) .Bind().ToConstant(new AndroidThreadingInterface()) diff --git a/src/Android/Avalonia.Android/CursorFactory.cs b/src/Android/Avalonia.Android/CursorFactory.cs index 9eb28c67f9..6293637d4e 100644 --- a/src/Android/Avalonia.Android/CursorFactory.cs +++ b/src/Android/Avalonia.Android/CursorFactory.cs @@ -1,12 +1,21 @@ -using System; using Avalonia.Input; using Avalonia.Platform; namespace Avalonia.Android { - internal class CursorFactory : IStandardCursorFactory + internal class CursorFactory : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) - => new PlatformHandle(IntPtr.Zero, "ZeroCursor"); + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => CursorImpl.ZeroCursor; + + public ICursorImpl GetCursor(StandardCursorType cursorType) => CursorImpl.ZeroCursor; + + private sealed class CursorImpl : ICursorImpl + { + public static CursorImpl ZeroCursor { get; } = new CursorImpl(); + + private CursorImpl() { } + + public void Dispose() { } + } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index a71b574198..10bf414f25 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -123,7 +123,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform return PixelPoint.FromPoint(point, 1); } - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { //still not implemented } From cf8117d9ecfe53687879ee4c6af1223f78d8319d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 5 Mar 2021 17:58:08 +0000 Subject: [PATCH 04/47] change default extend chrome hint --- src/Avalonia.Controls/ApiCompatBaseline.txt | 3 ++- src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs | 2 +- src/Avalonia.Native/avn.idl | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index e5adc8c6ed..f55f440db9 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -1,6 +1,7 @@ Compat issues with assembly Avalonia.Controls: MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. +EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -Total Issues: 4 +Total Issues: 5 diff --git a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs index de3f58886b..bb3c0288eb 100644 --- a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs +++ b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs @@ -16,7 +16,7 @@ namespace Avalonia.Platform /// /// The default for the platform. /// - Default = SystemChrome, + Default = PreferSystemChrome, /// /// Use SystemChrome diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 57a0c32067..476e64bd2d 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -397,7 +397,7 @@ enum AvnExtendClientAreaChromeHints AvnSystemChrome = 0x01, AvnPreferSystemChrome = 0x02, AvnOSXThickTitleBar = 0x08, - AvnDefaultChrome = AvnSystemChrome, + AvnDefaultChrome = AvnPreferSystemChrome, } [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] From 1fa6bd25eb16e4ace4990bd9638cbd843cb57a5a Mon Sep 17 00:00:00 2001 From: luthfiampas Date: Sun, 7 Mar 2021 14:27:42 +0700 Subject: [PATCH 05/47] better TextBox.MaxLength tests --- .../TextBoxTests.cs | 62 +++++++++++++++---- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 6ac7799828..9f09c19110 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -646,22 +646,49 @@ namespace Avalonia.Controls.UnitTests Assert.Null(target.Text); } } - - [Fact] - public void Text_Box_MaxLength_Work_Properly() + + [Theory] + [InlineData("abc", "d", 3, 0, 0, false, "abc")] + [InlineData("abc", "dd", 4, 3, 3, false, "abcd")] + [InlineData("abc", "ddd", 3, 0, 2, true, "ddc")] + [InlineData("abc", "dddd", 4, 1, 3, true, "addd")] + [InlineData("abc", "ddddd", 5, 3, 3, true, "abcdd")] + public void MaxLength_Works_Properly( + string initalText, + string textInput, + int maxLength, + int selectionStart, + int selectionEnd, + bool fromClipboard, + string expected) { using (UnitTestApplication.Start(Services)) { var target = new TextBox { Template = CreateTemplate(), - Text = "abc", - MaxLength = 3, + Text = initalText, + MaxLength = maxLength, + SelectionStart = selectionStart, + SelectionEnd = selectionEnd }; - - RaiseKeyEvent(target, Key.D, KeyModifiers.None); - - Assert.Equal("abc", target.Text); + + if (fromClipboard) + { + AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); + + var clipboard = AvaloniaLocator.CurrentMutable.GetService(); + clipboard.SetTextAsync(textInput).GetAwaiter().GetResult(); + + RaiseKeyEvent(target, Key.V, KeyModifiers.Control); + clipboard.ClearAsync().GetAwaiter().GetResult(); + } + else + { + RaiseTextEvent(target, textInput); + } + + Assert.Equal(expected, target.Text); } } @@ -758,11 +785,22 @@ namespace Avalonia.Controls.UnitTests private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard { - public Task GetTextAsync() => Task.FromResult(""); + private string _text; + + public Task GetTextAsync() => Task.FromResult(_text); - public Task SetTextAsync(string text) => Task.CompletedTask; + public Task SetTextAsync(string text) + { + _text = text; + return Task.CompletedTask; + } - public Task ClearAsync() => Task.CompletedTask; + public Task ClearAsync() + { + _text = null; + return Task.CompletedTask; + } + public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); From 17971b884187176615196564569e314a99c7544b Mon Sep 17 00:00:00 2001 From: luthfiampas Date: Sun, 7 Mar 2021 14:40:41 +0700 Subject: [PATCH 06/47] TextBox.MaxLength should respect occurred text from clipboard --- src/Avalonia.Controls/TextBox.cs | 43 +++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 54d3af9b59..44d1a1c489 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -514,21 +514,36 @@ namespace Avalonia.Controls private void HandleTextInput(string input) { - if (!IsReadOnly) + if (IsReadOnly) { - input = RemoveInvalidCharacters(input); - string text = Text ?? string.Empty; - int caretIndex = CaretIndex; - if (!string.IsNullOrEmpty(input) && (MaxLength == 0 || input.Length + text.Length - (Math.Abs(SelectionStart - SelectionEnd)) <= MaxLength)) - { - DeleteSelection(); - caretIndex = CaretIndex; - text = Text ?? string.Empty; - SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); - CaretIndex += input.Length; - ClearSelection(); - _undoRedoHelper.DiscardRedo(); - } + return; + } + + input = RemoveInvalidCharacters(input); + + if (string.IsNullOrEmpty(input)) + { + return; + } + + string text = Text ?? string.Empty; + int caretIndex = CaretIndex; + int estimatedLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); + + if (MaxLength > 0 && estimatedLength > MaxLength) + { + input = input.Remove(Math.Max(0, input.Length - (estimatedLength - MaxLength))); + } + + if (!string.IsNullOrEmpty(input)) + { + DeleteSelection(); + caretIndex = CaretIndex; + text = Text ?? string.Empty; + SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); + CaretIndex += input.Length; + ClearSelection(); + _undoRedoHelper.DiscardRedo(); } } From f22730aca8f975e7525d44651af952db7b4df32c Mon Sep 17 00:00:00 2001 From: aguahombre Date: Mon, 8 Mar 2021 11:16:17 +0000 Subject: [PATCH 07/47] AutoCompleteBox validation not working for SelectedItem #5586 --- src/Avalonia.Controls/AutoCompleteBox.cs | 8 +++-- .../AutoCompleteBoxTests.cs | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index b59fd7abde..aab6a41890 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -483,7 +483,9 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect( nameof(SelectedItem), o => o.SelectedItem, - (o, v) => o.SelectedItem = v); + (o, v) => o.SelectedItem = v, + defaultBindingMode: BindingMode.TwoWay, + enableDataValidation: true); /// /// Identifies the @@ -1333,7 +1335,7 @@ namespace Avalonia.Controls base.OnApplyTemplate(e); } - + /// /// Called to update the validation state for properties for which data validation is /// enabled. @@ -1342,7 +1344,7 @@ namespace Avalonia.Controls /// The new binding value for the property. protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { - if (property == TextProperty) + if (property == TextProperty || property == SelectedItemProperty) { DataValidationErrors.SetError(this, value.Error); } diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 3e78e951e2..b346fca330 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -14,6 +14,8 @@ using Avalonia.UnitTests; using Moq; using Xunit; using System.Collections.ObjectModel; +using System.Reactive.Linq; +using System.Reactive.Subjects; namespace Avalonia.Controls.UnitTests { @@ -396,6 +398,36 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(control.Text, control.ItemSelector(input, selectedItem)); }); } + + [Fact] + public void Text_Validation() + { + RunTest((control, textbox) => + { + var exception = new InvalidCastException("failed validation"); + var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); + control.Bind(AutoCompleteBox.TextProperty, textObservable); + Dispatcher.UIThread.RunJobs(); + + Assert.Equal(DataValidationErrors.GetHasErrors(control), true); + Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true); + }); + } + + [Fact] + public void SelectedItem_Validation() + { + RunTest((control, textbox) => + { + var exception = new InvalidCastException("failed validation"); + var itemObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); + control.Bind(AutoCompleteBox.SelectedItemProperty, itemObservable); + Dispatcher.UIThread.RunJobs(); + + Assert.Equal(DataValidationErrors.GetHasErrors(control), true); + Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true); + }); + } /// /// Retrieves a defined predicate filter through a new AutoCompleteBox From 5d130b5d7938efc36beda0eab7715e4a22e7ae06 Mon Sep 17 00:00:00 2001 From: aguahombre Date: Mon, 8 Mar 2021 15:21:31 +0000 Subject: [PATCH 08/47] Add data validation error support to NumericUpDown control #3591 --- .../NumericUpDown/NumericUpDown.cs | 18 +++- .../NumericUpDownTests.cs | 95 +++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index b6b71644a3..abfbc038eb 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -91,14 +91,14 @@ namespace Avalonia.Controls /// public static readonly DirectProperty TextProperty = AvaloniaProperty.RegisterDirect(nameof(Text), o => o.Text, (o, v) => o.Text = v, - defaultBindingMode: BindingMode.TwoWay); + defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// /// Defines the property. /// public static readonly DirectProperty ValueProperty = AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, - (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay); + (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// /// Defines the property. @@ -370,6 +370,20 @@ namespace Avalonia.Controls } } + /// + /// Called to update the validation state for properties for which data validation is + /// enabled. + /// + /// The property. + /// The new binding value for the property. + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + { + if (property == TextProperty || property == ValueProperty) + { + DataValidationErrors.SetError(this, value.Error); + } + } + /// /// Called when the property value changed. /// diff --git a/tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs b/tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs new file mode 100644 index 0000000000..4cef7e4d05 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using System.Reactive.Subjects; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Threading; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class NumericUpDownTests + { + private static TestServices Services => TestServices.StyledWindow; + + [Fact] + public void Text_Validation() + { + RunTest((control, textbox) => + { + var exception = new InvalidCastException("failed validation"); + var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); + control.Bind(NumericUpDown.TextProperty, textObservable); + Dispatcher.UIThread.RunJobs(); + + Assert.True(DataValidationErrors.GetHasErrors(control)); + Assert.True(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception })); + }); + } + + [Fact] + public void Value_Validation() + { + RunTest((control, textbox) => + { + var exception = new InvalidCastException("failed validation"); + var valueObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); + control.Bind(NumericUpDown.ValueProperty, valueObservable); + Dispatcher.UIThread.RunJobs(); + + Assert.True(DataValidationErrors.GetHasErrors(control)); + Assert.True(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception })); + }); + } + + private void RunTest(Action test) + { + using (UnitTestApplication.Start(Services)) + { + var control = CreateControl(); + TextBox textBox = GetTextBox(control); + var window = new Window { Content = control }; + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + Dispatcher.UIThread.RunJobs(); + test.Invoke(control, textBox); + } + } + + private NumericUpDown CreateControl() + { + var control = new NumericUpDown + { + Template = CreateTemplate() + }; + + control.ApplyTemplate(); + return control; + } + private TextBox GetTextBox(NumericUpDown control) + { + return control.GetTemplateChildren() + .OfType() + .Select(b => b.Content) + .OfType() + .First(); + } + private IControlTemplate CreateTemplate() + { + return new FuncControlTemplate((control, scope) => + { + var textBox = + new TextBox + { + Name = "PART_TextBox" + }.RegisterInNameScope(scope); + return new ButtonSpinner + { + Name = "PART_Spinner", + Content = textBox, + }.RegisterInNameScope(scope); + }); + } + } +} From 899ba649130a4327cd2d755e3c6ea76b10ddda60 Mon Sep 17 00:00:00 2001 From: aljosas Date: Fri, 12 Mar 2021 12:15:12 +0100 Subject: [PATCH 09/47] fixing data validation for Combobox control and all SelectingItems control #5652 --- .../Primitives/SelectingItemsControl.cs | 28 +++-- .../CarouselTests.cs | 110 +++++++----------- .../ComboBoxTests.cs | 65 +++++++++++ .../ListBoxTests.cs | 7 ++ 4 files changed, 136 insertions(+), 74 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2cd69793dc..ccdff8c9b7 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives nameof(SelectedItem), o => o.SelectedItem, (o, v) => o.SelectedItem = v, - defaultBindingMode: BindingMode.TwoWay); + defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// /// Defines the property. @@ -466,6 +466,20 @@ namespace Avalonia.Controls.Primitives EndUpdating(); } + /// + /// Called to update the validation state for properties for which data validation is + /// enabled. + /// + /// The property. + /// The new binding value for the property. + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + { + if (property == SelectedItemProperty) + { + DataValidationErrors.SetError(this, value.Error); + } + } + protected override void OnInitialized() { base.OnInitialized(); @@ -503,6 +517,7 @@ namespace Avalonia.Controls.Primitives { AutoScrollToSelectedItemIfNecessary(); } + if (change.Property == ItemsProperty && _updateState is null && _selection is object) { var newValue = change.NewValue.GetValueOrDefault(); @@ -707,7 +722,7 @@ namespace Avalonia.Controls.Primitives _oldSelectedItem = SelectedItem; } else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) && - _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) + _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) { RaisePropertyChanged( SelectedItemsProperty, @@ -853,10 +868,7 @@ namespace Avalonia.Controls.Primitives private ISelectionModel CreateDefaultSelectionModel() { - return new InternalSelectionModel - { - SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), - }; + return new InternalSelectionModel { SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), }; } private void InitializeSelectionModel(ISelectionModel model) @@ -977,7 +989,7 @@ namespace Avalonia.Controls.Primitives public Optional Selection { get; set; } public Optional SelectedItems { get; set; } - public Optional SelectedIndex + public Optional SelectedIndex { get => _selectedIndex; set @@ -996,6 +1008,6 @@ namespace Avalonia.Controls.Primitives _selectedIndex = default; } } - } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 051f6c3fd3..393fde0faf 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -1,10 +1,14 @@ using System.Collections.ObjectModel; using System.Linq; +using System.Reactive.Subjects; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.LogicalTree; +using Avalonia.Threading; using Avalonia.VisualTree; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests @@ -16,12 +20,7 @@ namespace Avalonia.Controls.UnitTests { var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = new[] - { - "Foo", - "Bar" - } + Template = new FuncControlTemplate(CreateTemplate), Items = new[] { "Foo", "Bar" } }; target.ApplyTemplate(); @@ -35,12 +34,7 @@ namespace Avalonia.Controls.UnitTests { var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = new[] - { - "Foo", - "Bar" - } + Template = new FuncControlTemplate(CreateTemplate), Items = new[] { "Foo", "Bar" } }; target.ApplyTemplate(); @@ -75,18 +69,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -111,18 +98,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes_And_Virtualized() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = true, + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = true, }; target.ApplyTemplate(); @@ -150,9 +130,7 @@ namespace Avalonia.Controls.UnitTests var items = new ObservableCollection(); var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -170,18 +148,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Index_Changes_To_None_When_Items_Assigned_Null() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -204,12 +175,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { @@ -233,18 +199,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_Next_First_Item_When_Item_Removed_From_Beggining_Of_List() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -267,18 +226,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -311,5 +263,31 @@ namespace Avalonia.Controls.UnitTests contentPresenter.UpdateChild(); return Assert.IsType(contentPresenter.Child); } + + [Fact] + public void SelectedItem_Validation() + { + using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + { + var target = new Carousel + { + Template = new FuncControlTemplate(CreateTemplate), IsVirtualized = false + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + var exception = new System.InvalidCastException("failed validation"); + var textObservable = + new BehaviorSubject(new BindingNotification(exception, + BindingErrorType.DataValidationError)); + target.Bind(ComboBox.SelectedItemProperty, textObservable); + + Dispatcher.UIThread.RunJobs(); + + Assert.True(DataValidationErrors.GetHasErrors(target)); + Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 4ea838358c..9f90037032 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -1,11 +1,14 @@ using System.Linq; +using System.Reactive.Subjects; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Media; +using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; @@ -173,5 +176,67 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(expectedSelectedIndex, target.SelectedIndex); } } + + [Fact] + public void SelectedItem_Validation() + { + + using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + { + var target = new ComboBox + { + Template = GetTemplate() + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + var exception = new System.InvalidCastException("failed validation"); + var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); + target.Bind(ComboBox.SelectedItemProperty, textObservable); + + Dispatcher.UIThread.RunJobs(); + + Assert.True(DataValidationErrors.GetHasErrors(target)); + Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); + + } + + } + private ComboBox CreateControl() + { + var control = new ComboBox() + { + Template = GetTemplate() + }; + + control.ApplyTemplate(); + return control; + } + + private TextBox GetTextBox(ComboBox control) + { + return control.GetTemplateChildren() + // .OfType() + // .Select(b => b.Content) + .OfType() + .First(); + } + // private IControlTemplate CreateTemplate() + // { + // return new FuncControlTemplate((control, scope) => + // { + // var textBox = + // new TextBox + // { + // Name = "PART_TextBox" + // }.RegisterInNameScope(scope); + // return new ButtonSpinner + // { + // Name = "PART_Spinner", + // Content = textBox, + // }.RegisterInNameScope(scope); + // }); + // } } } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 145fce4fed..d13f5b704d 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -559,5 +559,12 @@ namespace Avalonia.Controls.UnitTests public string Value { get; } } + + + [Fact] + public void SelectedItem_Validation() + { + + } } } From ede1e2db94b250af9364f3eb197738335c577d9e Mon Sep 17 00:00:00 2001 From: aljosas Date: Mon, 15 Mar 2021 13:01:42 +0100 Subject: [PATCH 10/47] fixing PR for data validation for Combobox control and all SelectingItemsControl --- .../Primitives/SelectingItemsControl.cs | 6 +- .../CarouselTests.cs | 80 +++++++++++++++---- .../ComboBoxTests.cs | 37 +-------- .../ListBoxTests.cs | 19 +++++ 4 files changed, 90 insertions(+), 52 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index ccdff8c9b7..19824a71f0 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -517,7 +517,6 @@ namespace Avalonia.Controls.Primitives { AutoScrollToSelectedItemIfNecessary(); } - if (change.Property == ItemsProperty && _updateState is null && _selection is object) { var newValue = change.NewValue.GetValueOrDefault(); @@ -868,7 +867,10 @@ namespace Avalonia.Controls.Primitives private ISelectionModel CreateDefaultSelectionModel() { - return new InternalSelectionModel { SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), }; + return new InternalSelectionModel + { + SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), + }; } private void InitializeSelectionModel(ISelectionModel model) diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 393fde0faf..ea39093602 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -20,7 +20,12 @@ namespace Avalonia.Controls.UnitTests { var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = new[] { "Foo", "Bar" } + Template = new FuncControlTemplate(CreateTemplate), + Items = new[] + { + "Foo", + "Bar" + } }; target.ApplyTemplate(); @@ -34,7 +39,12 @@ namespace Avalonia.Controls.UnitTests { var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = new[] { "Foo", "Bar" } + Template = new FuncControlTemplate(CreateTemplate), + Items = new[] + { + "Foo", + "Bar" + } }; target.ApplyTemplate(); @@ -69,11 +79,18 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes() { - var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = false }; target.ApplyTemplate(); @@ -98,11 +115,18 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes_And_Virtualized() { - var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = true, + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = true, }; target.ApplyTemplate(); @@ -130,7 +154,9 @@ namespace Avalonia.Controls.UnitTests var items = new ObservableCollection(); var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = false }; target.ApplyTemplate(); @@ -148,11 +174,18 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Index_Changes_To_None_When_Items_Assigned_Null() { - var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = false }; target.ApplyTemplate(); @@ -175,7 +208,12 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex() { - var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; var target = new Carousel { @@ -199,11 +237,18 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_Next_First_Item_When_Item_Removed_From_Beggining_Of_List() { - var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = false }; target.ApplyTemplate(); @@ -226,11 +271,18 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle() { - var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = false }; target.ApplyTemplate(); diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 9f90037032..a06211c040 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -202,41 +202,6 @@ namespace Avalonia.Controls.UnitTests } - } - private ComboBox CreateControl() - { - var control = new ComboBox() - { - Template = GetTemplate() - }; - - control.ApplyTemplate(); - return control; - } - - private TextBox GetTextBox(ComboBox control) - { - return control.GetTemplateChildren() - // .OfType() - // .Select(b => b.Content) - .OfType() - .First(); - } - // private IControlTemplate CreateTemplate() - // { - // return new FuncControlTemplate((control, scope) => - // { - // var textBox = - // new TextBox - // { - // Name = "PART_TextBox" - // }.RegisterInNameScope(scope); - // return new ButtonSpinner - // { - // Name = "PART_Spinner", - // Content = textBox, - // }.RegisterInNameScope(scope); - // }); - // } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index d13f5b704d..94ada5520e 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -1,11 +1,14 @@ using System.Linq; +using System.Reactive.Subjects; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Styling; +using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -564,7 +567,23 @@ namespace Avalonia.Controls.UnitTests [Fact] public void SelectedItem_Validation() { + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = new[] { "Foo" }, + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), + }; + + Prepare(target); + var exception = new System.InvalidCastException("failed validation"); + var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); + target.Bind(ComboBox.SelectedItemProperty, textObservable); + + Dispatcher.UIThread.RunJobs(); + + Assert.True(DataValidationErrors.GetHasErrors(target)); + Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); } } } From 9899fe5e81849363c13ba7f0707830369561d50b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 16 Mar 2021 09:51:52 +0100 Subject: [PATCH 11/47] Clarify naming. --- src/Avalonia.Controls/TextBox.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 44d1a1c489..1d75f08a41 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -528,11 +528,11 @@ namespace Avalonia.Controls string text = Text ?? string.Empty; int caretIndex = CaretIndex; - int estimatedLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); + int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); - if (MaxLength > 0 && estimatedLength > MaxLength) + if (MaxLength > 0 && newLength > MaxLength) { - input = input.Remove(Math.Max(0, input.Length - (estimatedLength - MaxLength))); + input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength))); } if (!string.IsNullOrEmpty(input)) From 4ae089bef88c03a4c6c4a686fbbab23ea229ab69 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 16 Mar 2021 18:51:58 +0800 Subject: [PATCH 12/47] Update XamlIL --- src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index ea80a607c5..9e90d34e97 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit ea80a607c5e9d8f000160dbbb48c27ed4cfafbc9 +Subproject commit 9e90d34e97c766ba8dcb70128147fcded65d195a From 89e5f3888d6cbe7778fe56013c738fc82a7d765f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 17 Mar 2021 12:26:44 +0100 Subject: [PATCH 13/47] Added Opening/Closed for NativeMenu on OSX. Adds the `Opening` and `Closed` events for OSX to `NativeMenu` which relate to `menuWillOpen` and `menuDidClose`. Note that `NativeMenu` already exposed `NeedsUpdate` as `Opening`; this event has been moved to a separate `NeedsUpdate` event. --- native/Avalonia.Native/src/OSX/menu.h | 5 +-- native/Avalonia.Native/src/OSX/menu.mm | 26 +++++++++++++++ .../INativeMenuExporterEventsImplBridge.cs | 2 ++ src/Avalonia.Controls/NativeMenu.cs | 33 ++++++++++++++++++- src/Avalonia.Native/IAvnMenu.cs | 22 +++++++++++++ src/Avalonia.Native/avn.idl | 5 ++- 6 files changed, 87 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 564fdc132b..ea68b354bc 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -60,7 +60,6 @@ public: void RaiseOnClicked(); }; - class AvnAppMenu : public ComSingleObject { private: @@ -71,10 +70,12 @@ public: FORWARD_IUNKNOWN() AvnAppMenu(IAvnMenuEvents* events); - + AvnMenu* GetNative(); void RaiseNeedsUpdate (); + void RaiseOpening(); + void RaiseClosed(); virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index ea5cca9ce8..c33a447fcc 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -298,6 +298,23 @@ void AvnAppMenu::RaiseNeedsUpdate() } } +void AvnAppMenu::RaiseOpening() +{ + if(_baseEvents != nullptr) + { + _baseEvents->Opening(); + } +} + +void AvnAppMenu::RaiseClosed() +{ + if(_baseEvents != nullptr) + { + _baseEvents->Closed(); + } +} + + HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { @autoreleasepool @@ -382,6 +399,15 @@ HRESULT AvnAppMenu::Clear() _parent->RaiseNeedsUpdate(); } +- (void)menuWillOpen:(NSMenu *)menu +{ + _parent->RaiseOpening(); +} + +- (void)menuDidClose:(NSMenu *)menu +{ + _parent->RaiseClosed(); +} @end diff --git a/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs b/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs index 672d5c1a13..f492e6ca0f 100644 --- a/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs +++ b/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs @@ -3,5 +3,7 @@ namespace Avalonia.Controls public interface INativeMenuExporterEventsImplBridge { void RaiseNeedsUpdate (); + void RaiseOpening(); + void RaiseClosed(); } } diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 38a9f03d29..58ee99722f 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -12,13 +12,34 @@ namespace Avalonia.Controls private readonly AvaloniaList _items = new AvaloniaList { ResetBehavior = ResetBehavior.Remove }; private NativeMenuItem _parent; + [Content] public IList Items => _items; /// - /// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically. + /// Raised when the menu requests an update. + /// + /// + /// Use this event to add, remove or modify menu items before a menu is + /// shown or a hotkey is pressed. + /// + public event EventHandler NeedsUpdate; + + /// + /// Raised before the menu is opened. /// + /// + /// Do not update the menu in this event; use . + /// public event EventHandler Opening; + + /// + /// Raised after the menu is closed. + /// + /// + /// Do not update the menu in this event; use . + /// + public event EventHandler Closed; public NativeMenu() { @@ -27,10 +48,20 @@ namespace Avalonia.Controls } void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate() + { + NeedsUpdate?.Invoke(this, EventArgs.Empty); + } + + void INativeMenuExporterEventsImplBridge.RaiseOpening() { Opening?.Invoke(this, EventArgs.Empty); } + void INativeMenuExporterEventsImplBridge.RaiseClosed() + { + Closed?.Invoke(this, EventArgs.Empty); + } + private void Validator(NativeMenuItemBase obj) { if (obj.Parent != null) diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index dd9464284f..cba59c481d 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -20,11 +20,23 @@ namespace Avalonia.Native.Interop { _parent?.RaiseNeedsUpdate(); } + + public void Opening() + { + _parent?.RaiseOpening(); + } + + public void Closed() + { + _parent?.RaiseClosed(); + } } partial interface IAvnMenu { void RaiseNeedsUpdate(); + void RaiseOpening(); + void RaiseClosed(); void Deinitialise(); } } @@ -45,6 +57,16 @@ namespace Avalonia.Native.Interop.Impl _exporter.UpdateIfNeeded(); } + public void RaiseOpening() + { + (ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseOpening(); + } + + public void RaiseClosed() + { + (ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseClosed(); + } + internal NativeMenu ManagedMenu { get; private set; } public static __MicroComIAvnMenuProxy Create(IAvaloniaNativeFactory factory) diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 2693f5f139..af39dd8c2f 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -685,10 +685,9 @@ interface IAvnMenuItem : IUnknown [uuid(0af7df53-7632-42f4-a650-0992c361b477)] interface IAvnMenuEvents : IUnknown { - /** - * NeedsUpdate - */ void NeedsUpdate(); + void Opening(); + void Closed(); } [uuid(5142bb41-66ab-49e7-bb37-cd079c000f27)] From 0b16629e9bc05af7e4da7ff47d4dba85bfd0ba92 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 17 Mar 2021 14:53:16 +0100 Subject: [PATCH 14/47] Update apicompat baseline. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index aa8db78087..0284463f1c 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -1,7 +1,9 @@ Compat issues with assembly Avalonia.Controls: +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -Total Issues: 5 +Total Issues: 7 From c686b86e38bb0cf498ea53a77887af68ac49e9cb Mon Sep 17 00:00:00 2001 From: Nathan Randle Date: Wed, 17 Mar 2021 23:12:15 +0000 Subject: [PATCH 15/47] Rename misspellings of separator. --- native/Avalonia.Native/src/OSX/common.h | 2 +- native/Avalonia.Native/src/OSX/main.mm | 4 ++-- native/Avalonia.Native/src/OSX/menu.h | 4 ++-- native/Avalonia.Native/src/OSX/menu.mm | 8 ++++---- samples/ControlCatalog/MainWindow.xaml | 4 ++-- src/Avalonia.Controls/NativeMenuItemSeparator.cs | 16 ++++++++++++++++ src/Avalonia.Controls/NativeMenuItemSeperator.cs | 10 ---------- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 2 +- src/Avalonia.Native/IAvnMenu.cs | 4 ++-- src/Avalonia.Native/avn.idl | 2 +- 10 files changed, 31 insertions(+), 25 deletions(-) create mode 100644 src/Avalonia.Controls/NativeMenuItemSeparator.cs delete mode 100644 src/Avalonia.Controls/NativeMenuItemSeperator.cs diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 0f7215f37c..c082003ccf 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -23,7 +23,7 @@ extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); extern IAvnMenuItem* CreateAppMenuItem(); -extern IAvnMenuItem* CreateAppMenuItemSeperator(); +extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); extern IAvnMenu* GetAppMenu (); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 11742e3b5c..aaaf381b26 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -253,9 +253,9 @@ public: return S_OK; } - virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) override + virtual HRESULT CreateMenuItemSeparator (IAvnMenuItem** ppv) override { - *ppv = ::CreateAppMenuItemSeperator(); + *ppv = ::CreateAppMenuItemSeparator(); return S_OK; } diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 564fdc132b..4a9348d17b 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -31,13 +31,13 @@ private: NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem IAvnActionCallback* _callback; IAvnPredicateCallback* _predicate; - bool _isSeperator; + bool _isSeparator; bool _isCheckable; public: FORWARD_IUNKNOWN() - AvnAppMenuItem(bool isSeperator); + AvnAppMenuItem(bool isSeparator); NSMenuItem* GetNative(); diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index ea5cca9ce8..02b6365a8c 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -71,12 +71,12 @@ } @end -AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) +AvnAppMenuItem::AvnAppMenuItem(bool isSeparator) { _isCheckable = false; - _isSeperator = isSeperator; + _isSeparator = isSeparator; - if(isSeperator) + if(isSeparator) { _native = [NSMenuItem separatorItem]; } @@ -401,7 +401,7 @@ extern IAvnMenuItem* CreateAppMenuItem() } } -extern IAvnMenuItem* CreateAppMenuItemSeperator() +extern IAvnMenuItem* CreateAppMenuItemSeparator() { @autoreleasepool { diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 6a70bb082f..a107ee2163 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -18,11 +18,11 @@ - + - + diff --git a/src/Avalonia.Controls/NativeMenuItemSeparator.cs b/src/Avalonia.Controls/NativeMenuItemSeparator.cs new file mode 100644 index 0000000000..d3d3721c89 --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuItemSeparator.cs @@ -0,0 +1,16 @@ +using System; + +namespace Avalonia.Controls +{ + + [Obsolete("This class exists to maintain backwards compatiblity with existing code. Use NativeMenuItemSeparator instead")] + public class NativeMenuItemSeperator : NativeMenuItemSeparator + { + } + + public class NativeMenuItemSeparator : NativeMenuItemBase + { + [Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)] + public string Header => "-"; + } +} diff --git a/src/Avalonia.Controls/NativeMenuItemSeperator.cs b/src/Avalonia.Controls/NativeMenuItemSeperator.cs deleted file mode 100644 index e743483dab..0000000000 --- a/src/Avalonia.Controls/NativeMenuItemSeperator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Avalonia.Controls -{ - public class NativeMenuItemSeperator : NativeMenuItemBase - { - [Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)] - public string Header => "-"; - } -} diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 2cf533195e..9e635e01f1 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -192,7 +192,7 @@ namespace Avalonia.FreeDesktop { var (it, menu) = i; - if (it is NativeMenuItemSeperator) + if (it is NativeMenuItemSeparator) { if (name == "type") return "separator"; diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index dd9464284f..d08388c49e 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -103,8 +103,8 @@ namespace Avalonia.Native.Interop.Impl private __MicroComIAvnMenuItemProxy CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) { - var nativeItem = (__MicroComIAvnMenuItemProxy)(item is NativeMenuItemSeperator ? - factory.CreateMenuItemSeperator() : + var nativeItem = (__MicroComIAvnMenuItemProxy)(item is NativeMenuItemSeparator ? + factory.CreateMenuItemSeparator() : factory.CreateMenuItem()); nativeItem.ManagedMenuItem = item; diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 2693f5f139..3f485dd375 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -417,7 +417,7 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT SetAppMenu(IAvnMenu* menu); HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv); HRESULT CreateMenuItem(IAvnMenuItem** ppv); - HRESULT CreateMenuItemSeperator(IAvnMenuItem** ppv); + HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] From a15e2597d2ac3e75b71d2c346193d20cf6b4b78c Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 18 Mar 2021 09:39:55 +0800 Subject: [PATCH 16/47] remove mono check on nuke --- nukebuild/Build.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 8e331edab4..d627a2bf19 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -89,10 +89,6 @@ partial class Build : NukeBuild Process.Start(new ProcessStartInfo(command, args) {UseShellExecute = false}).WaitForExit(); } ExecWait("dotnet version:", "dotnet", "--version"); - if (Parameters.IsRunningOnUnix) - ExecWait("Mono version:", "mono", "--version"); - - } IReadOnlyCollection MsBuildCommon( From 0a41ba08fd318269763385016339f69e65ba19f4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 19 Mar 2021 12:55:58 +0100 Subject: [PATCH 17/47] Added failing batch update test. --- .../AvaloniaObjectTests_BatchUpdate.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs index 9f0e52c8d9..050fefbd53 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs @@ -449,6 +449,29 @@ namespace Avalonia.Base.UnitTests Assert.Null(raised[1].NewValue); } + [Fact] + public void Can_Set_Cleared_Value_When_Ending_Batch_Update() + { + var target = new TestClass(); + var raised = 0; + + target.Foo = "foo"; + + target.BeginBatchUpdate(); + target.ClearValue(TestClass.FooProperty); + target.PropertyChanged += (sender, e) => + { + if (e.Property == TestClass.FooProperty && e.NewValue is null) + { + target.Foo = "bar"; + ++raised; + } + }; + target.EndBatchUpdate(); + + Assert.Equal(1, raised); + } + public class TestClass : AvaloniaObject { public static readonly StyledProperty FooProperty = From f1298fb1bd5d8e91161e0e8dc5d465749470c40b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 19 Mar 2021 13:28:35 +0100 Subject: [PATCH 18/47] Don't write new values to remove sentinels. During a batch update sentinel values are written to the value store to indicate that the value needs to be removed at the end of the update. If a new value is written to the store in the course of ending the batch update, don't update this sentinel value as the value will subsequently be lost. --- src/Avalonia.Base/ValueStore.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index e32b20cc96..470be35592 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -103,7 +103,7 @@ namespace Avalonia IDisposable? result = null; - if (_values.TryGetValue(property, out var slot)) + if (_values.TryGetValue(property, out var slot) && !IsRemoveSentinel(slot)) { result = SetExisting(slot, property, value, priority); } @@ -364,6 +364,14 @@ namespace Avalonia } } + private static bool IsRemoveSentinel(IValue value) + { + // Local value entries are optimized and contain only a single value field to save space, + // so there's no way to mark them for removal at the end of a batch update. Instead a + // ConstantValueEntry with a priority of Unset is used as a sentinel value. + return value is ConstantValueEntry t && t.Priority == BindingPriority.Unset; + } + private class BatchUpdate { private ValueStore _owner; From 03cf2c6f9f79294c3c07a82fa9a59b7e772546f2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 19 Mar 2021 13:30:37 +0100 Subject: [PATCH 19/47] Added another failing batch update test. And a bit of a sanity check to the previous one. --- .../AvaloniaObjectTests_BatchUpdate.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs index 050fefbd53..5bf3afc9e7 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs @@ -469,6 +469,31 @@ namespace Avalonia.Base.UnitTests }; target.EndBatchUpdate(); + Assert.Equal("bar", target.Foo); + Assert.Equal(1, raised); + } + + [Fact] + public void Can_Bind_Cleared_Value_When_Ending_Batch_Update() + { + var target = new TestClass(); + var raised = 0; + + target.Foo = "foo"; + + target.BeginBatchUpdate(); + target.ClearValue(TestClass.FooProperty); + target.PropertyChanged += (sender, e) => + { + if (e.Property == TestClass.FooProperty && e.NewValue is null) + { + target.Bind(TestClass.FooProperty, new TestObservable("bar")); + ++raised; + } + }; + target.EndBatchUpdate(); + + Assert.Equal("bar", target.Foo); Assert.Equal(1, raised); } From 3afa95253f0c517e85af08bcc6d1f3ea074e9ff3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 19 Mar 2021 13:38:29 +0100 Subject: [PATCH 20/47] Allow binding property during ending batch update. - Allow adding a binding to a cleared property while ending a batch update. Need to check that the existing value isn't a remove sentinel here, otherwise the binding will be lost. - When a binding is added during the end of a batch update, `_batchUpdate` will be non-null but newly added bindings shouldn't have `BeginBatchUpdate` called on them because no `EndBatchUpdate` will arrive (as we've already called them) - Add sanity checks to the unit test to make sure that correct notifications are raised --- src/Avalonia.Base/ValueStore.cs | 6 ++++-- .../AvaloniaObjectTests_BatchUpdate.cs | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 470be35592..9ece2b8042 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -138,7 +138,7 @@ namespace Avalonia IObservable> source, BindingPriority priority) { - if (_values.TryGetValue(property, out var slot)) + if (_values.TryGetValue(property, out var slot) && !IsRemoveSentinel(slot)) { return BindExisting(slot, property, source, priority); } @@ -338,7 +338,7 @@ namespace Avalonia private void AddValue(AvaloniaProperty property, IValue value) { _values.AddValue(property, value); - if (_batchUpdate is object && value is IBatchUpdate batch) + if (_batchUpdate?.IsBatchUpdating == true && value is IBatchUpdate batch) batch.BeginBatchUpdate(); value.Start(); } @@ -381,6 +381,8 @@ namespace Avalonia public BatchUpdate(ValueStore owner) => _owner = owner; + public bool IsBatchUpdating => _batchUpdateCount > 0; + public void Begin() { if (_batchUpdateCount++ == 0) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs index 5bf3afc9e7..036f275a71 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs @@ -478,6 +478,7 @@ namespace Avalonia.Base.UnitTests { var target = new TestClass(); var raised = 0; + var notifications = new List(); target.Foo = "foo"; @@ -490,11 +491,16 @@ namespace Avalonia.Base.UnitTests target.Bind(TestClass.FooProperty, new TestObservable("bar")); ++raised; } + + notifications.Add(e); }; target.EndBatchUpdate(); Assert.Equal("bar", target.Foo); Assert.Equal(1, raised); + Assert.Equal(2, notifications.Count); + Assert.Equal(null, notifications[0].NewValue); + Assert.Equal("bar", notifications[1].NewValue); } public class TestClass : AvaloniaObject From d5690cc83435f17afaceb95073f38e59560b2b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Fri, 19 Mar 2021 19:59:41 +0000 Subject: [PATCH 21/47] Upgraded target Android SDK to 11.0 (API level 30). --- samples/ControlCatalog.Android/ControlCatalog.Android.csproj | 2 +- samples/ControlCatalog.Android/Properties/AndroidManifest.xml | 2 +- src/Android/Avalonia.Android/Avalonia.Android.csproj | 2 +- .../Avalonia.AndroidTestApplication.csproj | 4 ++-- .../Properties/AndroidManifest.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 20ca0576d4..1a68c4d732 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v10.0 + v11.0 Properties\AndroidManifest.xml diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml index 02e97f3065..9effda7e79 100644 --- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml +++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index c170e8449c..8c6775733f 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,6 +1,6 @@  - monoandroid90 + monoandroid11.0 true diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index 4f49f3a863..9104f1618c 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v10.0 + v11.0 Properties\AndroidManifest.xml @@ -150,4 +150,4 @@ - \ No newline at end of file + diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml index e8e81da9de..57ee503005 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml +++ b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml @@ -1,6 +1,6 @@  - + \ No newline at end of file From c530956a0b003cf0bafe89419c83cc5e94a933cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Fri, 19 Mar 2021 22:17:27 +0000 Subject: [PATCH 22/47] Avalonia.Android code cleanup. --- .../Avalonia.Android/AndroidPlatform.cs | 22 +------------------ .../AndroidThreadingInterface.cs | 15 +++++++------ .../InvalidationAwareSurfaceView.cs | 2 ++ src/Android/Avalonia.Android/app.config | 11 ---------- 4 files changed, 11 insertions(+), 39 deletions(-) delete mode 100644 src/Android/Avalonia.Android/app.config diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index e0ed9b1fda..5e11d8eab2 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -29,21 +29,12 @@ namespace Avalonia namespace Avalonia.Android { - class AndroidPlatform : IPlatformSettings, IWindowingPlatform + class AndroidPlatform : IPlatformSettings { public static readonly AndroidPlatform Instance = new AndroidPlatform(); public static AndroidPlatformOptions Options { get; private set; } public Size DoubleClickSize => new Size(4, 4); public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200); - public double RenderScalingFactor => _scalingFactor; - public double LayoutScalingFactor => _scalingFactor; - - private readonly double _scalingFactor = 1; - - public AndroidPlatform() - { - _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity; - } public static void Initialize(Type appType, AndroidPlatformOptions options) { @@ -56,7 +47,6 @@ namespace Avalonia.Android .Bind().ToConstant(Instance) .Bind().ToConstant(new AndroidThreadingInterface()) .Bind().ToTransient() - .Bind().ToConstant(Instance) .Bind().ToSingleton() .Bind().ToConstant(new ChoreographerTimer()) .Bind().ToConstant(new RenderLoop()) @@ -70,16 +60,6 @@ namespace Avalonia.Android EglPlatformOpenGlInterface.TryInitialize(); } } - - public IWindowImpl CreateWindow() - { - throw new NotSupportedException(); - } - - public IWindowImpl CreateEmbeddableWindow() - { - throw new NotSupportedException(); - } } public sealed class AndroidPlatformOptions diff --git a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs index 6fe77adca1..e72f0aed90 100644 --- a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs +++ b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs @@ -1,25 +1,26 @@ using System; using System.Reactive.Disposables; using System.Threading; + using Android.OS; + using Avalonia.Platform; using Avalonia.Threading; +using App = Android.App.Application; + namespace Avalonia.Android { - class AndroidThreadingInterface : IPlatformThreadingInterface + internal sealed class AndroidThreadingInterface : IPlatformThreadingInterface { private Handler _handler; public AndroidThreadingInterface() { - _handler = new Handler(global::Android.App.Application.Context.MainLooper); + _handler = new Handler(App.Context.MainLooper); } - public void RunLoop(CancellationToken cancellationToken) - { - return; - } + public void RunLoop(CancellationToken cancellationToken) => throw new NotSupportedException(); public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { @@ -57,7 +58,7 @@ namespace Avalonia.Android }); } }, null, TimeSpan.Zero, interval); - + return Disposable.Create(() => { lock (l) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 02ea702236..16c5bdae3d 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -43,11 +43,13 @@ namespace Avalonia.Android } } + [Obsolete("deprecated")] public override void Invalidate(global::Android.Graphics.Rect dirty) { Invalidate(); } + [Obsolete("deprecated")] public override void Invalidate(int l, int t, int r, int b) { Invalidate(); diff --git a/src/Android/Avalonia.Android/app.config b/src/Android/Avalonia.Android/app.config deleted file mode 100644 index fc064cedfb..0000000000 --- a/src/Android/Avalonia.Android/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file From 7fb6e14716712ff2168708b909b017bd8c7b0096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 20 Mar 2021 16:26:19 +0000 Subject: [PATCH 23/47] Use TouchDevice on Android. --- .../Avalonia.Android/AvaloniaActivity.cs | 8 +- .../Platform/Input/AndroidMouseDevice.cs | 14 --- .../Platform/SkiaPlatform/TopLevelImpl.cs | 10 +- .../Helpers/AndroidTouchEventsHelper.cs | 91 +++++-------------- 4 files changed, 28 insertions(+), 95 deletions(-) delete mode 100644 src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs index 52b68f8e2f..b3a7585520 100644 --- a/src/Android/Avalonia.Android/AvaloniaActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs @@ -1,4 +1,3 @@ - using Android.App; using Android.OS; using Android.Views; @@ -7,7 +6,6 @@ namespace Avalonia.Android { public abstract class AvaloniaActivity : Activity { - internal AvaloniaView View; object _content; @@ -35,9 +33,7 @@ namespace Avalonia.Android } } - public override bool DispatchKeyEvent(KeyEvent e) - { - return View.DispatchKeyEvent(e); - } + public override bool DispatchKeyEvent(KeyEvent e) => + View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e); } } diff --git a/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs b/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs deleted file mode 100644 index d52eeb15e4..0000000000 --- a/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Avalonia.Input; - -namespace Avalonia.Android.Platform.Input -{ - public class AndroidMouseDevice : MouseDevice - { - public static AndroidMouseDevice Instance { get; } = new AndroidMouseDevice(); - - public AndroidMouseDevice() - { - - } - } -} \ No newline at end of file diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 10bf414f25..4fd9bc040b 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -6,7 +6,6 @@ using Android.Runtime; using Android.Views; using Avalonia.Android.OpenGL; -using Avalonia.Android.Platform.Input; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Controls; @@ -35,7 +34,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _view = new ViewImpl(context, this, placeOnTop); _keyboardHelper = new AndroidKeyboardEventsHelper(this); _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, - p => GetAvaloniaPointFromEvent(p)); + GetAvaloniaPointFromEvent); _gl = GlPlatformSurface.TryCreate(this); _framebuffer = new FramebufferManager(this); @@ -44,8 +43,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform _view.Resources.DisplayMetrics.HeightPixels); } - - private bool _handleEvents; public bool HandleEvents @@ -58,7 +55,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform } } - public virtual Point GetAvaloniaPointFromEvent(MotionEvent e) => new Point(e.GetX(), e.GetY()); + public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => + new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling; public IInputRoot InputRoot { get; private set; } @@ -76,7 +74,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform } } - public IMouseDevice MouseDevice => AndroidMouseDevice.Instance; + public IMouseDevice MouseDevice { get; } = new MouseDevice(); public Action Closed { get; set; } diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs index 0bfbb1c2df..6142598514 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs @@ -11,7 +11,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers private TView _view; public bool HandleEvents { get; set; } - public AndroidTouchEventsHelper(TView view, Func getInputRoot, Func getPointfunc) + public AndroidTouchEventsHelper(TView view, Func getInputRoot, Func getPointfunc) { this._view = view; HandleEvents = true; @@ -19,11 +19,9 @@ namespace Avalonia.Android.Platform.Specific.Helpers _getInputRoot = getInputRoot; } - private DateTime _lastTouchMoveEventTime = DateTime.Now; - private Point? _lastTouchMovePoint; - private Func _getPointFunc; + private TouchDevice _touchDevice = new TouchDevice(); + private Func _getPointFunc; private Func _getInputRoot; - private Point _point; public bool? DispatchTouchEvent(MotionEvent e, out bool callBase) { @@ -33,89 +31,44 @@ namespace Avalonia.Android.Platform.Specific.Helpers return null; } - RawPointerEventType? mouseEventType = null; var eventTime = DateTime.Now; + //Basic touch support - switch (e.Action) + var pointerEventType = e.Action switch { - case MotionEventActions.Move: - //may be bot flood the evnt system with too many event especially on not so powerfull mobile devices - if ((eventTime - _lastTouchMoveEventTime).TotalMilliseconds > 10) - { - mouseEventType = RawPointerEventType.Move; - } - break; - - case MotionEventActions.Down: - mouseEventType = RawPointerEventType.LeftButtonDown; + MotionEventActions.Down => RawPointerEventType.TouchBegin, + MotionEventActions.Up => RawPointerEventType.TouchEnd, + MotionEventActions.Cancel => RawPointerEventType.TouchCancel, + _ => RawPointerEventType.TouchUpdate + }; - break; + if (e.Action.HasFlag(MotionEventActions.PointerDown)) + { + pointerEventType = RawPointerEventType.TouchBegin; + } - case MotionEventActions.Up: - mouseEventType = RawPointerEventType.LeftButtonUp; - break; + if (e.Action.HasFlag(MotionEventActions.PointerUp)) + { + pointerEventType = RawPointerEventType.TouchEnd; } - if (mouseEventType != null) + for (int i = 0; i < e.PointerCount; i++) { //if point is in view otherwise it's possible avalonia not to find the proper window to dispatch the event - _point = _getPointFunc(e); + var point = _getPointFunc(e, i); double x = _view.View.GetX(); double y = _view.View.GetY(); double r = x + _view.View.Width; double b = y + _view.View.Height; - if (x <= _point.X && r >= _point.X && y <= _point.Y && b >= _point.Y) + if (x <= point.X && r >= point.X && y <= point.Y && b >= point.Y) { var inputRoot = _getInputRoot(); - var mouseDevice = Avalonia.Android.Platform.Input.AndroidMouseDevice.Instance; - - //in order the controls to work in a predictable way - //we need to generate mouse move before first mouse down event - //as this is the way buttons are working every time - //otherwise there is a problem sometimes - if (mouseEventType == RawPointerEventType.LeftButtonDown) - { - var me = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot, - RawPointerEventType.Move, _point, RawInputModifiers.None); - _view.Input(me); - } - var mouseEvent = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot, - mouseEventType.Value, _point, RawInputModifiers.LeftMouseButton); + var mouseEvent = new RawTouchEventArgs(_touchDevice, (uint)eventTime.Ticks, inputRoot, + i == e.ActionIndex ? pointerEventType : RawPointerEventType.TouchUpdate, point, RawInputModifiers.None, e.GetPointerId(i)); _view.Input(mouseEvent); - - if (e.Action == MotionEventActions.Move && mouseDevice.Captured == null) - { - if (_lastTouchMovePoint != null) - { - //raise mouse scroll event so the scrollers - //are moving with the cursor - double vectorX = _point.X - _lastTouchMovePoint.Value.X; - double vectorY = _point.Y - _lastTouchMovePoint.Value.Y; - //based on test correction of 0.02 is working perfect - double correction = 0.02; - var ps = AndroidPlatform.Instance.LayoutScalingFactor; - var mouseWheelEvent = new RawMouseWheelEventArgs( - mouseDevice, - (uint)eventTime.Ticks, - inputRoot, - _point, - new Vector(vectorX * correction / ps, vectorY * correction / ps), RawInputModifiers.LeftMouseButton); - _view.Input(mouseWheelEvent); - } - _lastTouchMovePoint = _point; - _lastTouchMoveEventTime = eventTime; - } - else if (e.Action == MotionEventActions.Down) - { - _lastTouchMovePoint = _point; - } - else - { - _lastTouchMovePoint = null; - } } } From 0257bdf6eddfa63b770c644aab41dc7ed18670c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sun, 21 Mar 2021 01:37:35 +0000 Subject: [PATCH 24/47] Implemented render scaling on Android. --- .../SkiaPlatform/AndroidFramebuffer.cs | 6 ++-- .../SkiaPlatform/FramebufferManager.cs | 2 +- .../Platform/SkiaPlatform/TopLevelImpl.cs | 28 ++++++------------- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs index 2afa4e83f1..b115917622 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs @@ -10,7 +10,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform { private IntPtr _window; - public AndroidFramebuffer(Surface surface) + public AndroidFramebuffer(Surface surface, double scaling) { if(surface == null) throw new ArgumentNullException(nameof(surface)); @@ -31,6 +31,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4); Address = buffer.bits; + + Dpi = scaling * new Vector(96, 96); } public void Dispose() @@ -44,7 +46,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IntPtr Address { get; set; } public PixelSize Size { get; } public int RowBytes { get; } - public Vector Dpi { get; } = new Vector(96, 96); + public Vector Dpi { get; } public PixelFormat Format { get; } [DllImport("android")] diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs index 18c4796fae..56a4eb22d4 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs @@ -12,6 +12,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform _topLevel = topLevel; } - public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface); + public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface, _topLevel.RenderScaling); } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 4fd9bc040b..fe237a1719 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -39,8 +39,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform _gl = GlPlatformSurface.TryCreate(this); _framebuffer = new FramebufferManager(this); - MaxClientSize = new Size(_view.Resources.DisplayMetrics.WidthPixels, - _view.Resources.DisplayMetrics.HeightPixels); + RenderScaling = (int)_view.Resources.DisplayMetrics.Density; + + MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, + _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); } private bool _handleEvents; @@ -60,19 +62,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IInputRoot InputRoot { get; private set; } - public virtual Size ClientSize - { - get - { - if (_view == null) - return new Size(0, 0); - return new Size(_view.Width, _view.Height); - } - set - { - - } - } + public virtual Size ClientSize => Size.ToSize(RenderScaling); public IMouseDevice MouseDevice { get; } = new MouseDevice(); @@ -113,12 +103,12 @@ namespace Avalonia.Android.Platform.SkiaPlatform public Point PointToClient(PixelPoint point) { - return point.ToPoint(1); + return point.ToPoint(RenderScaling); } public PixelPoint PointToScreen(Point point) { - return PixelPoint.FromPoint(point, 1); + return PixelPoint.FromPoint(point, RenderScaling); } public void SetCursor(ICursorImpl cursor) @@ -136,7 +126,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _view.Visibility = ViewStates.Visible; } - public double RenderScaling => 1; + public double RenderScaling { get; } void Draw() { @@ -191,7 +181,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) { - var newSize = new Size(width, height); + var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling); if (newSize != _oldSize) { From 4ba8c4ebb76fc45e96a9bdb506edf6608590a4ad Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sun, 21 Mar 2021 19:03:13 +0700 Subject: [PATCH 25/47] Fix #5699: filter BuildAvaloniaApp by signature --- src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 764fc7b332..be2405efde 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -169,7 +169,7 @@ namespace Avalonia.DesignerSupport.Remote if (entryPoint == null) throw Die($"Assembly {args.AppPath} doesn't have an entry point"); var builderMethod = entryPoint.DeclaringType.GetMethod(BuilderMethodName, - BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Array.Empty(), null); if (builderMethod == null) throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}"); Design.IsDesignMode = true; From 0527ae0b64ee3ba26464ed3dd04db1d74ab0507b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 22 Mar 2021 10:19:53 +0000 Subject: [PATCH 26/47] add null pointer check. --- native/Avalonia.Native/src/OSX/window.mm | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 4d63c486c4..b46655c7df 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -2231,9 +2231,12 @@ protected: { @autoreleasepool { - [Window setContentSize:NSSize{x, y}]; + if (Window != nullptr) + { + [Window setContentSize:NSSize{x, y}]; - [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; + [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; + } return S_OK; } From 1c61a5c1c582d2970ba22c4a22a8199b6e662089 Mon Sep 17 00:00:00 2001 From: aljosas Date: Tue, 23 Mar 2021 09:53:42 +0100 Subject: [PATCH 27/47] fixed failed test --- tests/Avalonia.Controls.UnitTests/ListBoxTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 94ada5520e..f3d71962b9 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -572,6 +572,8 @@ namespace Avalonia.Controls.UnitTests Template = ListBoxTemplate(), Items = new[] { "Foo" }, ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), + SelectionMode = SelectionMode.AlwaysSelected, + VirtualizationMode = ItemVirtualizationMode.None, }; Prepare(target); From 2dd6b574d41f5bcae5a2f878ef0fe0def902dc3d Mon Sep 17 00:00:00 2001 From: aljosas Date: Tue, 23 Mar 2021 10:27:11 +0100 Subject: [PATCH 28/47] fixing failing test --- tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index a06211c040..5395dfeadb 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -185,7 +185,8 @@ namespace Avalonia.Controls.UnitTests { var target = new ComboBox { - Template = GetTemplate() + Template = GetTemplate(), + VirtualizationMode = ItemVirtualizationMode.None }; target.ApplyTemplate(); From 2a185da6391bd550d9babc8d1e005135e18dd071 Mon Sep 17 00:00:00 2001 From: aljosas Date: Tue, 23 Mar 2021 14:45:42 +0100 Subject: [PATCH 29/47] removing Dispatcher.UIThread.RunJobs() from tests --- tests/Avalonia.Controls.UnitTests/CarouselTests.cs | 2 -- tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs | 2 -- tests/Avalonia.Controls.UnitTests/ListBoxTests.cs | 4 +--- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index ea39093602..b93e48618d 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -335,8 +335,6 @@ namespace Avalonia.Controls.UnitTests BindingErrorType.DataValidationError)); target.Bind(ComboBox.SelectedItemProperty, textObservable); - Dispatcher.UIThread.RunJobs(); - Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 5395dfeadb..8f9c7fdb0b 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -195,8 +195,6 @@ namespace Avalonia.Controls.UnitTests var exception = new System.InvalidCastException("failed validation"); var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); target.Bind(ComboBox.SelectedItemProperty, textObservable); - - Dispatcher.UIThread.RunJobs(); Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index f3d71962b9..963bba7c83 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -573,7 +573,7 @@ namespace Avalonia.Controls.UnitTests Items = new[] { "Foo" }, ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), SelectionMode = SelectionMode.AlwaysSelected, - VirtualizationMode = ItemVirtualizationMode.None, + VirtualizationMode = ItemVirtualizationMode.None }; Prepare(target); @@ -582,8 +582,6 @@ namespace Avalonia.Controls.UnitTests var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); target.Bind(ComboBox.SelectedItemProperty, textObservable); - Dispatcher.UIThread.RunJobs(); - Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); } From c6f0dfdfe3e3d359403f831e30a8017b902022d7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Mar 2021 14:18:47 +0100 Subject: [PATCH 30/47] Fix nullable warning. --- src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs index 1af6f21156..c513f75962 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -94,7 +94,7 @@ namespace Avalonia.Utilities return (0, false); } - public bool TryGetValue(AvaloniaProperty property, [MaybeNull] out TValue value) + public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) { (int index, bool found) = TryFindEntry(property.Id); if (!found) From 5bc392ee9579e591fdba2edfd8c974483dca476b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Mar 2021 15:33:10 +0100 Subject: [PATCH 31/47] Added more failing tests. To test completing/disposing bindings during and while ending batch updates. --- .../AvaloniaObjectTests_BatchUpdate.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs index 036f275a71..53ad87421e 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs @@ -53,6 +53,21 @@ namespace Avalonia.Base.UnitTests Assert.Empty(raised); } + [Fact] + public void Binding_Disposal_Should_Not_Raise_Property_Changes_During_Batch_Update() + { + var target = new TestClass(); + var observable = new TestObservable("foo"); + var raised = new List(); + + var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); + target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x)); + target.BeginBatchUpdate(); + sub.Dispose(); + + Assert.Empty(raised); + } + [Fact] public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1() { @@ -240,6 +255,27 @@ namespace Avalonia.Base.UnitTests Assert.Equal(BindingPriority.Unset, raised[0].Priority); } + [Fact] + public void Binding_Disposal_Should_Be_Raised_After_Batch_Update() + { + var target = new TestClass(); + var observable = new TestObservable("foo"); + var raised = new List(); + + var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue); + target.PropertyChanged += (s, e) => raised.Add(e); + + target.BeginBatchUpdate(); + sub.Dispose(); + target.EndBatchUpdate(); + + Assert.Equal(1, raised.Count); + Assert.Null(target.Foo); + Assert.Equal("foo", raised[0].OldValue); + Assert.Null(raised[0].NewValue); + Assert.Equal(BindingPriority.Unset, raised[0].Priority); + } + [Fact] public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1() { @@ -503,6 +539,38 @@ namespace Avalonia.Base.UnitTests Assert.Equal("bar", notifications[1].NewValue); } + [Fact] + public void Can_Bind_Completed_Binding_Back_To_Original_Value_When_Ending_Batch_Update() + { + var target = new TestClass(); + var raised = 0; + var notifications = new List(); + var observable1 = new TestObservable("foo"); + var observable2 = new TestObservable("foo"); + + target.Bind(TestClass.FooProperty, observable1); + + target.BeginBatchUpdate(); + observable1.OnCompleted(); + target.PropertyChanged += (sender, e) => + { + if (e.Property == TestClass.FooProperty && e.NewValue is null) + { + target.Bind(TestClass.FooProperty, observable2); + ++raised; + } + + notifications.Add(e); + }; + target.EndBatchUpdate(); + + Assert.Equal("foo", target.Foo); + Assert.Equal(1, raised); + Assert.Equal(2, notifications.Count); + Assert.Equal(null, notifications[0].NewValue); + Assert.Equal("foo", notifications[1].NewValue); + } + public class TestClass : AvaloniaObject { public static readonly StyledProperty FooProperty = From d2c38a85266abfb7dcd2ff5c1eead6007e7b49b6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Mar 2021 15:34:04 +0100 Subject: [PATCH 32/47] Fix more batch update problems. Fixes tests in previous commit which test scenarios where bindings are completed/disposed during and when ending batch updates. --- .../PropertyStore/BindingEntry.cs | 9 ++++++--- src/Avalonia.Base/ValueStore.cs | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 38c1728cd9..3e17a81dd8 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -65,7 +65,6 @@ namespace Avalonia.PropertyStore { _subscription?.Dispose(); _subscription = null; - _isSubscribed = false; OnCompleted(); } @@ -74,6 +73,7 @@ namespace Avalonia.PropertyStore var oldValue = _value; _value = default; Priority = BindingPriority.Unset; + _isSubscribed = false; _sink.Completed(Property, this, oldValue); } @@ -104,8 +104,11 @@ namespace Avalonia.PropertyStore public void Start(bool ignoreBatchUpdate) { // We can't use _subscription to check whether we're subscribed because it won't be set - // until Subscribe has finished, which will be too late to prevent reentrancy. - if (!_isSubscribed && (!_batchUpdate || ignoreBatchUpdate)) + // until Subscribe has finished, which will be too late to prevent reentrancy. In addition + // don't re-subscribe to completed/disposed bindings (indicated by Unset priority). + if (!_isSubscribed && + Priority != BindingPriority.Unset && + (!_batchUpdate || ignoreBatchUpdate)) { _isSubscribed = true; _subscription = Source.Subscribe(this); diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 9ece2b8042..8a8e9ad153 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -173,7 +173,7 @@ namespace Avalonia // During batch update values can't be removed immediately because they're needed to raise // a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal // by setting their priority to Unset. - if (_batchUpdate is null) + if (!IsBatchUpdating()) { _values.Remove(property); } @@ -285,7 +285,7 @@ namespace Avalonia else { var priorityValue = new PriorityValue(_owner, property, this, l); - if (_batchUpdate is object) + if (IsBatchUpdating()) priorityValue.BeginBatchUpdate(); result = priorityValue.SetValue(value, priority); _values.SetValue(property, priorityValue); @@ -311,7 +311,7 @@ namespace Avalonia { priorityValue = new PriorityValue(_owner, property, this, e); - if (_batchUpdate is object) + if (IsBatchUpdating()) { priorityValue.BeginBatchUpdate(); } @@ -338,7 +338,7 @@ namespace Avalonia private void AddValue(AvaloniaProperty property, IValue value) { _values.AddValue(property, value); - if (_batchUpdate?.IsBatchUpdating == true && value is IBatchUpdate batch) + if (IsBatchUpdating() && value is IBatchUpdate batch) batch.BeginBatchUpdate(); value.Start(); } @@ -364,6 +364,8 @@ namespace Avalonia } } + private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true; + private static bool IsRemoveSentinel(IValue value) { // Local value entries are optimized and contain only a single value field to save space, @@ -447,9 +449,14 @@ namespace Avalonia // During batch update values can't be removed immediately because they're needed to raise // the _sink.ValueChanged notification. They instead mark themselves for removal by setting - // their priority to Unset. - if (slot.Priority == BindingPriority.Unset) + // their priority to Unset. We need to re-read the slot here because raising ValueChanged + // could have caused it to be updated. + if (values.TryGetValue(entry.property, out var updatedSlot) && + updatedSlot.Priority == BindingPriority.Unset) { + if (entry.property.Name == "Transitions" && _owner._owner.GetType().Name == "TabItem") + { + } values.Remove(entry.property); } } From eba052972683abde8f0abbfea28b418ec9091b32 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Mar 2021 16:15:14 +0100 Subject: [PATCH 33/47] Added failing transitions test. --- tests/Avalonia.Animation.UnitTests/AnimatableTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs index 7633a761a3..93b3d2c180 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs @@ -330,6 +330,15 @@ namespace Avalonia.Animation.UnitTests } } + [Fact] + public void Transitions_Can_Be_Changed_To_Collection_That_Contains_The_Same_Transitions() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.Transitions = new Transitions { target.Object }; + } + private static Mock CreateTarget() { return CreateTransition(Visual.OpacityProperty); From 99370bff271f70810049b46ae2032a673e5f79ea Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Mar 2021 16:42:35 +0100 Subject: [PATCH 34/47] Correctly handle changing transitions. Handle changing a `Transitions` collections to new collection which contains some of the same elements. Added a comment explaining why we add new transitions before replacing old transitions; was explained in the commit message for 0694b22c51b96fb8d997ab2dc25a4d5503789a3f. --- src/Avalonia.Animation/Animatable.cs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 067d9f462f..b701a101be 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Data; #nullable enable @@ -93,16 +94,35 @@ namespace Avalonia.Animation var oldTransitions = change.OldValue.GetValueOrDefault(); var newTransitions = change.NewValue.GetValueOrDefault(); + // When transitions are replaced, we add the new transitions before removing the old + // transitions, so that when the old transition being disposed causes the value to + // change, there is a corresponding entry in `_transitionStates`. This means that we + // need to account for any transitions present in both the old and new transitions + // collections. if (newTransitions is object) { + var toAdd = (IList)newTransitions; + + if (newTransitions.Count > 0 && oldTransitions?.Count > 0) + { + toAdd = newTransitions.Except(oldTransitions).ToList(); + } + newTransitions.CollectionChanged += TransitionsCollectionChanged; - AddTransitions(newTransitions); + AddTransitions(toAdd); } if (oldTransitions is object) { + var toRemove = (IList)oldTransitions; + + if (oldTransitions.Count > 0 && newTransitions?.Count > 0) + { + toRemove = oldTransitions.Except(newTransitions).ToList(); + } + oldTransitions.CollectionChanged -= TransitionsCollectionChanged; - RemoveTransitions(oldTransitions); + RemoveTransitions(toRemove); } } else if (_transitionsEnabled && From 15a765490f7a6cb6ca3867a2e310d4d942faca70 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Mar 2021 16:57:21 +0100 Subject: [PATCH 35/47] Added another failing transitions test. --- .../AnimatableTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs index 93b3d2c180..b01fb70f58 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs @@ -339,6 +339,28 @@ namespace Avalonia.Animation.UnitTests control.Transitions = new Transitions { target.Object }; } + [Fact] + public void Transitions_Can_Re_Set_During_Batch_Update() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + // Assigning and then clearing Transitions ensures we have a transition state + // collection created. + control.Transitions = null; + + control.BeginBatchUpdate(); + + // Setting opacity then Transitions means that we receive the Transitions change + // after the Opacity change when EndBatchUpdate is called. + control.Opacity = 0.5; + control.Transitions = new Transitions { target.Object }; + + // Which means that the transition state hasn't been initialized with the new + // Transitions when the Opacity change notification gets raised here. + control.EndBatchUpdate(); + } + private static Mock CreateTarget() { return CreateTransition(Visual.OpacityProperty); From 66cd58566c83e045f2f63c7f044cff528a2aa773 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Mar 2021 18:09:35 +0100 Subject: [PATCH 36/47] Handle changing transitions during batch update. Fixes failing test from previous commit. --- src/Avalonia.Animation/Animatable.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index b701a101be..a415046513 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -135,9 +135,9 @@ namespace Avalonia.Animation { var transition = Transitions[i]; - if (transition.Property == change.Property) + if (transition.Property == change.Property && + _transitionState.TryGetValue(transition, out var state)) { - var state = _transitionState[transition]; var oldValue = state.BaseValue; var newValue = GetAnimationBaseValue(transition.Property); From f713af3a7c6282f9face859ce5b6cdf57d228e8f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Mar 2021 18:14:31 +0100 Subject: [PATCH 37/47] Make handling of sentinel values consistent. --- .../PropertyStore/ConstantValueEntry.cs | 9 ++++++- src/Avalonia.Base/ValueStore.cs | 27 ++++++++++++------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index 600d725187..d39fc3bb1e 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -6,12 +6,19 @@ using Avalonia.Data; namespace Avalonia.PropertyStore { + /// + /// Represents an untyped interface to . + /// + internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable + { + } + /// /// Stores a value with a priority in a or /// . /// /// The property type. - internal class ConstantValueEntry : IPriorityValueEntry, IDisposable + internal class ConstantValueEntry : IPriorityValueEntry, IConstantValueEntry { private IValueSink _sink; private Optional _value; diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 8a8e9ad153..17b3c7f7f5 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Avalonia.Data; using Avalonia.PropertyStore; using Avalonia.Utilities; @@ -56,7 +57,7 @@ namespace Avalonia public bool IsAnimating(AvaloniaProperty property) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { return slot.Priority < BindingPriority.LocalValue; } @@ -66,7 +67,7 @@ namespace Avalonia public bool IsSet(AvaloniaProperty property) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { return slot.GetValue().HasValue; } @@ -79,7 +80,7 @@ namespace Avalonia BindingPriority maxPriority, out T value) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { var v = ((IValue)slot).GetValue(maxPriority); @@ -103,7 +104,7 @@ namespace Avalonia IDisposable? result = null; - if (_values.TryGetValue(property, out var slot) && !IsRemoveSentinel(slot)) + if (TryGetValue(property, out var slot)) { result = SetExisting(slot, property, value, priority); } @@ -138,7 +139,7 @@ namespace Avalonia IObservable> source, BindingPriority priority) { - if (_values.TryGetValue(property, out var slot) && !IsRemoveSentinel(slot)) + if (TryGetValue(property, out var slot)) { return BindExisting(slot, property, source, priority); } @@ -160,7 +161,7 @@ namespace Avalonia public void ClearLocalValue(StyledPropertyBase property) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { if (slot is PriorityValue p) { @@ -198,7 +199,7 @@ namespace Avalonia public void CoerceValue(StyledPropertyBase property) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { if (slot is PriorityValue p) { @@ -209,7 +210,7 @@ namespace Avalonia public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { var slotValue = slot.GetValue(); return new Diagnostics.AvaloniaPropertyValue( @@ -242,6 +243,7 @@ namespace Avalonia IPriorityValueEntry entry, Optional oldValue) { + // We need to include remove sentinels here so call `_values.TryGetValue` directly. if (_values.TryGetValue(property, out var slot) && slot == entry) { if (_batchUpdate is null) @@ -366,12 +368,17 @@ namespace Avalonia private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true; - private static bool IsRemoveSentinel(IValue value) + private bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out IValue value) + { + return _values.TryGetValue(property, out value) && !IsRemoveSentinel(value); + } + + private static bool IsRemoveSentinel(IValue value) { // Local value entries are optimized and contain only a single value field to save space, // so there's no way to mark them for removal at the end of a batch update. Instead a // ConstantValueEntry with a priority of Unset is used as a sentinel value. - return value is ConstantValueEntry t && t.Priority == BindingPriority.Unset; + return value is IConstantValueEntry t && t.Priority == BindingPriority.Unset; } private class BatchUpdate From f325da4f4db7b3f45ab48d4cd3881056d6f5ab64 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Mar 2021 22:11:07 +0100 Subject: [PATCH 38/47] Remove debugging code. --- src/Avalonia.Base/ValueStore.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 17b3c7f7f5..495f13e1a9 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -461,9 +461,6 @@ namespace Avalonia if (values.TryGetValue(entry.property, out var updatedSlot) && updatedSlot.Priority == BindingPriority.Unset) { - if (entry.property.Name == "Transitions" && _owner._owner.GetType().Name == "TabItem") - { - } values.Remove(entry.property); } } From 31d7df39c0968a8fc991b6544e7db80a1c48ec3d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 29 Mar 2021 20:35:54 +0100 Subject: [PATCH 39/47] fix datavalidation errors on fluent theme. --- src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml index ed31f7b573..f83af266c2 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml @@ -35,7 +35,7 @@ - + From 37e8890d2729d5e14912b3cc1a680d4672194c25 Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Tue, 30 Mar 2021 12:40:03 +0200 Subject: [PATCH 40/47] CanExecute reevaluation on attaching to logical tree --- src/Avalonia.Controls/Button.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index c779e4b0cb..6093cbd581 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -218,6 +218,7 @@ namespace Avalonia.Controls if (Command != null) { Command.CanExecuteChanged += CanExecuteChanged; + CanExecuteChanged(this, EventArgs.Empty); } } From 741f3458d4ec1e5bb32916e12f3d538b46c483a1 Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Thu, 1 Apr 2021 17:44:00 +0200 Subject: [PATCH 41/47] Allowed empty templates --- .../Templates/DataTemplate.cs | 2 +- .../Templates/ItemsPanelTemplate.cs | 3 +-- .../Templates/Template.cs | 2 +- .../Templates/TemplateContent.cs | 8 ++++-- .../Templates/TreeDataTemplate.cs | 8 ++++-- .../Xaml/DataTemplateTests.cs | 25 +++++++++++++++++++ 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 07c5451135..650534b347 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -30,7 +30,7 @@ namespace Avalonia.Markup.Xaml.Templates public IControl Build(object data, IControl existing) { - return existing ?? TemplateContent.Load(Content).Control; + return existing ?? TemplateContent.Load(Content)?.Control; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs index c8843a3176..c096ed7ed7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs @@ -10,8 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object Content { get; set; } - public IPanel Build() - => (IPanel)TemplateContent.Load(Content).Control; + public IPanel Build() => (IPanel)TemplateContent.Load(Content)?.Control; object ITemplate.Build() => Build(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs index 65323ae665..45fae9cb28 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object Content { get; set; } - public IControl Build() => TemplateContent.Load(Content).Control; + public IControl Build() => TemplateContent.Load(Content)?.Control; object ITemplate.Build() => Build(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 96f25668fb..483a1a5d06 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -1,6 +1,4 @@ using System; -using Avalonia.Controls; -using System.Collections.Generic; using Avalonia.Controls.Templates; namespace Avalonia.Markup.Xaml.Templates @@ -14,6 +12,12 @@ namespace Avalonia.Markup.Xaml.Templates { return (ControlTemplateResult)direct(null); } + + if (templateContent is null) + { + return null; + } + throw new ArgumentException(nameof(templateContent)); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index d785ac4ac0..7b065c7f47 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -51,8 +51,12 @@ namespace Avalonia.Markup.Xaml.Templates public IControl Build(object data) { - var visualTreeForItem = TemplateContent.Load(Content).Control; - visualTreeForItem.DataContext = data; + var visualTreeForItem = TemplateContent.Load(Content)?.Control; + if (visualTreeForItem != null) + { + visualTreeForItem.DataContext = data; + } + return visualTreeForItem; } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index 033b670bf4..53881467e7 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -7,6 +7,31 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml { public class DataTemplateTests : XamlTestBase { + [Fact] + public void DataTemplate_Can_Be_Empty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var target = window.FindControl("target"); + + window.ApplyTemplate(); + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + Assert.Null(target.Presenter.Child); + } + } + [Fact] public void DataTemplate_Can_Contain_Name() { From 65792cf4b037ec3f48d8e8c54de775608a519db8 Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 2 Apr 2021 02:21:08 +0300 Subject: [PATCH 42/47] feature: Disable ReactiveUI platform registrations --- build/ReactiveUI.props | 2 +- src/Avalonia.ReactiveUI/AppBuilderExtensions.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props index f74ab07e31..c3b136d41d 100644 --- a/build/ReactiveUI.props +++ b/build/ReactiveUI.props @@ -1,5 +1,5 @@ - + diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index 6771d3e179..fa666e2125 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -17,6 +17,8 @@ namespace Avalonia.ReactiveUI { return builder.AfterPlatformServicesSetup(_ => { + PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia); + Locator.CurrentMutable.InitializeReactiveUI(); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); From cc5568781cf046d8b81efe0fc56137b1fe920499 Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 2 Apr 2021 02:31:08 +0300 Subject: [PATCH 43/47] Don't initialize things twice --- src/Avalonia.ReactiveUI/AppBuilderExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index fa666e2125..e5250484e2 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -18,7 +18,6 @@ namespace Avalonia.ReactiveUI return builder.AfterPlatformServicesSetup(_ => { PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia); - Locator.CurrentMutable.InitializeReactiveUI(); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); From 2045097cc800b27df5c881c9970f617634f795ac Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 2 Apr 2021 11:45:58 +0300 Subject: [PATCH 44/47] Wrap UseReactiveUI in RegisterResolverCallbackChanged --- src/Avalonia.ReactiveUI/AppBuilderExtensions.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index e5250484e2..359da3d7c2 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -9,19 +9,22 @@ namespace Avalonia.ReactiveUI { /// /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia - /// scheduler and Avalonia activation for view fetcher. Always remember to - /// call this method if you are using ReactiveUI in your application. + /// scheduler, an activation for view fetcher, a template binding hook. Remember + /// to call this method if you are using ReactiveUI in your application. /// public static TAppBuilder UseReactiveUI(this TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() - { - return builder.AfterPlatformServicesSetup(_ => + where TAppBuilder : AppBuilderBase, new() => + builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() => { + if (Locator.CurrentMutable is null) + { + return; + } + PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); - }); - } + })); } } From 38703b94232a562cfc20a7b50ca971e37a28bc43 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Apr 2021 15:29:46 +0200 Subject: [PATCH 45/47] Fix pointer interaction with reversed direction slider. Previously dragging a slider with `IsDirectionRevered = true` resulted in the slider moving the wrong way. --- src/Avalonia.Controls/Slider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index e02efc2bd2..6419981fb1 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -341,7 +341,9 @@ namespace Avalonia.Controls var pointNum = orient ? x.Position.X : x.Position.Y; var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d); - var invert = orient ? 0 : 1; + var invert = orient ? + IsDirectionReversed ? 1 : 0 : + IsDirectionReversed ? 0 : 1; var calcVal = Math.Abs(invert - logicalPos); var range = Maximum - Minimum; var finalValue = calcVal * range + Minimum; From 2fcad40bce09115ab022a600e26de8a1de4f1903 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 3 Apr 2021 16:27:00 +0800 Subject: [PATCH 46/47] Add Deterministic XamlX ID Generator (#5684) * Add Deterministic XamlX ID Generator * Apply suggestions from code review * simplify stuff and apply review * Update src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs * Update src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs * add the det id gen to runtine xamlx compiler * rerun tests * rerun tests * try this * use id gen instead of guid * a * Update src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs * Update AvaloniaXamlIlCompilerConfiguration.cs revert * Update XamlIlClrPropertyInfoHelper.cs * Update AvaloniaXamlIlRuntimeCompiler.cs * fix * revert hack * make id gen optional --- src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs | 12 ++++++++++++ src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs | 4 ++-- .../AvaloniaXamlIlCompilerConfiguration.cs | 5 +++-- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs diff --git a/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs b/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs new file mode 100644 index 0000000000..f207b558a3 --- /dev/null +++ b/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs @@ -0,0 +1,12 @@ +using System; +using XamlX.Transform; + +namespace Avalonia.Build.Tasks +{ + public class DeterministicIdGenerator : IXamlIdentifierGenerator + { + private int _nextId = 1; + + public string GenerateIdentifierPart() => (_nextId++).ToString(); + } +} diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 6ef8a98fae..508045dccb 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -22,7 +22,6 @@ using XamlX.IL; namespace Avalonia.Build.Tasks { - public static partial class XamlCompilerTaskExecutor { static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml") @@ -99,7 +98,8 @@ namespace Avalonia.Build.Tasks XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage), AvaloniaXamlIlLanguage.CustomValueConverter, new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)), - new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure))); + new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)), + new DeterministicIdGenerator()); var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext", diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs index 0c0dcb1634..f6f47dce0d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs @@ -14,8 +14,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XamlXmlnsMappings xmlnsMappings, XamlValueConverter customValueConverter, XamlIlClrPropertyInfoEmitter clrPropertyEmitter, - XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter) - : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter) + XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter, + IXamlIdentifierGenerator identifierGenerator = null) + : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter, identifierGenerator) { ClrPropertyEmitter = clrPropertyEmitter; AccessorFactoryEmitter = accessorFactoryEmitter; From 64b939bee84ae923d2d3baf3a01f8ab825a15a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Neves?= Date: Mon, 5 Apr 2021 12:41:51 +0100 Subject: [PATCH 47/47] Show and hide native control if visibility of any of its ancestors changes --- src/Avalonia.Controls/NativeControlHost.cs | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index 64414b1f47..c2fe1bb691 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -16,30 +16,16 @@ namespace Avalonia.Controls private bool _queuedForDestruction; private bool _queuedForMoveResize; private readonly List _propertyChangedSubscriptions = new List(); - private readonly EventHandler _propertyChangedHandler; - static NativeControlHost() - { - IsVisibleProperty.Changed.AddClassHandler(OnVisibleChanged); - } - - public NativeControlHost() - { - _propertyChangedHandler = PropertyChangedHandler; - } - - private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2) - => host.UpdateHost(); protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { _currentRoot = e.Root as TopLevel; var visual = (IVisual)this; - while (visual != _currentRoot) + while (visual != null) { - if (visual is Visual v) { - v.PropertyChanged += _propertyChangedHandler; + v.PropertyChanged += PropertyChangedHandler; _propertyChangedSubscriptions.Add(v); } @@ -51,7 +37,7 @@ namespace Avalonia.Controls private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e) { - if (e.IsEffectiveValueChange && e.Property == BoundsProperty) + if (e.IsEffectiveValueChange && (e.Property == BoundsProperty || e.Property == IsVisibleProperty)) EnqueueForMoveResize(); } @@ -61,7 +47,7 @@ namespace Avalonia.Controls if (_propertyChangedSubscriptions != null) { foreach (var v in _propertyChangedSubscriptions) - v.PropertyChanged -= _propertyChangedHandler; + v.PropertyChanged -= PropertyChangedHandler; _propertyChangedSubscriptions.Clear(); } UpdateHost();