From e6b13b66c50325096bb9ce7fa6fbed787cd5fa2c Mon Sep 17 00:00:00 2001 From: ili Date: Mon, 5 Apr 2021 20:47:31 +0500 Subject: [PATCH 01/16] Support android InputType --- .../ControlCatalog.Android.csproj | 2 +- .../Pages/AutoCompleteBoxPage.xaml | 3 +- samples/ControlCatalog/Pages/TextBoxPage.xaml | 2 +- .../Avalonia.Android/AvaloniaActivity.cs | 6 +- src/Android/Avalonia.Android/AvaloniaView.cs | 26 +++++++ .../SkiaPlatform/AndroidFramebuffer.cs | 2 +- .../InvalidationAwareSurfaceView.cs | 67 ++++++++++++++++++- .../Platform/SkiaPlatform/TopLevelImpl.cs | 23 ++++--- .../Helpers/AndroidKeyboardEventsHelper.cs | 56 +++++++++++----- .../Platform/Specific/IAndroidView.cs | 10 +++ src/Avalonia.Controls/TextBox.cs | 9 ++- src/Avalonia.Input/ISoftInputElement.cs | 11 +++ src/Avalonia.Input/InputType.cs | 32 +++++++++ 13 files changed, 214 insertions(+), 35 deletions(-) create mode 100644 src/Avalonia.Input/ISoftInputElement.cs create mode 100644 src/Avalonia.Input/InputType.cs diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 1a68c4d732..9da9627a6f 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -158,4 +158,4 @@ - + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index 1a53217842..363330be86 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -14,7 +14,8 @@ + MinimumPrefixLength="1" + /> - + - View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e); + //public override bool DispatchKeyEvent(KeyEvent e) => + // View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e); } } diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 8de3657283..f78f383041 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -2,6 +2,8 @@ using System; using Android.Content; using Android.Runtime; using Android.Views; +using Android.Views.Accessibility; +using Android.Views.InputMethods; using Android.Widget; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; @@ -23,6 +25,9 @@ namespace Avalonia.Android AddView(_view.View); _root = new EmbeddableControlRoot(_view); _root.Prepare(); + + Focusable = true; + FocusableInTouchMode = true; } public object Content @@ -89,5 +94,26 @@ namespace Avalonia.Android public WindowState WindowState { get; set; } public IDisposable ShowDialog() => null; } + + public override void OnInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) + { + base.OnInitializeAccessibilityNodeInfo(info); + } + + public override AccessibilityNodeInfo CreateAccessibilityNodeInfo() + { + return base.CreateAccessibilityNodeInfo(); + } + + public override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) + { + outAttrs.InputType = global::Android.Text.InputTypes.ClassNumber; + return base.OnCreateInputConnection(outAttrs); + } + + public override bool OnCheckIsTextEditor() + { + return true; + } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs index b115917622..aabf8160f8 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs @@ -32,7 +32,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4); Address = buffer.bits; - Dpi = scaling * new Vector(96, 96); + Dpi = new Vector(96, 96) * scaling; } public void Dispose() diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 16c5bdae3d..a1c2b2d9fb 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -2,23 +2,31 @@ using System; using Android.Content; using Android.Graphics; using Android.OS; +using Android.Runtime; using Android.Util; using Android.Views; +using Android.Views.Accessibility; +using Android.Views.InputMethods; +using Avalonia.Android.Platform.Specific; +using Avalonia.Input; using Avalonia.Platform; namespace Avalonia.Android { - public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformHandle + public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformHandle, IAndroidSoftInput { bool _invalidateQueued; + private ISoftInputElement _softInputElement; readonly object _lock = new object(); private readonly Handler _handler; - public InvalidationAwareSurfaceView(Context context) : base(context) { Holder.AddCallback(this); _handler = new Handler(context.MainLooper); + + Focusable = true; + FocusableInTouchMode = true; } public override void Invalidate() @@ -83,5 +91,60 @@ namespace Avalonia.Android } protected abstract void Draw(); public string HandleDescriptor => "SurfaceView"; + + public override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) + { + outAttrs.InputType = _softInputElement.InputType switch + { + InputType.Numeric => global::Android.Text.InputTypes.ClassNumber, + InputType.Phone => global::Android.Text.InputTypes.ClassPhone, + _ => global::Android.Text.InputTypes.Null + }; + + return base.OnCreateInputConnection(outAttrs); + } + + + public override bool CheckInputConnectionProxy(View view) + { + return base.CheckInputConnectionProxy(view); + } + + public override void OnInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) + { + base.OnInitializeAccessibilityNodeInfo(info); + } + + public override bool OnCheckIsTextEditor() + { + return true; + } + + public void ShowSoftInput(ISoftInputElement softInputElement) + { + var input = Context.GetSystemService(Context.InputMethodService).JavaCast(); + var previousSoftInput = _softInputElement; + _softInputElement = softInputElement; + + if (_softInputElement.InputType == InputType.None) + HideSoftInput(); + else + { + RequestFocus(); + + if (!ReferenceEquals(_softInputElement, previousSoftInput)) + { + input.RestartInput(this); + } + + input.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.NotAlways); + } + } + + public void HideSoftInput() + { + var input = Context.GetSystemService(Context.InputMethodService).JavaCast(); + input.HideSoftInputFromWindow(WindowToken, HideSoftInputFlags.None); + } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index fe237a1719..219dd4cf67 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -19,7 +19,7 @@ using Avalonia.Rendering; namespace Avalonia.Android.Platform.SkiaPlatform { - class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo + class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, IAndroidSoftInput { private readonly IGlPlatformSurface _gl; private readonly IFramebufferPlatformSurface _framebuffer; @@ -43,18 +43,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); + + _keyboardHelper.ActivateAutoShowKeyboard(); } - private bool _handleEvents; public bool HandleEvents { - get { return _handleEvents; } - set - { - _handleEvents = value; - _keyboardHelper.HandleEvents = _handleEvents; - } + get { return _keyboardHelper.HandleEvents; } + set { _keyboardHelper.HandleEvents = value; } } public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => @@ -213,5 +210,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform { throw new NotImplementedException(); } + + void IAndroidSoftInput.ShowSoftInput(ISoftInputElement softInputElement) + { + _view.ShowSoftInput(softInputElement); + } + + void IAndroidSoftInput.HideSoftInput() + { + _view.HideSoftInput(); + } } } diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs index 426b221738..264c9f4cb5 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs @@ -1,9 +1,11 @@ using System; using System.ComponentModel; +using Android.App; using Android.Content; using Android.Runtime; using Android.Views; using Android.Views.InputMethods; +using Android.Widget; using Avalonia.Android.Platform.Input; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; @@ -12,7 +14,7 @@ using Avalonia.Input.Raw; namespace Avalonia.Android.Platform.Specific.Helpers { - internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView + internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView, IAndroidSoftInput { private TView _view; private IInputElement _lastFocusedElement; @@ -36,9 +38,20 @@ namespace Avalonia.Android.Platform.Specific.Helpers return DispatchKeyEventInternal(e, out callBase); } + string? UnicodeTextInput(KeyEvent keyEvent) + { + return keyEvent.Action == KeyEventActions.Multiple + && keyEvent.RepeatCount == 0 + && !string.IsNullOrEmpty(keyEvent?.Characters) + ? keyEvent.Characters + : null; + } + private bool? DispatchKeyEventInternal(KeyEvent e, out bool callBase) { - if (e.Action == KeyEventActions.Multiple) + var unicodeTextInput = UnicodeTextInput(e); + + if (e.Action == KeyEventActions.Multiple && unicodeTextInput == null) { callBase = true; return null; @@ -53,13 +66,14 @@ namespace Avalonia.Android.Platform.Specific.Helpers _view.Input(rawKeyEvent); - if (e.Action == KeyEventActions.Down && e.UnicodeChar >= 32) + if ((e.Action == KeyEventActions.Down && e.UnicodeChar >= 32) + || unicodeTextInput != null) { var rawTextEvent = new RawTextInputEventArgs( AndroidKeyboardDevice.Instance, Convert.ToUInt32(e.EventTime), _view.InputRoot, - Convert.ToChar(e.UnicodeChar).ToString() + unicodeTextInput ?? Convert.ToChar(e.UnicodeChar).ToString() ); _view.Input(rawTextEvent); } @@ -88,41 +102,49 @@ namespace Avalonia.Android.Platform.Specific.Helpers private bool NeedsKeyboard(IInputElement element) { //may be some other elements - return element is TextBox; + return element is ISoftInputElement; } - private void TryShowHideKeyboard(IInputElement element, bool value) + private void TryShowHideKeyboard(ISoftInputElement element, bool value) { - var input = _view.View.Context.GetSystemService(Context.InputMethodService).JavaCast(); - if (value) { - //show keyboard - //may be in the future different keyboards support e.g. normal, only digits etc. - //Android.Text.InputTypes - input.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.ImplicitOnly); + _view.ShowSoftInput(element); } else { - //hide keyboard - input.HideSoftInputFromWindow(_view.View.WindowToken, HideSoftInputFlags.None); + _view.HideSoftInput(); } } public void UpdateKeyboardState(IInputElement element) { - var focusedElement = element; - bool oldValue = NeedsKeyboard(_lastFocusedElement); - bool newValue = NeedsKeyboard(focusedElement); + var focusedElement = element as ISoftInputElement; + var lastElement = _lastFocusedElement as ISoftInputElement; + + bool oldValue = lastElement?.InputType > InputType.None; + bool newValue = focusedElement?.InputType > InputType.None; if (newValue != oldValue || newValue) { + if (_lastFocusedElement != null) + _lastFocusedElement.PointerReleased -= RestoreSoftKeyboard; + TryShowHideKeyboard(focusedElement, newValue); + + if (newValue && focusedElement != null) + element.PointerReleased += RestoreSoftKeyboard; } _lastFocusedElement = element; } + private void RestoreSoftKeyboard(object sender, PointerReleasedEventArgs e) + { + if (_lastFocusedElement is ISoftInputElement softInputElement && softInputElement.InputType != InputType.None) + TryShowHideKeyboard(softInputElement, true); + } + public void ActivateAutoShowKeyboard() { var kbDevice = (KeyboardDevice.Instance as INotifyPropertyChanged); diff --git a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs index c72de8e197..db65a1335b 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs @@ -1,9 +1,19 @@ using Android.Views; +using Avalonia.Input; namespace Avalonia.Android.Platform.Specific { public interface IAndroidView { View View { get; } + + + } + + public interface IAndroidSoftInput + { + void ShowSoftInput(ISoftInputElement softInputElement); + + void HideSoftInput(); } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1d75f08a41..06729fa990 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -18,7 +18,7 @@ using Avalonia.Controls.Metadata; namespace Avalonia.Controls { [PseudoClasses(":empty")] - public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost + public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost, ISoftInputElement { public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current .GetService()?.Cut.FirstOrDefault(); @@ -130,6 +130,11 @@ namespace Avalonia.Controls nameof(CanPaste), o => o.CanPaste); + public static readonly DirectProperty InputTypeProperty = + AvaloniaProperty.RegisterDirect( + nameof(InputType), + o => o.InputType); + struct UndoRedoState : IEquatable { public string Text { get; } @@ -1243,5 +1248,7 @@ namespace Avalonia.Controls ClearSelection(); } } + + public InputType InputType { get; set; } = InputType.Text; } } diff --git a/src/Avalonia.Input/ISoftInputElement.cs b/src/Avalonia.Input/ISoftInputElement.cs new file mode 100644 index 0000000000..ed5e4e59e3 --- /dev/null +++ b/src/Avalonia.Input/ISoftInputElement.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Input +{ + public interface ISoftInputElement + { + InputType InputType { get; } + } +} diff --git a/src/Avalonia.Input/InputType.cs b/src/Avalonia.Input/InputType.cs new file mode 100644 index 0000000000..893f9dcc39 --- /dev/null +++ b/src/Avalonia.Input/InputType.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Input +{ + /// + /// Input type enumeration + /// + public enum InputType + { + /// + /// Do not use input + /// + None, + + /// + /// User full text input + /// + Text, + + /// + /// Use numeric text input + /// + Numeric, + + /// + /// Use phone input + /// + Phone + } +} From 2ca903631e95d73d3b9f76749a4b1db83fc45c84 Mon Sep 17 00:00:00 2001 From: ili Date: Mon, 5 Apr 2021 21:14:48 +0500 Subject: [PATCH 02/16] Move keyboard logic back to the helper --- .../Avalonia.Android/IInitEditorInfo.cs | 12 +++++ .../InvalidationAwareSurfaceView.cs | 48 +++++-------------- .../Platform/SkiaPlatform/TopLevelImpl.cs | 14 ++---- .../Helpers/AndroidKeyboardEventsHelper.cs | 27 +++++++++-- .../Platform/Specific/IAndroidView.cs | 7 --- 5 files changed, 52 insertions(+), 56 deletions(-) create mode 100644 src/Android/Avalonia.Android/IInitEditorInfo.cs diff --git a/src/Android/Avalonia.Android/IInitEditorInfo.cs b/src/Android/Avalonia.Android/IInitEditorInfo.cs new file mode 100644 index 0000000000..98fc4eafc1 --- /dev/null +++ b/src/Android/Avalonia.Android/IInitEditorInfo.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Android.Views.InputMethods; + +namespace Avalonia.Android +{ + interface IInitEditorInfo + { + void InitEditorInfo(Action init); + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index a1c2b2d9fb..a43861d4e7 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -13,7 +13,7 @@ using Avalonia.Platform; namespace Avalonia.Android { - public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformHandle, IAndroidSoftInput + public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformHandle, IInitEditorInfo { bool _invalidateQueued; private ISoftInputElement _softInputElement; @@ -92,14 +92,19 @@ namespace Avalonia.Android protected abstract void Draw(); public string HandleDescriptor => "SurfaceView"; - public override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) + private Action _initEditorInfo; + + public void InitEditorInfo(Action init) { - outAttrs.InputType = _softInputElement.InputType switch - { - InputType.Numeric => global::Android.Text.InputTypes.ClassNumber, - InputType.Phone => global::Android.Text.InputTypes.ClassPhone, - _ => global::Android.Text.InputTypes.Null - }; + _initEditorInfo = init; + } + + public sealed override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) + { + if (_initEditorInfo == null) + throw new InvalidOperationException("Call IInitEditorInfo.InitEditorInfo first"); + + _initEditorInfo(outAttrs); return base.OnCreateInputConnection(outAttrs); } @@ -119,32 +124,5 @@ namespace Avalonia.Android { return true; } - - public void ShowSoftInput(ISoftInputElement softInputElement) - { - var input = Context.GetSystemService(Context.InputMethodService).JavaCast(); - var previousSoftInput = _softInputElement; - _softInputElement = softInputElement; - - if (_softInputElement.InputType == InputType.None) - HideSoftInput(); - else - { - RequestFocus(); - - if (!ReferenceEquals(_softInputElement, previousSoftInput)) - { - input.RestartInput(this); - } - - input.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.NotAlways); - } - } - - public void HideSoftInput() - { - var input = Context.GetSystemService(Context.InputMethodService).JavaCast(); - input.HideSoftInputFromWindow(WindowToken, HideSoftInputFlags.None); - } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 219dd4cf67..bd9a5868c0 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -4,7 +4,7 @@ using Android.Content; using Android.Graphics; using Android.Runtime; using Android.Views; - +using Android.Views.InputMethods; using Avalonia.Android.OpenGL; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; @@ -19,7 +19,7 @@ using Avalonia.Rendering; namespace Avalonia.Android.Platform.SkiaPlatform { - class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, IAndroidSoftInput + class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, IInitEditorInfo { private readonly IGlPlatformSurface _gl; private readonly IFramebufferPlatformSurface _framebuffer; @@ -210,15 +210,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform { throw new NotImplementedException(); } - - void IAndroidSoftInput.ShowSoftInput(ISoftInputElement softInputElement) - { - _view.ShowSoftInput(softInputElement); - } - - void IAndroidSoftInput.HideSoftInput() + public void InitEditorInfo(Action init) { - _view.HideSoftInput(); + _view.InitEditorInfo(init); } } } diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs index 264c9f4cb5..e98a8f183e 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs @@ -14,7 +14,7 @@ using Avalonia.Input.Raw; namespace Avalonia.Android.Platform.Specific.Helpers { - internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView, IAndroidSoftInput + internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView, IInitEditorInfo { private TView _view; private IInputElement _lastFocusedElement; @@ -107,13 +107,32 @@ namespace Avalonia.Android.Platform.Specific.Helpers private void TryShowHideKeyboard(ISoftInputElement element, bool value) { - if (value) + _view.InitEditorInfo((outAttrs) => { - _view.ShowSoftInput(element); + outAttrs.InputType = element.InputType switch + { + InputType.Numeric => global::Android.Text.InputTypes.ClassNumber, + InputType.Phone => global::Android.Text.InputTypes.ClassPhone, + _ => global::Android.Text.InputTypes.Null + }; + }); + + var input = _view.View.Context.GetSystemService(Context.InputMethodService).JavaCast(); + + if (value && element != null && element.InputType != InputType.None) + { + _view.View.RequestFocus(); + + if (!ReferenceEquals(_lastFocusedElement, element)) + { + input.RestartInput(_view.View); + } + + input.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.NotAlways); } else { - _view.HideSoftInput(); + input.HideSoftInputFromWindow(_view.View.WindowToken, HideSoftInputFlags.None); } } diff --git a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs index db65a1335b..1f99051fe6 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs @@ -9,11 +9,4 @@ namespace Avalonia.Android.Platform.Specific } - - public interface IAndroidSoftInput - { - void ShowSoftInput(ISoftInputElement softInputElement); - - void HideSoftInput(); - } } From 78859b88294897dced36ccda030722f47d09652a Mon Sep 17 00:00:00 2001 From: ili Date: Wed, 7 Apr 2021 18:54:12 +0500 Subject: [PATCH 03/16] Tech: working on Avalonia.Input support --- global.json | 2 +- .../Avalonia.Android/AndroidInputMethod.cs | 50 +++++++++++++++++++ .../InvalidationAwareSurfaceView.cs | 20 +------- .../Platform/SkiaPlatform/TopLevelImpl.cs | 35 +++++++++++-- .../Helpers/AndroidKeyboardEventsHelper.cs | 3 +- 5 files changed, 85 insertions(+), 25 deletions(-) create mode 100644 src/Android/Avalonia.Android/AndroidInputMethod.cs diff --git a/global.json b/global.json index b2b2da7c4f..351c7c7e1e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.401" + "version": "3.1.407" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs new file mode 100644 index 0000000000..fb6f5a0562 --- /dev/null +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Android.Content; +using Android.Runtime; +using Android.Views; +using Android.Views.InputMethods; +using Avalonia.Input.TextInput; + +namespace Avalonia.Android +{ + class AndroidInputMethod : ITextInputMethodImpl + where TView: View, IInitEditorInfo + { + private readonly TView _host; + private readonly InputMethodManager _imm; + + public AndroidInputMethod(TView host) + { + if (host.OnCheckIsTextEditor() == false) + throw new InvalidOperationException("Host should return true from OnCheckIsTextEditor()"); + + _host = host; + _imm = host.Context.GetSystemService(Context.InputMethodService).JavaCast(); + + _host.Focusable = true; + _host.FocusableInTouchMode = true; + } + + public void Reset() + { + _imm.RestartInput(_host); + } + + public void SetActive(bool active) + { + if (active) + _host.RequestFocus(); + } + + public void SetCursorRect(Rect rect) + { + } + + public void SetOptions(TextInputOptionsQueryEventArgs options) + { + //throw new NotImplementedException(); + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index a43861d4e7..3444193140 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -13,7 +13,7 @@ using Avalonia.Platform; namespace Avalonia.Android { - public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformHandle, IInitEditorInfo + public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformHandle { bool _invalidateQueued; private ISoftInputElement _softInputElement; @@ -92,24 +92,6 @@ namespace Avalonia.Android protected abstract void Draw(); public string HandleDescriptor => "SurfaceView"; - private Action _initEditorInfo; - - public void InitEditorInfo(Action init) - { - _initEditorInfo = init; - } - - public sealed override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) - { - if (_initEditorInfo == null) - throw new InvalidOperationException("Call IInitEditorInfo.InitEditorInfo first"); - - _initEditorInfo(outAttrs); - - return base.OnCreateInputConnection(outAttrs); - } - - public override bool CheckInputConnectionProxy(View view) { return base.CheckInputConnectionProxy(view); diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index bd9a5868c0..dc9932b38c 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -9,9 +9,11 @@ using Avalonia.Android.OpenGL; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; @@ -19,19 +21,20 @@ using Avalonia.Rendering; namespace Avalonia.Android.Platform.SkiaPlatform { - class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, IInitEditorInfo + class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod { private readonly IGlPlatformSurface _gl; private readonly IFramebufferPlatformSurface _framebuffer; private readonly AndroidKeyboardEventsHelper _keyboardHelper; private readonly AndroidTouchEventsHelper _touchHelper; - + private readonly ITextInputMethodImpl _textInputMethod; private ViewImpl _view; public TopLevelImpl(Context context, bool placeOnTop = false) { _view = new ViewImpl(context, this, placeOnTop); + _textInputMethod = new AndroidInputMethod(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(this); _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, GetAvaloniaPointFromEvent); @@ -47,7 +50,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform _keyboardHelper.ActivateAutoShowKeyboard(); } - public bool HandleEvents { get { return _keyboardHelper.HandleEvents; } @@ -141,7 +143,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform Resized?.Invoke(size); } - class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback + class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo { private readonly TopLevelImpl _tl; private Size _oldSize; @@ -188,6 +190,29 @@ namespace Avalonia.Android.Platform.SkiaPlatform base.SurfaceChanged(holder, format, width, height); } + + public sealed override bool OnCheckIsTextEditor() + { + return true; + } + + private Action _initEditorInfo; + + public void InitEditorInfo(Action init) + { + _initEditorInfo = init; + } + + public sealed override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) + { + if (_initEditorInfo == null) + throw new InvalidOperationException("Call IInitEditorInfo.InitEditorInfo first"); + + _initEditorInfo(outAttrs); + + return base.OnCreateInputConnection(outAttrs); + } + } public IPopupImpl CreatePopup() => null; @@ -206,6 +231,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform public double Scaling => RenderScaling; + public ITextInputMethodImpl TextInputMethod => _textInputMethod; + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { throw new NotImplementedException(); diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs index e98a8f183e..db864c0ee0 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs @@ -9,12 +9,13 @@ using Android.Widget; using Avalonia.Android.Platform.Input; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; namespace Avalonia.Android.Platform.Specific.Helpers { - internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView, IInitEditorInfo + internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView, ITopLevelImplWithTextInputMethod { private TView _view; private IInputElement _lastFocusedElement; From 12c2c16d13b73bb419a1585af5df0d6607a4af76 Mon Sep 17 00:00:00 2001 From: ili Date: Wed, 7 Apr 2021 22:13:57 +0500 Subject: [PATCH 04/16] Android soft input using Avalonia.Input --- global.json | 2 +- samples/ControlCatalog/Pages/TextBoxPage.xaml | 2 +- .../ControlCatalog/Pages/TextBoxPage.xaml.cs | 6 ++ .../Avalonia.Android/AndroidInputMethod.cs | 47 ++++++++++- .../InvalidationAwareSurfaceView.cs | 1 - .../Platform/SkiaPlatform/TopLevelImpl.cs | 6 +- .../Helpers/AndroidKeyboardEventsHelper.cs | 78 ++----------------- .../Platform/Specific/IAndroidView.cs | 2 - src/Avalonia.Controls/TextBox.cs | 9 +-- src/Avalonia.Input/ISoftInputElement.cs | 11 --- src/Avalonia.Input/InputType.cs | 32 -------- 11 files changed, 64 insertions(+), 132 deletions(-) delete mode 100644 src/Avalonia.Input/ISoftInputElement.cs delete mode 100644 src/Avalonia.Input/InputType.cs diff --git a/global.json b/global.json index 351c7c7e1e..b2b2da7c4f 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.407" + "version": "3.1.401" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index a3ba9d0eb3..1ac447ea69 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -13,7 +13,7 @@ - + ("numericWatermark") + .TextInputOptionsQuery += (s, a) => + { + a.ContentType = Avalonia.Input.TextInput.TextInputContentType.Number; + }; } } } diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index fb6f5a0562..a3e6b6ee88 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -5,6 +5,8 @@ using Android.Content; using Android.Runtime; using Android.Views; using Android.Views.InputMethods; +using Avalonia.Controls.Platform; +using Avalonia.Input; using Avalonia.Input.TextInput; namespace Avalonia.Android @@ -14,6 +16,7 @@ namespace Avalonia.Android { private readonly TView _host; private readonly InputMethodManager _imm; + private IInputElement _inputElement; public AndroidInputMethod(TView host) { @@ -44,7 +47,49 @@ namespace Avalonia.Android public void SetOptions(TextInputOptionsQueryEventArgs options) { - //throw new NotImplementedException(); + if (_inputElement != null) + { + _inputElement.PointerReleased -= RestoreSoftKeyboard; + } + + _inputElement = options.Source as InputElement; + + if (_inputElement == null) + { + _imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None); + } + + _host.InitEditorInfo((outAttrs) => + { + outAttrs.InputType = options.ContentType switch + { + TextInputContentType.Email => global::Android.Text.InputTypes.TextVariationEmailAddress, + TextInputContentType.Number => global::Android.Text.InputTypes.ClassNumber, + TextInputContentType.Password => global::Android.Text.InputTypes.TextVariationPassword, + TextInputContentType.Phone => global::Android.Text.InputTypes.ClassPhone, + TextInputContentType.Url => global::Android.Text.InputTypes.TextVariationUri, + _ => global::Android.Text.InputTypes.Null + }; + + if (options.AutoCapitalization) + { + outAttrs.InitialCapsMode = global::Android.Text.CapitalizationMode.Sentences; + outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagCapSentences; + } + + if (options.Multiline) + outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagMultiLine; + }); + + Reset(); + _inputElement.PointerReleased += RestoreSoftKeyboard; + RestoreSoftKeyboard(null, null); + } + + private void RestoreSoftKeyboard(object sender, PointerReleasedEventArgs e) + { + //_imm.ToggleSoftInput(ShowFlags.Implicit, HideSoftInputFlags.NotAlways); + _imm.ShowSoftInput(_host, ShowFlags.Implicit); } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 3444193140..4267082a4f 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -16,7 +16,6 @@ namespace Avalonia.Android public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformHandle { bool _invalidateQueued; - private ISoftInputElement _softInputElement; readonly object _lock = new object(); private readonly Handler _handler; diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index dc9932b38c..a4bc8c4b76 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -35,7 +35,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform { _view = new ViewImpl(context, this, placeOnTop); _textInputMethod = new AndroidInputMethod(_view); - _keyboardHelper = new AndroidKeyboardEventsHelper(this); + _keyboardHelper = new AndroidKeyboardEventsHelper(this, _textInputMethod); _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, GetAvaloniaPointFromEvent); @@ -237,9 +237,5 @@ namespace Avalonia.Android.Platform.SkiaPlatform { throw new NotImplementedException(); } - public void InitEditorInfo(Action init) - { - _view.InitEditorInfo(init); - } } } diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs index db864c0ee0..d79417a9f1 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs @@ -12,19 +12,22 @@ using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; namespace Avalonia.Android.Platform.Specific.Helpers { - internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView, ITopLevelImplWithTextInputMethod + internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView { - private TView _view; + private readonly TView _view; + private readonly ITextInputMethodImpl _textInpuMethod; private IInputElement _lastFocusedElement; public bool HandleEvents { get; set; } - public AndroidKeyboardEventsHelper(TView view) + public AndroidKeyboardEventsHelper(TView view, ITextInputMethodImpl androidTextInput) { - this._view = view; + _view = view; + _textInpuMethod = androidTextInput; HandleEvents = true; } @@ -100,71 +103,6 @@ namespace Avalonia.Android.Platform.Specific.Helpers return rv; } - private bool NeedsKeyboard(IInputElement element) - { - //may be some other elements - return element is ISoftInputElement; - } - - private void TryShowHideKeyboard(ISoftInputElement element, bool value) - { - _view.InitEditorInfo((outAttrs) => - { - outAttrs.InputType = element.InputType switch - { - InputType.Numeric => global::Android.Text.InputTypes.ClassNumber, - InputType.Phone => global::Android.Text.InputTypes.ClassPhone, - _ => global::Android.Text.InputTypes.Null - }; - }); - - var input = _view.View.Context.GetSystemService(Context.InputMethodService).JavaCast(); - - if (value && element != null && element.InputType != InputType.None) - { - _view.View.RequestFocus(); - - if (!ReferenceEquals(_lastFocusedElement, element)) - { - input.RestartInput(_view.View); - } - - input.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.NotAlways); - } - else - { - input.HideSoftInputFromWindow(_view.View.WindowToken, HideSoftInputFlags.None); - } - } - - public void UpdateKeyboardState(IInputElement element) - { - var focusedElement = element as ISoftInputElement; - var lastElement = _lastFocusedElement as ISoftInputElement; - - bool oldValue = lastElement?.InputType > InputType.None; - bool newValue = focusedElement?.InputType > InputType.None; - - if (newValue != oldValue || newValue) - { - if (_lastFocusedElement != null) - _lastFocusedElement.PointerReleased -= RestoreSoftKeyboard; - - TryShowHideKeyboard(focusedElement, newValue); - - if (newValue && focusedElement != null) - element.PointerReleased += RestoreSoftKeyboard; - } - - _lastFocusedElement = element; - } - - private void RestoreSoftKeyboard(object sender, PointerReleasedEventArgs e) - { - if (_lastFocusedElement is ISoftInputElement softInputElement && softInputElement.InputType != InputType.None) - TryShowHideKeyboard(softInputElement, true); - } - public void ActivateAutoShowKeyboard() { var kbDevice = (KeyboardDevice.Instance as INotifyPropertyChanged); @@ -178,7 +116,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers { if (e.PropertyName == nameof(KeyboardDevice.FocusedElement)) { - UpdateKeyboardState(KeyboardDevice.Instance.FocusedElement); + //UpdateKeyboardState(KeyboardDevice.Instance.FocusedElement); } } diff --git a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs index 1f99051fe6..662792a4df 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs @@ -6,7 +6,5 @@ namespace Avalonia.Android.Platform.Specific public interface IAndroidView { View View { get; } - - } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 06729fa990..1d75f08a41 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -18,7 +18,7 @@ using Avalonia.Controls.Metadata; namespace Avalonia.Controls { [PseudoClasses(":empty")] - public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost, ISoftInputElement + public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost { public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current .GetService()?.Cut.FirstOrDefault(); @@ -130,11 +130,6 @@ namespace Avalonia.Controls nameof(CanPaste), o => o.CanPaste); - public static readonly DirectProperty InputTypeProperty = - AvaloniaProperty.RegisterDirect( - nameof(InputType), - o => o.InputType); - struct UndoRedoState : IEquatable { public string Text { get; } @@ -1248,7 +1243,5 @@ namespace Avalonia.Controls ClearSelection(); } } - - public InputType InputType { get; set; } = InputType.Text; } } diff --git a/src/Avalonia.Input/ISoftInputElement.cs b/src/Avalonia.Input/ISoftInputElement.cs deleted file mode 100644 index ed5e4e59e3..0000000000 --- a/src/Avalonia.Input/ISoftInputElement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Input -{ - public interface ISoftInputElement - { - InputType InputType { get; } - } -} diff --git a/src/Avalonia.Input/InputType.cs b/src/Avalonia.Input/InputType.cs deleted file mode 100644 index 893f9dcc39..0000000000 --- a/src/Avalonia.Input/InputType.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Input -{ - /// - /// Input type enumeration - /// - public enum InputType - { - /// - /// Do not use input - /// - None, - - /// - /// User full text input - /// - Text, - - /// - /// Use numeric text input - /// - Numeric, - - /// - /// Use phone input - /// - Phone - } -} From 42ad65167cceacc193dddfeda3eef4c1bdee59a7 Mon Sep 17 00:00:00 2001 From: ili Date: Wed, 7 Apr 2021 22:37:04 +0500 Subject: [PATCH 05/16] Cleanup --- .../Avalonia.Android/AndroidInputMethod.cs | 12 +++++----- .../Platform/SkiaPlatform/TopLevelImpl.cs | 10 +-------- .../Helpers/AndroidKeyboardEventsHelper.cs | 22 +------------------ 3 files changed, 8 insertions(+), 36 deletions(-) diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index a3e6b6ee88..efb48158bc 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -1,11 +1,8 @@ using System; -using System.Collections.Generic; -using System.Text; using Android.Content; using Android.Runtime; using Android.Views; using Android.Views.InputMethods; -using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.TextInput; @@ -38,7 +35,13 @@ namespace Avalonia.Android public void SetActive(bool active) { if (active) + { _host.RequestFocus(); + Reset(); + _imm.ShowSoftInput(_host, ShowFlags.Implicit); + } + else + _imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None); } public void SetCursorRect(Rect rect) @@ -81,14 +84,11 @@ namespace Avalonia.Android outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagMultiLine; }); - Reset(); _inputElement.PointerReleased += RestoreSoftKeyboard; - RestoreSoftKeyboard(null, null); } private void RestoreSoftKeyboard(object sender, PointerReleasedEventArgs e) { - //_imm.ToggleSoftInput(ShowFlags.Implicit, HideSoftInputFlags.NotAlways); _imm.ShowSoftInput(_host, ShowFlags.Implicit); } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index a4bc8c4b76..0b6dcba1ef 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -35,7 +35,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform { _view = new ViewImpl(context, this, placeOnTop); _textInputMethod = new AndroidInputMethod(_view); - _keyboardHelper = new AndroidKeyboardEventsHelper(this, _textInputMethod); + _keyboardHelper = new AndroidKeyboardEventsHelper(this); _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, GetAvaloniaPointFromEvent); @@ -46,14 +46,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); - - _keyboardHelper.ActivateAutoShowKeyboard(); - } - - public bool HandleEvents - { - get { return _keyboardHelper.HandleEvents; } - set { _keyboardHelper.HandleEvents = value; } } public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs index d79417a9f1..c7821d495e 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs @@ -19,15 +19,12 @@ namespace Avalonia.Android.Platform.Specific.Helpers internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView { private readonly TView _view; - private readonly ITextInputMethodImpl _textInpuMethod; - private IInputElement _lastFocusedElement; public bool HandleEvents { get; set; } - public AndroidKeyboardEventsHelper(TView view, ITextInputMethodImpl androidTextInput) + public AndroidKeyboardEventsHelper(TView view) { _view = view; - _textInpuMethod = androidTextInput; HandleEvents = true; } @@ -103,23 +100,6 @@ namespace Avalonia.Android.Platform.Specific.Helpers return rv; } - public void ActivateAutoShowKeyboard() - { - var kbDevice = (KeyboardDevice.Instance as INotifyPropertyChanged); - - //just in case we've called more than once the method - kbDevice.PropertyChanged -= KeyboardDevice_PropertyChanged; - kbDevice.PropertyChanged += KeyboardDevice_PropertyChanged; - } - - private void KeyboardDevice_PropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(KeyboardDevice.FocusedElement)) - { - //UpdateKeyboardState(KeyboardDevice.Instance.FocusedElement); - } - } - public void Dispose() { HandleEvents = false; From 494d3d26cdf671ebbca9a0216471ca47ebd5b61f Mon Sep 17 00:00:00 2001 From: ili Date: Wed, 7 Apr 2021 22:45:55 +0500 Subject: [PATCH 06/16] Cleanup --- .../SkiaPlatform/InvalidationAwareSurfaceView.cs | 15 --------------- .../Platform/Specific/IAndroidView.cs | 1 - 2 files changed, 16 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 4267082a4f..07f42e7e9f 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -90,20 +90,5 @@ namespace Avalonia.Android } protected abstract void Draw(); public string HandleDescriptor => "SurfaceView"; - - public override bool CheckInputConnectionProxy(View view) - { - return base.CheckInputConnectionProxy(view); - } - - public override void OnInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) - { - base.OnInitializeAccessibilityNodeInfo(info); - } - - public override bool OnCheckIsTextEditor() - { - return true; - } } } diff --git a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs index 662792a4df..c72de8e197 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs @@ -1,5 +1,4 @@ using Android.Views; -using Avalonia.Input; namespace Avalonia.Android.Platform.Specific { From 5f491bc7d6dfc9daa8134e4bd3711c90508a6749 Mon Sep 17 00:00:00 2001 From: ili Date: Wed, 7 Apr 2021 23:07:32 +0500 Subject: [PATCH 07/16] Cleanup --- .../ControlCatalog.Android.csproj | 2 +- .../Pages/AutoCompleteBoxPage.xaml | 3 +-- src/Android/Avalonia.Android/AvaloniaView.cs | 21 ------------------- 3 files changed, 2 insertions(+), 24 deletions(-) diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 9da9627a6f..1a68c4d732 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -158,4 +158,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index 363330be86..1a53217842 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -14,8 +14,7 @@ + MinimumPrefixLength="1"/> null; } - - public override void OnInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) - { - base.OnInitializeAccessibilityNodeInfo(info); - } - - public override AccessibilityNodeInfo CreateAccessibilityNodeInfo() - { - return base.CreateAccessibilityNodeInfo(); - } - - public override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) - { - outAttrs.InputType = global::Android.Text.InputTypes.ClassNumber; - return base.OnCreateInputConnection(outAttrs); - } - - public override bool OnCheckIsTextEditor() - { - return true; - } } } From 244aaf596f570ce249541f8f3b0c3bb82541aca8 Mon Sep 17 00:00:00 2001 From: ili Date: Wed, 7 Apr 2021 23:13:38 +0500 Subject: [PATCH 08/16] Cleanup --- src/Android/Avalonia.Android/AvaloniaView.cs | 5 ----- .../SkiaPlatform/InvalidationAwareSurfaceView.cs | 8 -------- .../Specific/Helpers/AndroidKeyboardEventsHelper.cs | 9 --------- 3 files changed, 22 deletions(-) diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 5be72bb0f4..8de3657283 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -2,8 +2,6 @@ using System; using Android.Content; using Android.Runtime; using Android.Views; -using Android.Views.Accessibility; -using Android.Views.InputMethods; using Android.Widget; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; @@ -25,9 +23,6 @@ namespace Avalonia.Android AddView(_view.View); _root = new EmbeddableControlRoot(_view); _root.Prepare(); - - Focusable = true; - FocusableInTouchMode = true; } public object Content diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 07f42e7e9f..bc62bcde65 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -2,13 +2,8 @@ using System; using Android.Content; using Android.Graphics; using Android.OS; -using Android.Runtime; using Android.Util; using Android.Views; -using Android.Views.Accessibility; -using Android.Views.InputMethods; -using Avalonia.Android.Platform.Specific; -using Avalonia.Input; using Avalonia.Platform; namespace Avalonia.Android @@ -23,9 +18,6 @@ namespace Avalonia.Android { Holder.AddCallback(this); _handler = new Handler(context.MainLooper); - - Focusable = true; - FocusableInTouchMode = true; } public override void Invalidate() diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs index c7821d495e..2b2a9dd2b4 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs @@ -1,18 +1,9 @@ using System; -using System.ComponentModel; -using Android.App; -using Android.Content; -using Android.Runtime; using Android.Views; -using Android.Views.InputMethods; -using Android.Widget; using Avalonia.Android.Platform.Input; using Avalonia.Android.Platform.SkiaPlatform; -using Avalonia.Controls; -using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Input.TextInput; namespace Avalonia.Android.Platform.Specific.Helpers { From c94da58c094249638fe21670eb09e1e37598cce8 Mon Sep 17 00:00:00 2001 From: ili Date: Thu, 8 Apr 2021 15:22:54 +0500 Subject: [PATCH 09/16] Cleanup --- src/Android/Avalonia.Android/AvaloniaActivity.cs | 4 ---- .../Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs index 7e63e2d9fc..3c9f373a66 100644 --- a/src/Android/Avalonia.Android/AvaloniaActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs @@ -15,7 +15,6 @@ namespace Avalonia.Android if (_content != null) View.Content = _content; SetContentView(View); - //TakeKeyEvents(true); base.OnCreate(savedInstanceState); } @@ -32,8 +31,5 @@ namespace Avalonia.Android View.Content = value; } } - - //public override bool DispatchKeyEvent(KeyEvent e) => - // View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e); } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index bc62bcde65..4df7befd29 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -14,6 +14,7 @@ namespace Avalonia.Android readonly object _lock = new object(); private readonly Handler _handler; + public InvalidationAwareSurfaceView(Context context) : base(context) { Holder.AddCallback(this); From c37830989437553b43a183288680db1cbb797721 Mon Sep 17 00:00:00 2001 From: ili Date: Thu, 8 Apr 2021 18:35:11 +0500 Subject: [PATCH 10/16] AndroidTestApplication all content type text boxes Also do not throw when input not configured --- .../InvalidationAwareSurfaceView.cs | 2 +- .../Platform/SkiaPlatform/TopLevelImpl.cs | 6 ++--- .../MainActivity.cs | 22 ++++++++++++++++++- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 4df7befd29..34784612f1 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -13,7 +13,7 @@ namespace Avalonia.Android bool _invalidateQueued; readonly object _lock = new object(); private readonly Handler _handler; - + public InvalidationAwareSurfaceView(Context context) : base(context) { diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 0b6dcba1ef..60b772a183 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -197,10 +197,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform public sealed override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) { - if (_initEditorInfo == null) - throw new InvalidOperationException("Call IInitEditorInfo.InitEditorInfo first"); - - _initEditorInfo(outAttrs); + if (_initEditorInfo != null) + _initEditorInfo(outAttrs); return base.OnCreateInputConnection(outAttrs); } diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs index 121acb6351..e139185be6 100644 --- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs +++ b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs @@ -72,13 +72,33 @@ namespace Avalonia.AndroidTestApplication Height = 40, Background = Brushes.LightGreen, Foreground = Brushes.Black - } + }, + CreateTextBox(Input.TextInput.TextInputContentType.Normal), + CreateTextBox(Input.TextInput.TextInputContentType.Password), + CreateTextBox(Input.TextInput.TextInputContentType.Email), + CreateTextBox(Input.TextInput.TextInputContentType.Url), + CreateTextBox(Input.TextInput.TextInputContentType.Phone), + CreateTextBox(Input.TextInput.TextInputContentType.Number), } } }; return window; } + + private static TextBox CreateTextBox(Input.TextInput.TextInputContentType contentType) + { + var textBox = new TextBox() + { + Margin = new Thickness(20, 10), + Watermark = contentType.ToString(), + BorderThickness = new Thickness(3), + FontSize = 20 + }; + textBox.TextInputOptionsQuery += (s, e) => { e.ContentType = contentType; }; + + return textBox; + } } } From 00b70953dff867b5b325fd49c644fa92ce83f7f8 Mon Sep 17 00:00:00 2001 From: ili Date: Fri, 9 Apr 2021 19:10:03 +0500 Subject: [PATCH 11/16] Fix default keyboard InputType.Null does not change soft keyboard type --- src/Android/Avalonia.Android/AndroidInputMethod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index efb48158bc..19aaffa37d 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -71,7 +71,7 @@ namespace Avalonia.Android TextInputContentType.Password => global::Android.Text.InputTypes.TextVariationPassword, TextInputContentType.Phone => global::Android.Text.InputTypes.ClassPhone, TextInputContentType.Url => global::Android.Text.InputTypes.TextVariationUri, - _ => global::Android.Text.InputTypes.Null + _ => global::Android.Text.InputTypes.ClassText }; if (options.AutoCapitalization) From cb22388de721cb467811908dc1cade518a75a2fe Mon Sep 17 00:00:00 2001 From: ili Date: Sat, 10 Apr 2021 13:35:45 +0500 Subject: [PATCH 12/16] Android: remove input focus on soft keyboard hidden --- .../Avalonia.Android/AndroidInputMethod.cs | 3 +- .../Avalonia.Android/SoftKeyboardListner.cs | 42 +++++++++++++++++++ .../MainActivity.cs | 6 +-- 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/Android/Avalonia.Android/SoftKeyboardListner.cs diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index 19aaffa37d..7e49cb5dfa 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -25,6 +25,7 @@ namespace Avalonia.Android _host.Focusable = true; _host.FocusableInTouchMode = true; + _host.ViewTreeObserver.AddOnGlobalLayoutListener(new SoftKeyboardListner(_host)); } public void Reset() @@ -84,7 +85,7 @@ namespace Avalonia.Android outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagMultiLine; }); - _inputElement.PointerReleased += RestoreSoftKeyboard; + //_inputElement.PointerReleased += RestoreSoftKeyboard; } private void RestoreSoftKeyboard(object sender, PointerReleasedEventArgs e) diff --git a/src/Android/Avalonia.Android/SoftKeyboardListner.cs b/src/Android/Avalonia.Android/SoftKeyboardListner.cs new file mode 100644 index 0000000000..df658f6314 --- /dev/null +++ b/src/Android/Avalonia.Android/SoftKeyboardListner.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Android.Content; +using Android.OS; +using Android.Util; +using Android.Views; +using Avalonia.Input; + +namespace Avalonia.Android +{ + class SoftKeyboardListner : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener + { + private const int DefaultKeyboardHeightDP = 100; + private static readonly int EstimatedKeyboardDP = DefaultKeyboardHeightDP + (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop ? 48 : 0); + + private readonly View _host; + private bool _wasKeyboard; + + public SoftKeyboardListner(View view) + { + _host = view; + } + + public void OnGlobalLayout() + { + int estimatedKeyboardHeight = (int)TypedValue.ApplyDimension(ComplexUnitType.Dip, + EstimatedKeyboardDP, _host.Resources.DisplayMetrics); + + var rect = new global::Android.Graphics.Rect(); + _host.GetWindowVisibleDisplayFrame(rect); + + int heightDiff = _host.RootView.Height - (rect.Bottom - rect.Top); + var isKeyboard = heightDiff >= estimatedKeyboardHeight; + + if (_wasKeyboard && !isKeyboard) + KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); + + _wasKeyboard = isKeyboard; + } + } +} diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs index e139185be6..5f33cadf2e 100644 --- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs +++ b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs @@ -16,18 +16,18 @@ namespace Avalonia.AndroidTestApplication Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance/*, ScreenOrientation = ScreenOrientation.Landscape*/)] - public class MainBaseActivity : Activity + public class MainBaseActivity : AvaloniaActivity { protected override void OnCreate(Bundle savedInstanceState) { - base.OnCreate(savedInstanceState); if (Avalonia.Application.Current == null) { AppBuilder.Configure() .UseAndroid() .SetupWithoutStarting(); } - SetContentView(new AvaloniaView(this) { Content = App.CreateSimpleWindow() }); + base.OnCreate(savedInstanceState); + Content = App.CreateSimpleWindow(); } } From 6e26cd58fa6d7324484a4766da03b2bdc1c91eb7 Mon Sep 17 00:00:00 2001 From: ReeJK Date: Tue, 13 Apr 2021 13:37:04 +0300 Subject: [PATCH 13/16] Button state update when CommandParameter changed --- src/Avalonia.Controls/Button.cs | 13 ++++++++ .../ButtonTests.cs | 33 +++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 6093cbd581..6ebbfd05f6 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -89,6 +89,7 @@ namespace Avalonia.Controls { FocusableProperty.OverrideDefaultValue(typeof(Button), true); CommandProperty.Changed.Subscribe(CommandChanged); + CommandParameterProperty.Changed.Subscribe(CommandParameterChanged); IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); IsCancelProperty.Changed.Subscribe(IsCancelChanged); } @@ -380,6 +381,18 @@ namespace Avalonia.Controls } } + /// + /// Called when the property changes. + /// + /// The event args. + private static void CommandParameterChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is Button button) + { + button.CanExecuteChanged(button, EventArgs.Empty); + } + } + /// /// Called when the property changes. /// diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 7ad0e480c6..c9ad24e654 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -269,6 +269,19 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, command.SubscriptionCount); } + [Fact] + public void Button_Invokes_CanExecute_When_CommandParameter_Changed() + { + var command = new TestCommand(p => p is bool value && value); + var target = new Button { Command = command }; + + target.CommandParameter = true; + Assert.True(target.IsEffectivelyEnabled); + + target.CommandParameter = false; + Assert.False(target.IsEffectivelyEnabled); + } + private class TestButton : Button, IRenderRoot { public TestButton() @@ -324,12 +337,22 @@ namespace Avalonia.Controls.UnitTests private class TestCommand : ICommand { + private readonly Func _canExecute; + private readonly Action _execute; private EventHandler _canExecuteChanged; - private bool _enabled; + private bool _enabled = true; - public TestCommand(bool enabled) + public TestCommand(bool enabled = true) { _enabled = enabled; + _canExecute = _ => _enabled; + _execute = _ => { }; + } + + public TestCommand(Func canExecute, Action execute = null) + { + _canExecute = canExecute; + _execute = execute ?? (_ => { }); } public bool IsEnabled @@ -353,11 +376,9 @@ namespace Avalonia.Controls.UnitTests remove { _canExecuteChanged -= value; --SubscriptionCount; } } - public bool CanExecute(object parameter) => _enabled; + public bool CanExecute(object parameter) => _canExecute(parameter); - public void Execute(object parameter) - { - } + public void Execute(object parameter) => _execute(parameter); } } } From b37afa4a3d4aaaa3831877b42df90dcd7265bccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20So=C3=B3s?= Date: Fri, 16 Apr 2021 13:06:00 +0200 Subject: [PATCH 14/16] clear selected item from CollectionView if null --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 8f9b9583cf..2604c7a082 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -910,6 +910,11 @@ namespace Avalonia.Controls // Clear all row selections ClearRowSelection(resetAnchorSlot: true); + + if (DataConnection.CollectionView != null) + { + DataConnection.CollectionView.MoveCurrentTo(null); + } } else { From 2c3f70bd07f0a1fa7d7ff8947a8672a4d34360fb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 16 Apr 2021 15:22:24 +0200 Subject: [PATCH 15/16] Add more tests for resetting InternalSelectionModel. Always check that `WriteableSelectedItems` is correct - sometimes it's not it seems. --- .../Selection/InternalSelectionModelTests.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs index ce3e698cf3..224b89bf85 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs @@ -97,6 +97,7 @@ namespace Avalonia.Controls.UnitTests.Selection target.WritableSelectedItems.Clear(); Assert.Empty(target.SelectedIndexes); + Assert.Empty(target.WritableSelectedItems); } [Fact] @@ -123,6 +124,7 @@ namespace Avalonia.Controls.UnitTests.Selection target.WritableSelectedItems = null; Assert.Empty(target.SelectedIndexes); + Assert.Empty(target.WritableSelectedItems); } [Fact] @@ -182,6 +184,7 @@ namespace Avalonia.Controls.UnitTests.Selection target.Source = items; Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { "bar" }, target.WritableSelectedItems); } [Fact] @@ -203,6 +206,7 @@ namespace Avalonia.Controls.UnitTests.Selection items.Reset(new[] { "baz", "foo", "bar" }); Assert.Equal(2, target.SelectedIndex); + Assert.Equal(new[] { "bar" }, target.WritableSelectedItems); } [Fact] @@ -227,6 +231,7 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(-1, target.SelectedIndex); Assert.Equal(null, target.SelectedItem); + Assert.Empty(target.WritableSelectedItems); Assert.Contains(nameof(target.SelectedIndex), changed); Assert.Contains(nameof(target.SelectedItem), changed); @@ -246,6 +251,7 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal("foo", target.SelectedItem); Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { "foo" }, target.WritableSelectedItems); } [Fact] @@ -257,6 +263,7 @@ namespace Avalonia.Controls.UnitTests.Selection target.Source = new[] { "baz", "foo", "bar" }; Assert.Equal(2, target.SelectedIndex); + Assert.Equal(new[] { "bar" }, target.WritableSelectedItems); } private static InternalSelectionModel CreateTarget( From 5c3218ae18b610ef2cb8c1b0e0289fb262785e21 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 16 Apr 2021 15:48:20 +0200 Subject: [PATCH 16/16] Correctly sync SelectedItems on collection reset. When `InternalSelectionModel` gets a `Reset` event it tries to restore the selection from `WritableSelectedItems`, however this was causing items to be added twice to `WriteableSelectedItems`: collection is reset - Selection is cleared - `InternalSelectionModel` restores selection on reset from `WritableSelectedItems` - Which changes the selection - Which adds the selection back to `WritableSelectedItems`, causing the selected item to appear twice in the collection --- .../Selection/InternalSelectionModel.cs | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index fcdaf44166..c9d91b1ae6 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -13,8 +13,9 @@ namespace Avalonia.Controls.Selection internal class InternalSelectionModel : SelectionModel { private IList? _writableSelectedItems; - private bool _ignoreModelChanges; + private int _ignoreModelChanges; private bool _ignoreSelectedItemsChanges; + private bool _isResetting; public InternalSelectionModel() { @@ -130,17 +131,31 @@ namespace Avalonia.Controls.Selection try { - _ignoreModelChanges = true; + ++_ignoreModelChanges; using (BatchUpdate()) { Clear(); - Add(_writableSelectedItems); + + for (var i = 0; i < _writableSelectedItems.Count; ++i) + { + var index = IndexOf(Source, _writableSelectedItems[i]); + + if (index != -1) + { + Select(index); + } + else + { + _writableSelectedItems.RemoveAt(i); + --i; + } + } } } finally { - _ignoreModelChanges = false; + --_ignoreModelChanges; } } @@ -162,7 +177,7 @@ namespace Avalonia.Controls.Selection private void OnSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) { - if (_ignoreModelChanges) + if (_ignoreModelChanges > 0) { return; } @@ -191,6 +206,27 @@ namespace Avalonia.Controls.Selection } } + private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Reset) + { + ++_ignoreModelChanges; + _isResetting = true; + } + + base.OnSourceCollectionChanged(e); + } + + protected override void OnSourceCollectionChangeFinished() + { + base.OnSourceCollectionChangeFinished(); + + if (_isResetting) + { + --_ignoreModelChanges; + } + } + private void OnSourceReset(object sender, EventArgs e) => SyncFromSelectedItems(); private void OnSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -222,7 +258,7 @@ namespace Avalonia.Controls.Selection { using var operation = BatchUpdate(); - _ignoreModelChanges = true; + ++_ignoreModelChanges; switch (e.Action) { @@ -244,7 +280,7 @@ namespace Avalonia.Controls.Selection } finally { - _ignoreModelChanges = false; + --_ignoreModelChanges; } }