Browse Source

Merge branch 'master' into rename-native-menu-separator

pull/5680/head
Steven Kirk 5 years ago
committed by GitHub
parent
commit
c4384bc639
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      native/Avalonia.Native/src/OSX/menu.h
  2. 26
      native/Avalonia.Native/src/OSX/menu.mm
  3. 7
      native/Avalonia.Native/src/OSX/window.mm
  4. 2
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  5. 2
      samples/ControlCatalog.Android/Properties/AndroidManifest.xml
  6. 47
      src/Android/Avalonia.Android/ActivityTracker.cs
  7. 32
      src/Android/Avalonia.Android/AndroidPlatform.cs
  8. 15
      src/Android/Avalonia.Android/AndroidThreadingInterface.cs
  9. 2
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  10. 11
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  11. 35
      src/Android/Avalonia.Android/AvaloniaView.cs
  12. 101
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  13. 17
      src/Android/Avalonia.Android/CursorFactory.cs
  14. 6
      src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
  15. 13
      src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
  16. 14
      src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs
  17. 6
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  18. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
  19. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  20. 48
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  21. 91
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
  22. 11
      src/Android/Avalonia.Android/app.config
  23. 4
      src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
  24. 2
      src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
  25. 4
      src/Avalonia.Controls/ApiCompatBaseline.txt
  26. 8
      src/Avalonia.Controls/AutoCompleteBox.cs
  27. 2
      src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs
  28. 33
      src/Avalonia.Controls/NativeMenu.cs
  29. 18
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  30. 2
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  31. 22
      src/Avalonia.Native/IAvnMenu.cs
  32. 5
      src/Avalonia.Native/avn.idl
  33. 32
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  34. 95
      tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs

5
native/Avalonia.Native/src/OSX/menu.h

@ -60,7 +60,6 @@ public:
void RaiseOnClicked();
};
class AvnAppMenu : public ComSingleObject<IAvnMenu, &IID_IAvnMenu>
{
private:
@ -71,10 +70,12 @@ public:
FORWARD_IUNKNOWN()
AvnAppMenu(IAvnMenuEvents* events);
AvnMenu* GetNative();
void RaiseNeedsUpdate ();
void RaiseOpening();
void RaiseClosed();
virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override;

26
native/Avalonia.Native/src/OSX/menu.mm

@ -298,6 +298,23 @@ void AvnAppMenu::RaiseNeedsUpdate()
}
}
void AvnAppMenu::RaiseOpening()
{
if(_baseEvents != nullptr)
{
_baseEvents->Opening();
}
}
void AvnAppMenu::RaiseClosed()
{
if(_baseEvents != nullptr)
{
_baseEvents->Closed();
}
}
HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item)
{
@autoreleasepool
@ -382,6 +399,15 @@ HRESULT AvnAppMenu::Clear()
_parent->RaiseNeedsUpdate();
}
- (void)menuWillOpen:(NSMenu *)menu
{
_parent->RaiseOpening();
}
- (void)menuDidClose:(NSMenu *)menu
{
_parent->RaiseClosed();
}
@end

7
native/Avalonia.Native/src/OSX/window.mm

@ -2231,9 +2231,12 @@ protected:
{
@autoreleasepool
{
[Window setContentSize:NSSize{x, y}];
if (Window != nullptr)
{
[Window setContentSize:NSSize{x, y}];
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];
}
return S_OK;
}

2
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -16,7 +16,7 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
<TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

2
samples/ControlCatalog.Android/Properties/AndroidManifest.xml

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ControlCatalog.Android" android:versionCode="1" android:versionName="1.0" android:installLocation="auto">
<uses-sdk android:targetSdkVersion="29" />
<uses-sdk android:targetSdkVersion="30" />
<application android:label="ControlCatalog.Android"></application>
</manifest>

47
src/Android/Avalonia.Android/ActivityTracker.cs

@ -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;
}
}
}

