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/33] 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/33] 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/33] 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 ea580c71619331b2d6eebefd203138151c519c3f Mon Sep 17 00:00:00 2001 From: sdoroff Date: Wed, 3 Mar 2021 23:47:06 -0500 Subject: [PATCH 04/33] DataGridCheckBoxColumn Edit Fix --- .../DataGridCheckBoxColumn.cs | 58 +++++++++++++------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs index e2a067ac61..179abd4b63 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs @@ -153,23 +153,7 @@ namespace Avalonia.Controls { if (editingElement is CheckBox editingCheckBox) { - bool? uneditedValue = editingCheckBox.IsChecked; - bool editValue = false; - if(editingEventArgs is PointerPressedEventArgs args) - { - // Editing was triggered by a mouse click - Point position = args.GetPosition(editingCheckBox); - Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height); - editValue = rect.Contains(position); - } - else if (_beganEditWithKeyboard) - { - // Editing began by a user pressing spacebar - editValue = true; - _beganEditWithKeyboard = false; - } - - if (editValue) + void EditValue() { // User clicked the checkbox itself or pressed space, let's toggle the IsChecked value if (editingCheckBox.IsThreeState) @@ -192,6 +176,46 @@ namespace Avalonia.Controls editingCheckBox.IsChecked = !editingCheckBox.IsChecked; } } + + bool? uneditedValue = editingCheckBox.IsChecked; + if(editingEventArgs is PointerPressedEventArgs args) + { + void ProcessPointerArgs() + { + // Editing was triggered by a mouse click + Point position = args.GetPosition(editingCheckBox); + Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height); + if(rect.Contains(position)) + { + EditValue(); + } + } + + void OnLayoutUpdated(object sender, EventArgs e) + { + if(!editingCheckBox.Bounds.IsEmpty) + { + editingCheckBox.LayoutUpdated -= OnLayoutUpdated; + ProcessPointerArgs(); + } + } + + if(editingCheckBox.Bounds.IsEmpty) + { + editingCheckBox.LayoutUpdated += OnLayoutUpdated; + } + else + { + ProcessPointerArgs(); + } + } + else if (_beganEditWithKeyboard) + { + // Editing began by a user pressing spacebar + _beganEditWithKeyboard = false; + EditValue(); + } + return uneditedValue; } return false; From fe0b343b3877e7ba2b4f52ee7638a5c44d3171a6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 5 Mar 2021 20:27:13 +0000 Subject: [PATCH 05/33] fix window sizing when client area is extended on win32 --- src/Windows/Avalonia.Win32/WindowImpl.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 1ddec2e763..a42dd5fc07 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -84,6 +84,7 @@ namespace Avalonia.Win32 private WindowImpl _parent; private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; private bool _isCloseRequested; + private bool _shown; public WindowImpl() { @@ -565,6 +566,7 @@ namespace Avalonia.Win32 public void Hide() { UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide); + _shown = false; } public virtual void Show(bool activate) @@ -871,6 +873,11 @@ namespace Avalonia.Win32 private void ExtendClientArea() { + if (!_shown) + { + return; + } + if (DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled) { _isClientAreaExtended = false; @@ -916,6 +923,13 @@ namespace Avalonia.Win32 private void ShowWindow(WindowState state, bool activate) { + _shown = true; + + if (_isClientAreaExtended) + { + ExtendClientArea(); + } + ShowWindowCommand? command; var newWindowProperties = _windowProperties; From 1fa6bd25eb16e4ace4990bd9638cbd843cb57a5a Mon Sep 17 00:00:00 2001 From: luthfiampas Date: Sun, 7 Mar 2021 14:27:42 +0700 Subject: [PATCH 06/33] 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 07/33] 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 08/33] 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 09/33] 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 20ddbf8dd8a87cb5eb055556e6265890c8f09014 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Mar 2021 16:23:59 +0000 Subject: [PATCH 10/33] OSX - add interface for file/url opened api. --- src/Avalonia.Native/avn.idl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 57a0c32067..2693f5f139 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -403,7 +403,7 @@ enum AvnExtendClientAreaChromeHints [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] interface IAvaloniaNativeFactory : IUnknown { - HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator); + HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* appCb); IAvnMacOptions* GetMacOptions(); HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv); HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv); @@ -728,3 +728,9 @@ interface IAvnNativeControlHostTopLevelAttachment : IUnknown void HideWithSize(float width, float height); void ReleaseChild(); } + +[uuid(6575b5af-f27a-4609-866c-f1f014c20f79)] +interface IAvnApplicationEvents : IUnknown +{ + void FilesOpened (IAvnStringArray* urls); +} From 4191e276efa42aea921a41140c7f19a32261ad32 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Mar 2021 16:24:17 +0000 Subject: [PATCH 11/33] osx - native side implementation of apis. --- native/Avalonia.Native/src/OSX/app.mm | 21 ++++++++++++++++++--- native/Avalonia.Native/src/OSX/common.h | 2 +- native/Avalonia.Native/src/OSX/main.mm | 4 ++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 814b91cb62..b87810dde1 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -1,10 +1,20 @@ #include "common.h" +#include "AvnString.h" @interface AvnAppDelegate : NSObject +-(AvnAppDelegate* _Nonnull) initWithEvents: (IAvnApplicationEvents* _Nonnull) events; @end NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; @implementation AvnAppDelegate +ComPtr _events; + +- (AvnAppDelegate *)initWithEvents:(IAvnApplicationEvents *)events +{ + _events = events; + return self; +} + - (void)applicationWillFinishLaunching:(NSNotification *)notification { if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy) @@ -27,11 +37,16 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps]; } +- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames +{ + auto array = CreateAvnStringArray(filenames); + + _events->FilesOpened(array); +} @end @interface AvnApplication : NSApplication - @end @implementation AvnApplication @@ -63,9 +78,9 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati @end -extern void InitializeAvnApp() +extern void InitializeAvnApp(IAvnApplicationEvents* events) { NSApplication* app = [AvnApplication sharedApplication]; - id delegate = [AvnAppDelegate new]; + id delegate = [[AvnAppDelegate alloc] initWithEvents:events]; [app setDelegate:delegate]; } diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 05b2d762ae..0f7215f37c 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -31,7 +31,7 @@ extern NSMenuItem* GetAppMenuItem (); extern void SetAutoGenerateDefaultAppMenuItems (bool enabled); extern bool GetAutoGenerateDefaultAppMenuItems (); -extern void InitializeAvnApp(); +extern void InitializeAvnApp(IAvnApplicationEvents* events); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); extern AvnPoint ToAvnPoint (NSPoint p); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 17746e1d1d..11742e3b5c 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -163,13 +163,13 @@ class AvaloniaNative : public ComSingleObject Date: Mon, 8 Mar 2021 21:37:05 +0000 Subject: [PATCH 12/33] add implementation of url event for application class. --- src/Avalonia.Controls/Application.cs | 14 +++++++++++++- .../Platform/IApplicationPlatform.cs | 9 +++++++++ src/Avalonia.Controls/UrlOpenedEventArgs.cs | 14 ++++++++++++++ .../AvaloniaNativeApplicationPlatform.cs | 16 ++++++++++++++++ src/Avalonia.Native/AvaloniaNativePlatform.cs | 8 +++++--- 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 src/Avalonia.Controls/Platform/IApplicationPlatform.cs create mode 100644 src/Avalonia.Controls/UrlOpenedEventArgs.cs create mode 100644 src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 3bf72460df..4b3fb70d72 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -55,6 +55,8 @@ namespace Avalonia /// public event EventHandler ResourcesChanged; + public event EventHandler UrlOpened; + /// /// Creates an instance of the class. /// @@ -247,7 +249,17 @@ namespace Avalonia public virtual void OnFrameworkInitializationCompleted() { - + var applicationPlatform = AvaloniaLocator.Current.GetService(); + + if (applicationPlatform != null) + { + applicationPlatform.FilesOpened = OnFilesOpened; + } + } + + private void OnFilesOpened(string[] urls) + { + UrlOpened?.Invoke(this, new UrlOpenedEventArgs (urls)); } private void NotifyResourcesChanged(ResourcesChangedEventArgs e) diff --git a/src/Avalonia.Controls/Platform/IApplicationPlatform.cs b/src/Avalonia.Controls/Platform/IApplicationPlatform.cs new file mode 100644 index 0000000000..ec193ce0d6 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IApplicationPlatform.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Platform +{ + public interface IApplicationPlatform + { + Action FilesOpened { get; set; } + } +} diff --git a/src/Avalonia.Controls/UrlOpenedEventArgs.cs b/src/Avalonia.Controls/UrlOpenedEventArgs.cs new file mode 100644 index 0000000000..bc29d7ffa2 --- /dev/null +++ b/src/Avalonia.Controls/UrlOpenedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia +{ + public class UrlOpenedEventArgs : EventArgs + { + public UrlOpenedEventArgs(string[] urls) + { + Urls = urls; + } + + public string[] Urls { get; } + } +} diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs new file mode 100644 index 0000000000..ff7c4fdd6e --- /dev/null +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia.Native.Interop; +using Avalonia.Platform; + +namespace Avalonia.Native +{ + internal class AvaloniaNativeApplicationPlatform : CallbackBase, IApplicationPlatform, IAvnApplicationEvents + { + public Action FilesOpened { get; set; } + + void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls) + { + FilesOpened?.Invoke(urls.ToStringArray()); + } + } +} diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 35d21a75d3..f9806f02a4 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.InteropServices; -using System.Security.Cryptography; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; @@ -9,7 +8,6 @@ using Avalonia.Native.Interop; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; -using Avalonia.Platform.Interop; namespace Avalonia.Native { @@ -86,7 +84,10 @@ namespace Avalonia.Native void DoInitialize(AvaloniaNativePlatformOptions options) { _options = options; - _factory.Initialize(new GCHandleDeallocator()); + + var applicationPlatform = new AvaloniaNativeApplicationPlatform(); + + _factory.Initialize(new GCHandleDeallocator(), applicationPlatform); if (_factory.MacOptions != null) { var macOpts = AvaloniaLocator.Current.GetService(); @@ -97,6 +98,7 @@ namespace Avalonia.Native } AvaloniaLocator.CurrentMutable + .Bind().ToConstant(applicationPlatform) .Bind() .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface())) .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) From 9ded3bc2d32a5bac0b5435ec4643c876d8753eca Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Mar 2021 21:47:31 +0000 Subject: [PATCH 13/33] make event name plural --- src/Avalonia.Controls/Application.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 4b3fb70d72..6c35c2749e 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -55,7 +55,7 @@ namespace Avalonia /// public event EventHandler ResourcesChanged; - public event EventHandler UrlOpened; + public event EventHandler UrlsOpened; /// /// Creates an instance of the class. @@ -259,7 +259,7 @@ namespace Avalonia private void OnFilesOpened(string[] urls) { - UrlOpened?.Invoke(this, new UrlOpenedEventArgs (urls)); + UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls)); } private void NotifyResourcesChanged(ResourcesChangedEventArgs e) From df8f30793c6f7f490462a12fe478bf58436a6cf3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Mar 2021 21:47:45 +0000 Subject: [PATCH 14/33] implement both files and uris opened on osx. --- native/Avalonia.Native/src/OSX/app.mm | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index b87810dde1..460c24ea3a 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -43,6 +43,13 @@ ComPtr _events; _events->FilesOpened(array); } + +- (void)application:(NSApplication *)application openURLs:(NSArray *)urls +{ + auto array = CreateAvnStringArray(urls); + + _events->FilesOpened(array); +} @end @interface AvnApplication : NSApplication From 8e6502f222c3eb8305a28277c8d635fe6fcdfc34 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Mar 2021 21:56:02 +0000 Subject: [PATCH 15/33] much simpler interface with application class and platform events. --- src/Avalonia.Controls/Application.cs | 13 ++++--------- .../Platform/IApplicationPlatform.cs | 9 --------- .../Platform/IApplicationPlatformEvents.cs | 7 +++++++ .../AvaloniaNativeApplicationPlatform.cs | 6 ++---- src/Avalonia.Native/AvaloniaNativePlatform.cs | 1 - 5 files changed, 13 insertions(+), 23 deletions(-) delete mode 100644 src/Avalonia.Controls/Platform/IApplicationPlatform.cs create mode 100644 src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 6c35c2749e..54c576bb76 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -30,7 +30,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost, IApplicationPlatformEvents { /// /// The application-global data templates. @@ -249,15 +249,9 @@ namespace Avalonia public virtual void OnFrameworkInitializationCompleted() { - var applicationPlatform = AvaloniaLocator.Current.GetService(); - - if (applicationPlatform != null) - { - applicationPlatform.FilesOpened = OnFilesOpened; - } } - - private void OnFilesOpened(string[] urls) + + void IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls) { UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls)); } @@ -300,5 +294,6 @@ namespace Avalonia get => _name; set => SetAndRaise(NameProperty, ref _name, value); } + } } diff --git a/src/Avalonia.Controls/Platform/IApplicationPlatform.cs b/src/Avalonia.Controls/Platform/IApplicationPlatform.cs deleted file mode 100644 index ec193ce0d6..0000000000 --- a/src/Avalonia.Controls/Platform/IApplicationPlatform.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Avalonia.Platform -{ - public interface IApplicationPlatform - { - Action FilesOpened { get; set; } - } -} diff --git a/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs b/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs new file mode 100644 index 0000000000..a8d3a3b3ac --- /dev/null +++ b/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Platform +{ + public interface IApplicationPlatformEvents + { + void RaiseUrlsOpened(string[] urls); + } +} diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index ff7c4fdd6e..9579aa93b1 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -4,13 +4,11 @@ using Avalonia.Platform; namespace Avalonia.Native { - internal class AvaloniaNativeApplicationPlatform : CallbackBase, IApplicationPlatform, IAvnApplicationEvents + internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents { - public Action FilesOpened { get; set; } - void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls) { - FilesOpened?.Invoke(urls.ToStringArray()); + ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray()); } } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index f9806f02a4..82a845ffc1 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -98,7 +98,6 @@ namespace Avalonia.Native } AvaloniaLocator.CurrentMutable - .Bind().ToConstant(applicationPlatform) .Bind() .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface())) .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) From 890185ea02032478b8663843971645633b47f2eb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Mar 2021 21:58:19 +0000 Subject: [PATCH 16/33] ClassicDesktopApplicationLifetime raises urlsopened api. --- .../ClassicDesktopStyleApplicationLifetime.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index e2c8e7e8e2..a9cfbb2a6b 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -5,6 +5,7 @@ using System.Threading; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Interactivity; +using Avalonia.Platform; using Avalonia.Threading; namespace Avalonia.Controls.ApplicationLifetimes @@ -102,6 +103,7 @@ namespace Avalonia.Controls.ApplicationLifetimes public int Start(string[] args) { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); + ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); _cts = new CancellationTokenSource(); MainWindow?.Show(); Dispatcher.UIThread.MainLoop(_cts.Token); From e2da5b190cc4239512a851621fea9d85ff87a193 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Mar 2021 22:03:12 +0000 Subject: [PATCH 17/33] fix nsurl conversion to string[] --- native/Avalonia.Native/src/OSX/AvnString.h | 1 + native/Avalonia.Native/src/OSX/AvnString.mm | 15 +++++++++++++++ native/Avalonia.Native/src/OSX/clipboard.mm | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h index 5d299374e5..3ce83d370a 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.h +++ b/native/Avalonia.Native/src/OSX/AvnString.h @@ -11,6 +11,7 @@ extern IAvnString* CreateAvnString(NSString* string); extern IAvnStringArray* CreateAvnStringArray(NSArray* array); +extern IAvnStringArray* CreateAvnStringArray(NSArray* array); extern IAvnStringArray* CreateAvnStringArray(NSString* string); extern IAvnString* CreateByteArray(void* data, int len); #endif /* AvnString_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index 00b748ef63..001cf151d8 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -85,6 +85,16 @@ public: } } + AvnStringArrayImpl(NSArray* array) + { + for(int c = 0; c < [array count]; c++) + { + ComPtr s; + *s.getPPV() = new AvnStringImpl([array objectAtIndex:c].absoluteString); + _list.push_back(s); + } + } + AvnStringArrayImpl(NSString* string) { ComPtr s; @@ -117,6 +127,11 @@ IAvnStringArray* CreateAvnStringArray(NSArray * array) return new AvnStringArrayImpl(array); } +IAvnStringArray* CreateAvnStringArray(NSArray * array) +{ + return new AvnStringArrayImpl(array); +} + IAvnStringArray* CreateAvnStringArray(NSString* string) { return new AvnStringArrayImpl(string); diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 303f727317..f148374759 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -56,7 +56,7 @@ public: return S_OK; } - NSArray* arr = (NSArray*)data; + NSArray* arr = (NSArray*)data; for(int c = 0; c < [arr count]; c++) if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]]) From 60f9408b4590332a3fc76a5fbd6074591b429680 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Mar 2021 22:05:07 +0000 Subject: [PATCH 18/33] only raise urls opened event from classic lifetime if there are arguments. --- .../ClassicDesktopStyleApplicationLifetime.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index a9cfbb2a6b..f8606fcd6f 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -103,7 +103,12 @@ namespace Avalonia.Controls.ApplicationLifetimes public int Start(string[] args) { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); - ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); + + if (args.Length > 0) + { + ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); + } + _cts = new CancellationTokenSource(); MainWindow?.Show(); Dispatcher.UIThread.MainLoop(_cts.Token); From c204f4f22315558578829b6e8f5ef46fa3b30404 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Mar 2021 22:50:40 +0000 Subject: [PATCH 19/33] try and parse as a uri before raising event. --- .../ClassicDesktopStyleApplicationLifetime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index f8606fcd6f..38220c250a 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -104,7 +104,7 @@ namespace Avalonia.Controls.ApplicationLifetimes { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); - if (args.Length > 0) + if (args.Length > 0 && Uri.TryCreate(args[0], UriKind.Absolute, out _)) { ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); } From 6ee69afd0db8ff867a32390cbc20485dc0e249fd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 9 Mar 2021 09:51:43 +0000 Subject: [PATCH 20/33] remove uri check. --- .../ClassicDesktopStyleApplicationLifetime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 38220c250a..86f0491dc5 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -104,7 +104,7 @@ namespace Avalonia.Controls.ApplicationLifetimes { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); - if (args.Length > 0 && Uri.TryCreate(args[0], UriKind.Absolute, out _)) + if (args.Length > 0)) { ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); } From 9d4ab09dd7ae2f0eedf4df9fd82242948f55e395 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 9 Mar 2021 10:08:35 +0000 Subject: [PATCH 21/33] fix typo --- .../ClassicDesktopStyleApplicationLifetime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 86f0491dc5..f8606fcd6f 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -104,7 +104,7 @@ namespace Avalonia.Controls.ApplicationLifetimes { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); - if (args.Length > 0)) + if (args.Length > 0) { ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); } From 344c876c44ae23cc97217284ead37a198dc27486 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 9 Mar 2021 13:12:29 +0000 Subject: [PATCH 22/33] add an options class for ClassicDesktopStyleApplicationLifeCycle. --- .../ClassicDesktopStyleApplicationLifetime.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index f8606fcd6f..07a0510f13 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -104,7 +104,9 @@ namespace Avalonia.Controls.ApplicationLifetimes { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); - if (args.Length > 0) + var options = AvaloniaLocator.Current.GetService(); + + if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0) { ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); } @@ -122,6 +124,13 @@ namespace Avalonia.Controls.ApplicationLifetimes _activeLifetime = null; } } + + public class ClassicDesktopStyleApplicationLifetimeOptions + { + public bool ProcessUrlActivationCommandLine { get; set; } + + public bool IsSingleInstance { get; set; } + } } namespace Avalonia From c321127a563140c1114cefc9a518034f909eb5ce Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 10 Mar 2021 11:56:45 +0000 Subject: [PATCH 23/33] remove IsSingleInstance --- .../ClassicDesktopStyleApplicationLifetime.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 07a0510f13..aa4342f075 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -128,8 +128,6 @@ namespace Avalonia.Controls.ApplicationLifetimes public class ClassicDesktopStyleApplicationLifetimeOptions { public bool ProcessUrlActivationCommandLine { get; set; } - - public bool IsSingleInstance { get; set; } } } From 425c3e8fecf0a419b21fa1dbc781008aa73905ee Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 10 Mar 2021 22:20:47 +0000 Subject: [PATCH 24/33] OSX backend when quiting app (closeAll) should only performClose on non-child windows. --- native/Avalonia.Native/src/OSX/window.mm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 7fa6614d4d..4d63c486c4 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1877,7 +1877,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent for(int i = 0; i < numWindows; i++) { - [[windows objectAtIndex:i] performClose:nil]; + auto window = (AvnWindow*)[windows objectAtIndex:i]; + + if([window parentWindow] == nullptr) // Avalonia will handle the child windows. + { + [window performClose:nil]; + } } } From 8decb3bbc66e71a53b8c5a56698ab723c1fc0188 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 10 Mar 2021 17:39:07 +0100 Subject: [PATCH 25/33] fix(Benchmark): FluentBenchmark thrown: 'Avalonia.Markup.Xaml.XamlLoadException' --- tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs index d41665c78c..363a4fabaf 100644 --- a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs @@ -64,9 +64,9 @@ namespace Avalonia.Benchmarks.Themes AssetLoader.RegisterResUriParsers(); return new Styles { - new StyleInclude(new Uri("avares://Avalonia.Benchmarks")) + new Avalonia.Themes.Fluent.FluentTheme(new Uri("avares://Avalonia.Benchmarks")) { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentDark.xaml") + } }; } From 4cb94f351192c634435b5687d39360e9d3350f16 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Fri, 12 Mar 2021 12:53:17 -0500 Subject: [PATCH 26/33] Fixed SharedSizeGroup Layout Bug --- src/Avalonia.Controls/DefinitionBase.cs | 77 ++++++++++++++++++------- 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 38ebbe5bf9..3237f6f37b 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -662,31 +662,64 @@ namespace Avalonia.Controls { DefinitionBase definitionBase = _registry[i]; - if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated) + // we'll set d.UseSharedMinimum to maintain the invariant: + // d.UseSharedMinimum iff d._minSize < this.MinSize + // i.e. iff d is not a "long-pole" definition. + // + // Measure/Arrange of d's Grid uses d._minSize for long-pole + // definitions, and max(d._minSize, shared size) for + // short-pole definitions. This distinction allows us to react + // to changes in "long-pole-ness" more efficiently and correctly, + // by avoiding remeasures when a long-pole definition changes. + bool useSharedMinimum = !MathUtilities.AreClose(definitionBase._minSize, sharedMinSize); + + // before doing that, determine whether d's Grid needs to be remeasured. + // It's important _not_ to remeasure if the last measure is still + // valid, otherwise infinite loops are possible + bool measureIsValid; + + if(!definitionBase.UseSharedMinimum) { - // if definition's min size is different, then need to re-measure - if (!MathUtilities.AreClose(sharedMinSize, definitionBase.MinSize)) - { - Grid parentGrid = (Grid)definitionBase.Parent; - parentGrid.InvalidateMeasure(); - definitionBase.UseSharedMinimum = true; - } - else - { - definitionBase.UseSharedMinimum = false; - - // if measure is valid then also need to check arrange. - // Note: definitionBase.SizeCache is volatile but at this point - // it contains up-to-date final size - if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache)) - { - Grid parentGrid = (Grid)definitionBase.Parent; - parentGrid.InvalidateArrange(); - } - } + // d was a long-pole. measure is valid iff it's still a long-pole, + // since previous measure didn't use shared size. + measureIsValid = !useSharedMinimum; + } + else if(useSharedMinimum) + { + // d was a short-pole, and still is. measure is valid + // iff the shared size didn't change + measureIsValid = !sharedMinSizeChanged; + } + else + { + // d was a short-pole, but is now a long-pole. This can + // happen in several ways: + // a. d's minSize increased to or past the old shared size + // b. other long-pole definitions decreased, leaving + // d as the new winner + // In the former case, the measure is valid - it used + // d's new larger minSize. In the latter case, the + // measure is invalid - it used the old shared size, + // which is larger than d's (possibly changed) minSize + measureIsValid = (definitionBase.LayoutWasUpdated && + MathUtilities.GreaterThanOrClose(definitionBase._minSize, this.MinSize)); + } - definitionBase.LayoutWasUpdated = false; + if(!measureIsValid) + { + definitionBase.Parent.InvalidateMeasure(); } + else if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache)) + { + // if measure is valid then also need to check arrange. + // Note: definitionBase.SizeCache is volatile but at this point + // it contains up-to-date final size + definitionBase.Parent.InvalidateArrange(); + } + + // now we can restore the invariant, and clear the layout flag + definitionBase.UseSharedMinimum = useSharedMinimum; + definitionBase.LayoutWasUpdated = false; } _minSize = sharedMinSize; From b48e7fe432fe9c461d54c3509e6a75845e3d427b Mon Sep 17 00:00:00 2001 From: sdoroff Date: Fri, 12 Mar 2021 13:45:34 -0500 Subject: [PATCH 27/33] Fix Keyboard Handling --- .../DataGridCheckBoxColumn.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs index 179abd4b63..3a3cb3bb68 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs @@ -17,7 +17,6 @@ namespace Avalonia.Controls /// public class DataGridCheckBoxColumn : DataGridBoundColumn { - private bool _beganEditWithKeyboard; private CheckBox _currentCheckBox; private DataGrid _owningGrid; @@ -173,6 +172,7 @@ namespace Avalonia.Controls } else { + System.Diagnostics.Debug.WriteLine($"InitialValue: {editingCheckBox.IsChecked}"); editingCheckBox.IsChecked = !editingCheckBox.IsChecked; } } @@ -209,12 +209,6 @@ namespace Avalonia.Controls ProcessPointerArgs(); } } - else if (_beganEditWithKeyboard) - { - // Editing began by a user pressing spacebar - _beganEditWithKeyboard = false; - EditValue(); - } return uneditedValue; } @@ -308,13 +302,10 @@ namespace Avalonia.Controls CheckBox checkBox = GetCellContent(row) as CheckBox; if (checkBox == _currentCheckBox) { - _beganEditWithKeyboard = true; OwningGrid.BeginEdit(); - return; } } } - _beganEditWithKeyboard = false; } private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e) From 0db62855b48d3baf716d231683b120e1d20b2f1d Mon Sep 17 00:00:00 2001 From: sdoroff Date: Fri, 12 Mar 2021 14:42:07 -0500 Subject: [PATCH 28/33] Remove Print Line --- src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs index 3a3cb3bb68..ccf1f3f77a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs @@ -172,7 +172,6 @@ namespace Avalonia.Controls } else { - System.Diagnostics.Debug.WriteLine($"InitialValue: {editingCheckBox.IsChecked}"); editingCheckBox.IsChecked = !editingCheckBox.IsChecked; } } From 9899fe5e81849363c13ba7f0707830369561d50b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 16 Mar 2021 09:51:52 +0100 Subject: [PATCH 29/33] 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 30/33] 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 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 31/33] 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 32/33] 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 33/33] 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; - } } }