32
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -29,60 +29,42 @@ 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)
{
Options = options;
AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToTransient<ClipboardImpl>()
.Bind<IStandardCursorFactory>().ToTransient<CursorFactory>()
.Bind<ICursorFactory>().ToTransient<CursorFactory>()
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToConstant(Instance)
.Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
.Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
.Bind<IWindowingPlatform>().ToConstant(Instance)
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<IAssetLoader>().ToConstant(new AssetLoader(appType.Assembly));
SkiaPlatform.Initialize();
((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext)
.RegisterActivityLifecycleCallbacks(new ActivityTracker());
if (options.UseGpu)
{
EglPlatformOpenGlInterface.TryInitialize();
}
}
public IWindowImpl CreateWindow()
{
throw new NotSupportedException();
}
public IWindowImpl CreateEmbeddableWindow()
{
throw new NotSupportedException();
}
}
public sealed class AndroidPlatformOptions
{
public bool UseDeferredRendering { get; set; } = true;
public bool UseGpu { get; set; } = true;
}
}

15
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)

2
src/Android/Avalonia.Android/Avalonia.Android.csproj

@ -1,6 +1,6 @@
<Project Sdk="MSBuild.Sdk.Extras">
<PropertyGroup>
<TargetFramework>monoandroid90</TargetFramework>
<TargetFramework>monoandroid11.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

11
src/Android/Avalonia.Android/AvaloniaActivity.cs

@ -1,4 +1,3 @@
using Android.App;
using Android.OS;
using Android.Views;
@ -7,15 +6,13 @@ namespace Avalonia.Android
{
public abstract class AvaloniaActivity : Activity
{
internal AvaloniaView View;
object _content;
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);
@ -36,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);
}
}

35
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
{
@ -14,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);
@ -33,6 +36,36 @@ 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)
{
if (AvaloniaLocator.Current.GetService<IRenderTimer>() is ChoreographerTimer timer)
{
_timerSubscription = timer.SubscribeView(this);
}
_root.Renderer.Start();
}
else
{
_root.Renderer.Stop();
_timerSubscription?.Dispose();
}
}
class ViewImpl : TopLevelImpl
{
public ViewImpl(Context context) : base(context)

101
src/Android/Avalonia.Android/ChoreographerTimer.cs

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
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> _choreographer = new TaskCompletionSource<Choreographer>();
private readonly ISet<AvaloniaView> _views = new HashSet<AvaloniaView>();
private Action<TimeSpan> _tick;
private int _count;
public ChoreographerTimer()
{
_thread = new Thread(Loop);
_thread.Start();
}
public event Action<TimeSpan> Tick
{
add
{
lock (_lock)
{
_tick += value;
_count++;
if (_count == 1)
{
_choreographer.Task.Result.PostFrameCallback(this);
}
}
}
remove
{
lock (_lock)
{
_tick -= value;
_count--;
}
}
}
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();
_choreographer.SetResult(Choreographer.Instance);
Looper.Loop();
}
public void DoFrame(long frameTimeNanos)
{
_tick?.Invoke(TimeSpan.FromTicks(frameTimeNanos / 100));
lock (_lock)
{
if (_count > 0 && _views.Count > 0)
{
Choreographer.Instance.PostFrameCallback(this);
}
}
}
}
}

17
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() { }
}
}
}

6
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)
{

13
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);
}
}

14
src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs

@ -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()
{
}
}
}

6
src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs

@ -10,7 +10,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
private IntPtr _window;
public AndroidFramebuffer(Surface surface)
public AndroidFramebuffer(Surface surface, double scaling)
{
if(surface == null)
throw new ArgumentNullException(nameof(surface));
@ -31,6 +31,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4);
Address = buffer.bits;
Dpi = scaling * new Vector(96, 96);
}
public void Dispose()
@ -44,7 +46,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IntPtr Address { get; set; }
public PixelSize Size { get; }
public int RowBytes { get; }
public Vector Dpi { get; } = new Vector(96, 96);
public Vector Dpi { get; }
public PixelFormat Format { get; }
[DllImport("android")]

2
src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs

@ -12,6 +12,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_topLevel = topLevel;
}
public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface);
public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface, _topLevel.RenderScaling);
}
}

2
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();

48
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,16 +34,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view = new ViewImpl(context, this, placeOnTop);
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
_touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(this, () => InputRoot,
p => GetAvaloniaPointFromEvent(p));
GetAvaloniaPointFromEvent);
_gl = GlPlatformSurface.TryCreate(this);
_framebuffer = new FramebufferManager(this);
MaxClientSize = new Size(_view.Resources.DisplayMetrics.WidthPixels,
_view.Resources.DisplayMetrics.HeightPixels);
}
RenderScaling = (int)_view.Resources.DisplayMetrics.Density;
MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
_view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
}
private bool _handleEvents;
@ -58,25 +57,14 @@ 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; }
public virtual Size ClientSize
{
get
{
if (_view == null)
return new Size(0, 0);
return new Size(_view.Width, _view.Height);
}
set
{
}
}
public virtual Size ClientSize => Size.ToSize(RenderScaling);
public IMouseDevice MouseDevice => AndroidMouseDevice.Instance;
public IMouseDevice MouseDevice { get; } = new MouseDevice();
public Action Closed { get; set; }
@ -98,10 +86,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IEnumerable<object> 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<IRenderLoop>()) { RenderOnlyOnRenderThread = true }
: new ImmediateRenderer(root);
public virtual void Hide()
{
@ -115,15 +103,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public Point PointToClient(PixelPoint point)
{
return point.ToPoint(1);
return point.ToPoint(RenderScaling);
}
public PixelPoint PointToScreen(Point point)
{
return PixelPoint.FromPoint(point, 1);
return PixelPoint.FromPoint(point, RenderScaling);
}
public void SetCursor(IPlatformHandle cursor)
public void SetCursor(ICursorImpl cursor)
{
//still not implemented
}
@ -138,7 +126,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view.Visibility = ViewStates.Visible;
}
public double RenderScaling => 1;
public double RenderScaling { get; }
void Draw()
{
@ -193,7 +181,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
{
var newSize = new Size(width, height);
var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling);
if (newSize != _oldSize)
{

91
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<IInputRoot> getInputRoot, Func<MotionEvent, Point> getPointfunc)
public AndroidTouchEventsHelper(TView view, Func<IInputRoot> getInputRoot, Func<MotionEvent, int, Point> 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<MotionEvent, Point> _getPointFunc;
private TouchDevice _touchDevice = new TouchDevice();
private Func<MotionEvent, int, Point> _getPointFunc;
private Func<IInputRoot> _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;
}
}
}

11
src/Android/Avalonia.Android/app.config

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.InteropServices.WindowsRuntime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

4
src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj

@ -16,7 +16,7 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
<TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -150,4 +150,4 @@
<Import Project="..\..\..\build\System.Memory.props" />
<Import Project="..\..\..\build\AndroidWorkarounds.props" />
<Import Project="..\..\..\build\LegacyProject.targets" />
</Project>
</Project>

2
src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="Avalonia.AndroidTestApplication" android:versionCode="1" android:versionName="1.0" android:installLocation="auto">
<uses-sdk android:targetSdkVersion="29" />
<uses-sdk android:targetSdkVersion="30" />
<application android:label="Avalonia.AndroidTestApplication" android:icon="@drawable/Icon" android:hardwareAccelerated="true"></application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

4
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -1,7 +1,9 @@
Compat issues with assembly Avalonia.Controls:
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
Total Issues: 5
Total Issues: 7

8
src/Avalonia.Controls/AutoCompleteBox.cs

@ -483,7 +483,9 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<AutoCompleteBox, object>(
nameof(SelectedItem),
o => o.SelectedItem,
(o, v) => o.SelectedItem = v);
(o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);
/// <summary>
/// Identifies the
@ -1333,7 +1335,7 @@ namespace Avalonia.Controls
base.OnApplyTemplate(e);
}
/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.
@ -1342,7 +1344,7 @@ namespace Avalonia.Controls
/// <param name="value">The new binding value for the property.</param>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == TextProperty)
if (property == TextProperty || property == SelectedItemProperty)
{
DataValidationErrors.SetError(this, value.Error);
}

2
src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs

@ -3,5 +3,7 @@ namespace Avalonia.Controls
public interface INativeMenuExporterEventsImplBridge
{
void RaiseNeedsUpdate ();
void RaiseOpening();
void RaiseClosed();
}
}

33
src/Avalonia.Controls/NativeMenu.cs

@ -12,13 +12,34 @@ namespace Avalonia.Controls
private readonly AvaloniaList<NativeMenuItemBase> _items =
new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove };
private NativeMenuItem _parent;
[Content]
public IList<NativeMenuItemBase> Items => _items;
/// <summary>
/// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically.
/// Raised when the menu requests an update.
/// </summary>
/// <remarks>
/// Use this event to add, remove or modify menu items before a menu is
/// shown or a hotkey is pressed.
/// </remarks>
public event EventHandler<EventArgs> NeedsUpdate;
/// <summary>
/// Raised before the menu is opened.
/// </summary>
/// <remarks>
/// Do not update the menu in this event; use <see cref="NeedsUpdate"/>.
/// </remarks>
public event EventHandler<EventArgs> Opening;
/// <summary>
/// Raised after the menu is closed.
/// </summary>
/// <remarks>
/// Do not update the menu in this event; use <see cref="NeedsUpdate"/>.
/// </remarks>
public event EventHandler<EventArgs> Closed;
public NativeMenu()
{
@ -27,10 +48,20 @@ namespace Avalonia.Controls
}
void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate()
{
NeedsUpdate?.Invoke(this, EventArgs.Empty);
}
void INativeMenuExporterEventsImplBridge.RaiseOpening()
{
Opening?.Invoke(this, EventArgs.Empty);
}
void INativeMenuExporterEventsImplBridge.RaiseClosed()
{
Closed?.Invoke(this, EventArgs.Empty);
}
private void Validator(NativeMenuItemBase obj)
{
if (obj.Parent != null)

18
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -91,14 +91,14 @@ namespace Avalonia.Controls
/// </summary>
public static readonly DirectProperty<NumericUpDown, string> TextProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay);
defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary>
/// Defines the <see cref="Value"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, double> ValueProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value,
(updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay);
(updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary>
/// Defines the <see cref="Watermark"/> property.
@ -370,6 +370,20 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The new binding value for the property.</param>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == TextProperty || property == ValueProperty)
{
DataValidationErrors.SetError(this, value.Error);
}
}
/// <summary>
/// Called when the <see cref="CultureInfo"/> property value changed.
/// </summary>

2
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@ -169,7 +169,7 @@ namespace Avalonia.DesignerSupport.Remote
if (entryPoint == null)
throw Die($"Assembly {args.AppPath} doesn't have an entry point");
var builderMethod = entryPoint.DeclaringType.GetMethod(BuilderMethodName,
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Array.Empty<Type>(), null);
if (builderMethod == null)
throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}");
Design.IsDesignMode = true;

22
src/Avalonia.Native/IAvnMenu.cs

@ -20,11 +20,23 @@ namespace Avalonia.Native.Interop
{
_parent?.RaiseNeedsUpdate();
}
public void Opening()
{
_parent?.RaiseOpening();
}
public void Closed()
{
_parent?.RaiseClosed();
}
}
partial interface IAvnMenu
{
void RaiseNeedsUpdate();
void RaiseOpening();
void RaiseClosed();
void Deinitialise();
}
}
@ -45,6 +57,16 @@ namespace Avalonia.Native.Interop.Impl
_exporter.UpdateIfNeeded();
}
public void RaiseOpening()
{
(ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseOpening();
}
public void RaiseClosed()
{
(ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseClosed();
}
internal NativeMenu ManagedMenu { get; private set; }
public static __MicroComIAvnMenuProxy Create(IAvaloniaNativeFactory factory)

5
src/Avalonia.Native/avn.idl

@ -685,10 +685,9 @@ interface IAvnMenuItem : IUnknown
[uuid(0af7df53-7632-42f4-a650-0992c361b477)]
interface IAvnMenuEvents : IUnknown
{
/**
* NeedsUpdate
*/
void NeedsUpdate();
void Opening();
void Closed();
}
[uuid(5142bb41-66ab-49e7-bb37-cd079c000f27)]

32
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<BindingNotification>(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<BindingNotification>(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);
});
}
/// <summary>
/// Retrieves a defined predicate filter through a new AutoCompleteBox

95
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<BindingNotification>(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<BindingNotification>(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<NumericUpDown, TextBox> 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<ButtonSpinner>()
.Select(b => b.Content)
.OfType<TextBox>()
.First();
}
private IControlTemplate CreateTemplate()
{
return new FuncControlTemplate<NumericUpDown>((control, scope) =>
{
var textBox =
new TextBox
{
Name = "PART_TextBox"
}.RegisterInNameScope(scope);
return new ButtonSpinner
{
Name = "PART_Spinner",
Content = textBox,
}.RegisterInNameScope(scope);
});
}
}
}
Loading…
Cancel
Save