From 90b405952e13080f842d03c4597b13e374b5b630 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 25 Jun 2020 21:10:02 +0300 Subject: [PATCH 01/60] Extracted ANGLE-based EGL display to a separate class and added some DX-interop features --- .../Angle/AngleEglInterface.cs | 31 +++++++ .../Angle/AngleWin32EglDisplay.cs | 88 +++++++++++++++++++ src/Avalonia.OpenGL/EglConsts.cs | 10 +++ src/Avalonia.OpenGL/EglDisplay.cs | 76 ++++++++-------- src/Avalonia.OpenGL/EglGlPlatformFeature.cs | 7 +- src/Avalonia.OpenGL/EglInterface.cs | 29 ++++-- src/Windows/Avalonia.Win32/Win32GlManager.cs | 3 +- 7 files changed, 190 insertions(+), 54 deletions(-) create mode 100644 src/Avalonia.OpenGL/Angle/AngleEglInterface.cs create mode 100644 src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs diff --git a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs new file mode 100644 index 0000000000..375b93c27c --- /dev/null +++ b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Platform; +using Avalonia.Platform.Interop; + +namespace Avalonia.OpenGL.Angle +{ + public class AngleEglInterface : EglInterface + { + [DllImport("libegl.dll", CharSet = CharSet.Ansi)] + static extern IntPtr eglGetProcAddress(string proc); + + public AngleEglInterface() : base(LoadAngle()) + { + + } + + static Func LoadAngle() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new PlatformNotSupportedException(); + { + var disp = eglGetProcAddress("eglGetPlatformDisplayEXT"); + if (disp == IntPtr.Zero) + throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point"); + return eglGetProcAddress; + } + } + + } +} diff --git a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs new file mode 100644 index 0000000000..530411bfea --- /dev/null +++ b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using static Avalonia.OpenGL.EglConsts; + +namespace Avalonia.OpenGL.Angle +{ + public class AngleWin32EglDisplay : EglDisplay + { + struct AngleInfo + { + public IntPtr Display { get; set; } + public AngleOptions.PlatformApi PlatformApi { get; set; } + } + + static AngleInfo CreateAngleDisplay(EglInterface _egl) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new PlatformNotSupportedException(); + var display = IntPtr.Zero; + AngleOptions.PlatformApi angleApi = default; + { + if (_egl.GetPlatformDisplayEXT == null) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); + + var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis + ?? new List {AngleOptions.PlatformApi.DirectX9}; + + foreach (var platformApi in allowedApis) + { + int dapi; + if (platformApi == AngleOptions.PlatformApi.DirectX9) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; + else if (platformApi == AngleOptions.PlatformApi.DirectX11) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; + else + continue; + + display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, + new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE }); + if (display != IntPtr.Zero) + { + angleApi = platformApi; + break; + } + } + + if (display == IntPtr.Zero) + throw new OpenGlException("Unable to create ANGLE display"); + return new AngleInfo { Display = display, PlatformApi = angleApi }; + } + } + + private AngleWin32EglDisplay(EglInterface egl, AngleInfo info) : base(egl, info.Display) + { + PlatformApi = info.PlatformApi; + } + + public AngleWin32EglDisplay(EglInterface egl) : this(egl, CreateAngleDisplay(egl)) + { + + } + + public AngleWin32EglDisplay() : this(new AngleEglInterface()) + { + + } + + public AngleOptions.PlatformApi PlatformApi { get; } + + public IntPtr GetDirect3DDevice() + { + if (!EglInterface.QueryDisplayAttribExt(Handle, EglConsts.EGL_DEVICE_EXT, out var eglDevice)) + throw new OpenGlException("Unable to get EGL_DEVICE_EXT"); + if (!EglInterface.QueryDeviceAttribExt(eglDevice, PlatformApi == AngleOptions.PlatformApi.DirectX9 ? EGL_D3D9_DEVICE_ANGLE : EGL_D3D11_DEVICE_ANGLE, out var d3dDeviceHandle)) + throw new OpenGlException("Unable to get EGL_D3D9_DEVICE_ANGLE"); + return d3dDeviceHandle; + } + + public EglSurface WrapDirect3D11Texture(IntPtr handle) + { + if (PlatformApi != AngleOptions.PlatformApi.DirectX11) + throw new InvalidOperationException("Current platform API is " + PlatformApi); + return CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); + } + } +} diff --git a/src/Avalonia.OpenGL/EglConsts.cs b/src/Avalonia.OpenGL/EglConsts.cs index 62fb3faef6..d09537eb86 100644 --- a/src/Avalonia.OpenGL/EglConsts.cs +++ b/src/Avalonia.OpenGL/EglConsts.cs @@ -192,5 +192,15 @@ namespace Avalonia.OpenGL public const int EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE = 0x320F; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_WARP_ANGLE = 0x320B; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_REFERENCE_ANGLE = 0x320C; + + //EXT_device_query + public const int EGL_DEVICE_EXT = 0x322C; + + //ANGLE_device_d3d + public const int EGL_D3D9_DEVICE_ANGLE = 0x33A0; + public const int EGL_D3D11_DEVICE_ANGLE = 0x33A1; + + public const int EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE = 0x3200; + public const int EGL_D3D_TEXTURE_ANGLE = 0x33A3; } } diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 0436f6ac52..7b194e4346 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -15,7 +15,6 @@ namespace Avalonia.OpenGL private readonly int _surfaceType; public IntPtr Handle => _display; - private AngleOptions.PlatformApi? _angleApi; private int _sampleCount; private int _stencilSize; private GlVersion _version; @@ -24,56 +23,41 @@ namespace Avalonia.OpenGL { } - public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) - { - _egl = egl; + static IntPtr CreateDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) + { + var display = IntPtr.Zero; if (platformType == -1 && platformDisplay == IntPtr.Zero) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - if (_egl.GetPlatformDisplayEXT == null) - throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); - - var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new List {AngleOptions.PlatformApi.DirectX9}; - - foreach (var platformApi in allowedApis) - { - int dapi; - if (platformApi == AngleOptions.PlatformApi.DirectX9) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; - else if (platformApi == AngleOptions.PlatformApi.DirectX11) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; - else - continue; - - _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, - new[] {EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE}); - if (_display != IntPtr.Zero) - { - _angleApi = platformApi; - break; - } - } - - if (_display == IntPtr.Zero) - throw new OpenGlException("Unable to create ANGLE display"); - } - - if (_display == IntPtr.Zero) - _display = _egl.GetDisplay(IntPtr.Zero); + if (display == IntPtr.Zero) + display = egl.GetDisplay(IntPtr.Zero); } else { - if (_egl.GetPlatformDisplayEXT == null) + if (egl.GetPlatformDisplayEXT == null) throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); - _display = _egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs); + display = egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs); } + + if (display == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglGetDisplay", egl); + return display; + } - if (_display == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglGetDisplay", _egl); + public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) + : this(egl, CreateDisplay(egl, platformType, platformDisplay, attrs)) + { + + } + public EglDisplay(EglInterface egl, IntPtr display) + { + _egl = egl; + _display = display; + if(_display == IntPtr.Zero) + throw new ArgumentException(); + + if (!_egl.Initialize(_display, out var major, out var minor)) throw OpenGlException.GetFormattedException("eglInitialize", _egl); @@ -172,5 +156,15 @@ namespace Avalonia.OpenGL throw OpenGlException.GetFormattedException("eglCreateWindowSurface", _egl); return new EglSurface(this, _egl, s); } + + public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) + { + var s = _egl.CreatePbufferFromClientBuffer(_display, bufferType, handle, + _config, attribs); + + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", _egl); + return new EglSurface(this, _egl, s); + } } } diff --git a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs index f59c6b7751..7e9383432c 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs @@ -20,12 +20,13 @@ namespace Avalonia.OpenGL if (feature != null) AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); } - - public static EglGlPlatformFeature TryCreate() + + public static EglGlPlatformFeature TryCreate() => TryCreate(() => new EglDisplay()); + public static EglGlPlatformFeature TryCreate(Func displayFactory) { try { - var disp = new EglDisplay(); + var disp = displayFactory(); return new EglGlPlatformFeature { _display = disp, diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index c0665a1ea1..666c0d8351 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -17,25 +17,21 @@ namespace Avalonia.OpenGL } + public EglInterface(Func getProcAddress) : base(getProcAddress) + { + + } + public EglInterface(string library) : base(Load(library)) { } - [DllImport("libegl.dll", CharSet = CharSet.Ansi)] - static extern IntPtr eglGetProcAddress(string proc); static Func Load() { var os = AvaloniaLocator.Current.GetService().GetRuntimeInfo().OperatingSystem; if(os == OperatingSystemType.Linux || os == OperatingSystemType.Android) return Load("libEGL.so.1"); - if (os == OperatingSystemType.WinNT) - { - var disp = eglGetProcAddress("eglGetPlatformDisplayEXT"); - if (disp == IntPtr.Zero) - throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point"); - return eglGetProcAddress; - } throw new PlatformNotSupportedException(); } @@ -147,6 +143,21 @@ namespace Avalonia.OpenGL return null; return Marshal.PtrToStringAnsi(rv); } + + public delegate IntPtr EglCreatePbufferFromClientBuffer(IntPtr display, int buftype, IntPtr buffer, IntPtr config, int[] attrib_list); + [GlEntryPoint("eglCreatePbufferFromClientBuffer")] + + public EglCreatePbufferFromClientBuffer CreatePbufferFromClientBuffer { get; } + + public delegate bool EglQueryDisplayAttribEXT(IntPtr display, int attr, out IntPtr res); + + [GlEntryPoint("eglQueryDisplayAttribEXT"), GlOptionalEntryPoint] + public EglQueryDisplayAttribEXT QueryDisplayAttribExt { get; } + + public delegate bool EglQueryDeviceAttribEXT(IntPtr display, int attr, out IntPtr res); + + [GlEntryPoint("eglQueryDeviceAttribEXT"), GlOptionalEntryPoint] + public EglQueryDisplayAttribEXT QueryDeviceAttribExt { get; } // ReSharper restore UnassignedGetOnlyAutoProperty } diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 585e68056b..bd188ad53a 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,4 +1,5 @@ using Avalonia.OpenGL; +using Avalonia.OpenGL.Angle; namespace Avalonia.Win32 { @@ -15,7 +16,7 @@ namespace Avalonia.Win32 { if (!s_attemptedToInitialize) { - EglFeature = EglGlPlatformFeature.TryCreate(); + EglFeature = EglGlPlatformFeature.TryCreate(() => new AngleWin32EglDisplay()); s_attemptedToInitialize = true; } From a34fefc998d9dce6416b8d7de8f89f86869cb3a7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 25 Jun 2020 21:32:29 +0300 Subject: [PATCH 02/60] Allow more customization for EglGlPlatformSurface --- src/Avalonia.OpenGL/EglGlPlatformSurface.cs | 84 ++------------ .../EglGlPlatformSurfaceBase.cs | 103 ++++++++++++++++++ 2 files changed, 112 insertions(+), 75 deletions(-) create mode 100644 src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs index 3e4befe2c6..21fadff19e 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs @@ -3,33 +3,26 @@ using System.Threading; namespace Avalonia.OpenGL { - public class EglGlPlatformSurface : IGlPlatformSurface + public class EglGlPlatformSurface : EglGlPlatformSurfaceBase { - public interface IEglWindowGlPlatformSurfaceInfo - { - IntPtr Handle { get; } - PixelSize Size { get; } - double Scaling { get; } - } - private readonly EglDisplay _display; private readonly EglContext _context; private readonly IEglWindowGlPlatformSurfaceInfo _info; - public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info) + public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info) : base() { _display = context.Display; _context = context; _info = info; } - - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + + public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() { var glSurface = _display.CreateWindowSurface(_info.Handle); return new RenderTarget(_display, _context, glSurface, _info); } - class RenderTarget : IGlPlatformSurfaceRenderTargetWithCorruptionInfo + class RenderTarget : EglPlatformSurfaceRenderTargetBase { private readonly EglDisplay _display; private readonly EglContext _context; @@ -38,7 +31,7 @@ namespace Avalonia.OpenGL private PixelSize _initialSize; public RenderTarget(EglDisplay display, EglContext context, - EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) : base(display, context) { _display = display; _context = context; @@ -47,70 +40,11 @@ namespace Avalonia.OpenGL _initialSize = info.Size; } - public void Dispose() => _glSurface.Dispose(); + public override void Dispose() => _glSurface.Dispose(); - public bool IsCorrupted => _initialSize != _info.Size; - - public IGlPlatformSurfaceRenderingSession BeginDraw() - { - var l = _context.Lock(); - try - { - if (IsCorrupted) - throw new RenderTargetCorruptedException(); - var restoreContext = _context.MakeCurrent(_glSurface); - _display.EglInterface.WaitClient(); - _display.EglInterface.WaitGL(); - _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); - - return new Session(_display, _context, _glSurface, _info, l, restoreContext); - } - catch - { - l.Dispose(); - throw; - } - } + public override bool IsCorrupted => _initialSize != _info.Size; - class Session : IGlPlatformSurfaceRenderingSession - { - private readonly EglContext _context; - private readonly EglSurface _glSurface; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - private readonly EglDisplay _display; - private IDisposable _lock; - private readonly IDisposable _restoreContext; - - - public Session(EglDisplay display, EglContext context, - EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info, - IDisposable @lock, IDisposable restoreContext) - { - _context = context; - _display = display; - _glSurface = glSurface; - _info = info; - _lock = @lock; - _restoreContext = restoreContext; - } - - public void Dispose() - { - _context.GlInterface.Flush(); - _display.EglInterface.WaitGL(); - _glSurface.SwapBuffers(); - _display.EglInterface.WaitClient(); - _display.EglInterface.WaitGL(); - _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); - _restoreContext.Dispose(); - _lock.Dispose(); - } - - public IGlContext Context => _context; - public PixelSize Size => _info.Size; - public double Scaling => _info.Scaling; - public bool IsYFlipped { get; } - } + public override IGlPlatformSurfaceRenderingSession BeginDraw() => base.BeginDraw(_glSurface, _info); } } } diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs b/src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs new file mode 100644 index 0000000000..00c7c4796c --- /dev/null +++ b/src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs @@ -0,0 +1,103 @@ +using System; + +namespace Avalonia.OpenGL +{ + public abstract class EglGlPlatformSurfaceBase : IGlPlatformSurface + { + public interface IEglWindowGlPlatformSurfaceInfo + { + IntPtr Handle { get; } + PixelSize Size { get; } + double Scaling { get; } + } + + public abstract IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(); + } + + public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTargetWithCorruptionInfo + { + private readonly EglDisplay _display; + private readonly EglContext _context; + + protected EglPlatformSurfaceRenderTargetBase(EglDisplay display, EglContext context) + { + _display = display; + _context = context; + } + + public abstract bool IsCorrupted { get; } + + public virtual void Dispose() + { + + } + + public abstract IGlPlatformSurfaceRenderingSession BeginDraw(); + + protected IGlPlatformSurfaceRenderingSession BeginDraw(EglSurface surface, + EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, Action onFinish = null, bool isYFlipped = false) + { + var l = _context.Lock(); + try + { + if (IsCorrupted) + throw new RenderTargetCorruptedException(); + var restoreContext = _context.MakeCurrent(surface); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); + + return new Session(_display, _context, surface, info, l, restoreContext, onFinish, isYFlipped); + } + catch + { + l.Dispose(); + throw; + } + } + + class Session : IGlPlatformSurfaceRenderingSession + { + private readonly EglContext _context; + private readonly EglSurface _glSurface; + private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; + private readonly EglDisplay _display; + private readonly IDisposable _lock; + private readonly IDisposable _restoreContext; + private readonly Action _onFinish; + + + public Session(EglDisplay display, EglContext context, + EglSurface glSurface, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, + IDisposable @lock, IDisposable restoreContext, Action onFinish, bool isYFlipped) + { + IsYFlipped = isYFlipped; + _context = context; + _display = display; + _glSurface = glSurface; + _info = info; + _lock = @lock; + _restoreContext = restoreContext; + _onFinish = onFinish; + } + + public void Dispose() + { + _context.GlInterface.Flush(); + _display.EglInterface.WaitGL(); + _glSurface.SwapBuffers(); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); + _restoreContext.Dispose(); + _lock.Dispose(); + _onFinish?.Invoke(); + } + + public IGlContext Context => _context; + public PixelSize Size => _info.Size; + public double Scaling => _info.Scaling; + public bool IsYFlipped { get; } + } + } +} From a42e8cbdede4d8d2509eaf5ac8a848ef62835351 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Jul 2020 22:19:33 +0200 Subject: [PATCH 03/60] Nullable enable Avalonia.Input. --- src/Avalonia.Input/AccessKeyHandler.cs | 22 +++--- src/Avalonia.Input/Avalonia.Input.csproj | 2 + src/Avalonia.Input/DataObject.cs | 6 +- src/Avalonia.Input/DragDropDevice.cs | 6 +- src/Avalonia.Input/FocusManager.cs | 47 ++++++------ .../GestureRecognizerCollection.cs | 16 ++-- .../ScrollGestureRecognizer.cs | 15 ++-- src/Avalonia.Input/Gestures.cs | 9 ++- src/Avalonia.Input/IAccessKeyHandler.cs | 2 +- src/Avalonia.Input/IDataObject.cs | 6 +- src/Avalonia.Input/IFocusManager.cs | 6 +- src/Avalonia.Input/IInputElement.cs | 2 +- src/Avalonia.Input/IInputRoot.cs | 4 +- src/Avalonia.Input/IKeyboardDevice.cs | 4 +- src/Avalonia.Input/IPointer.cs | 4 +- src/Avalonia.Input/IPointerDevice.cs | 4 +- src/Avalonia.Input/InputElement.cs | 8 +- src/Avalonia.Input/InputExtensions.cs | 2 +- src/Avalonia.Input/KeyEventArgs.cs | 2 +- src/Avalonia.Input/KeyboardDevice.cs | 8 +- .../KeyboardNavigationHandler.cs | 13 ++-- src/Avalonia.Input/MouseDevice.cs | 73 ++++++++++--------- .../Navigation/TabNavigation.cs | 42 ++++++----- src/Avalonia.Input/Pointer.cs | 9 +-- src/Avalonia.Input/PointerEventArgs.cs | 14 ++-- src/Avalonia.Input/Raw/RawDragEventType.cs | 2 +- src/Avalonia.Input/Raw/RawInputEventArgs.cs | 2 +- src/Avalonia.Input/TextInputEventArgs.cs | 4 +- 28 files changed, 172 insertions(+), 162 deletions(-) diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 96f0bb59b3..660584e2ed 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -28,7 +28,7 @@ namespace Avalonia.Input /// /// The window to which the handler belongs. /// - private IInputRoot _owner; + private IInputRoot? _owner; /// /// Whether access keys are currently being shown; @@ -48,17 +48,17 @@ namespace Avalonia.Input /// /// Element to restore following AltKey taking focus. /// - private IInputElement _restoreFocusElement; + private IInputElement? _restoreFocusElement; /// /// The window's main menu. /// - private IMainMenu _mainMenu; + private IMainMenu? _mainMenu; /// /// Gets or sets the window's main menu. /// - public IMainMenu MainMenu + public IMainMenu? MainMenu { get => _mainMenu; set @@ -86,14 +86,12 @@ namespace Avalonia.Input /// public void SetOwner(IInputRoot owner) { - Contract.Requires(owner != null); - if (_owner != null) { throw new InvalidOperationException("AccessKeyHandler owner has already been set."); } - _owner = owner; + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); _owner.AddHandler(InputElement.KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel); _owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown, RoutingStrategies.Bubble); @@ -149,7 +147,7 @@ namespace Avalonia.Input // When Alt is pressed without a main menu, or with a closed main menu, show // access key markers in the window (i.e. "_File"). - _owner.ShowAccessKeys = _showingAccessKeys = true; + _owner!.ShowAccessKeys = _showingAccessKeys = true; } else { @@ -241,7 +239,7 @@ namespace Avalonia.Input { if (_showingAccessKeys) { - _owner.ShowAccessKeys = false; + _owner!.ShowAccessKeys = false; } } @@ -250,13 +248,13 @@ namespace Avalonia.Input /// private void CloseMenu() { - MainMenu.Close(); - _owner.ShowAccessKeys = _showingAccessKeys = false; + MainMenu!.Close(); + _owner!.ShowAccessKeys = _showingAccessKeys = false; } private void MainMenuClosed(object sender, EventArgs e) { - _owner.ShowAccessKeys = false; + _owner!.ShowAccessKeys = false; } } } diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj index ea560ce2ea..58927e2c93 100644 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ b/src/Avalonia.Input/Avalonia.Input.csproj @@ -1,6 +1,8 @@  netstandard2.0 + Enable + CS8600;CS8602;CS8603 diff --git a/src/Avalonia.Input/DataObject.cs b/src/Avalonia.Input/DataObject.cs index 60d7d67606..688f5f9cc8 100644 --- a/src/Avalonia.Input/DataObject.cs +++ b/src/Avalonia.Input/DataObject.cs @@ -11,7 +11,7 @@ namespace Avalonia.Input return _items.ContainsKey(dataFormat); } - public object Get(string dataFormat) + public object? Get(string dataFormat) { if (_items.ContainsKey(dataFormat)) return _items[dataFormat]; @@ -23,12 +23,12 @@ namespace Avalonia.Input return _items.Keys; } - public IEnumerable GetFileNames() + public IEnumerable? GetFileNames() { return Get(DataFormats.FileNames) as IEnumerable; } - public string GetText() + public string? GetText() { return Get(DataFormats.Text) as string; } diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs index bcd962bc31..30a08eda17 100644 --- a/src/Avalonia.Input/DragDropDevice.cs +++ b/src/Avalonia.Input/DragDropDevice.cs @@ -9,9 +9,9 @@ namespace Avalonia.Input { public static readonly DragDropDevice Instance = new DragDropDevice(); - private Interactive _lastTarget = null; + private Interactive? _lastTarget = null; - private Interactive GetTarget(IInputRoot root, Point local) + private Interactive? GetTarget(IInputRoot root, Point local) { var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType()?.FirstOrDefault(); if (target != null && DragDrop.GetAllowDrop(target)) @@ -19,7 +19,7 @@ namespace Avalonia.Input return null; } - private DragDropEffects RaiseDragEvent(Interactive target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers) + private DragDropEffects RaiseDragEvent(Interactive? target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers) { if (target == null) return DragDropEffects.None; diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 66355da8b9..a1f1478f51 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -15,8 +15,8 @@ namespace Avalonia.Input /// /// The focus scopes in which the focus is currently defined. /// - private readonly ConditionalWeakTable _focusScopes = - new ConditionalWeakTable(); + private readonly ConditionalWeakTable _focusScopes = + new ConditionalWeakTable(); /// /// Initializes a new instance of the class. @@ -37,12 +37,12 @@ namespace Avalonia.Input /// /// Gets the currently focused . /// - public IInputElement Current => KeyboardDevice.Instance?.FocusedElement; + public IInputElement? Current => KeyboardDevice.Instance?.FocusedElement; /// /// Gets the current focus scope. /// - public IFocusScope Scope + public IFocusScope? Scope { get; private set; @@ -55,7 +55,7 @@ namespace Avalonia.Input /// The method by which focus was changed. /// Any key modifiers active at the time of focus. public void Focus( - IInputElement control, + IInputElement? control, NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None) { @@ -75,17 +75,18 @@ namespace Avalonia.Input // If control is null, set focus to the topmost focus scope. foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList()) { - IInputElement element; - - if (_focusScopes.TryGetValue(scope, out element) && element != null) + if (_focusScopes.TryGetValue(scope, out var element) && element != null) { Focus(element, method); return; } } - // Couldn't find a focus scope, clear focus. - SetFocusedElement(Scope, null); + if (Scope is object) + { + // Couldn't find a focus scope, clear focus. + SetFocusedElement(Scope, null); + } } } @@ -102,13 +103,13 @@ namespace Avalonia.Input /// public void SetFocusedElement( IFocusScope scope, - IInputElement element, + IInputElement? element, NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None) { - Contract.Requires(scope != null); + scope = scope ?? throw new ArgumentNullException(nameof(scope)); - if (_focusScopes.TryGetValue(scope, out IInputElement existingElement)) + if (_focusScopes.TryGetValue(scope, out var existingElement)) { if (element != existingElement) { @@ -133,11 +134,9 @@ namespace Avalonia.Input /// The new focus scope. public void SetFocusScope(IFocusScope scope) { - Contract.Requires(scope != null); + scope = scope ?? throw new ArgumentNullException(nameof(scope)); - IInputElement e; - - if (!_focusScopes.TryGetValue(scope, out e)) + if (!_focusScopes.TryGetValue(scope, out var e)) { // TODO: Make this do something useful, i.e. select the first focusable // control, select a control that the user has specified to have default @@ -164,17 +163,19 @@ namespace Avalonia.Input /// The focus scopes. private static IEnumerable GetFocusScopeAncestors(IInputElement control) { - while (control != null) + IInputElement? c = control; + + while (c != null) { - var scope = control as IFocusScope; + var scope = c as IFocusScope; - if (scope != null && control.VisualRoot?.IsVisible == true) + if (scope != null && c.VisualRoot?.IsVisible == true) { yield return scope; } - control = control.GetVisualParent() ?? - ((control as IHostedVisualTreeRoot)?.Host as IInputElement); + c = c.GetVisualParent() ?? + ((c as IHostedVisualTreeRoot)?.Host as IInputElement); } } @@ -190,7 +191,7 @@ namespace Avalonia.Input if (sender == e.Source && ev.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { - IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement; + IVisual? element = ev.Pointer?.Captured ?? e.Source as IInputElement; while (element != null) { diff --git a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs index 112abb1a4e..54ef0b1a68 100644 --- a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs +++ b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -1,8 +1,6 @@ -using System; using System.Collections; using System.Collections.Generic; using Avalonia.Controls; -using Avalonia.Data; using Avalonia.LogicalTree; using Avalonia.Styling; @@ -11,8 +9,8 @@ namespace Avalonia.Input.GestureRecognizers public class GestureRecognizerCollection : IReadOnlyCollection, IGestureRecognizerActionsDispatcher { private readonly IInputElement _inputElement; - private List _recognizers; - private Dictionary _pointerGrabs; + private List? _recognizers; + private Dictionary? _pointerGrabs; public GestureRecognizerCollection(IInputElement inputElement) @@ -72,7 +70,7 @@ namespace Avalonia.Input.GestureRecognizers { if (_recognizers == null) return false; - if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) + if (_pointerGrabs!.TryGetValue(e.Pointer, out var capture)) { capture.PointerReleased(e); } @@ -90,7 +88,7 @@ namespace Avalonia.Input.GestureRecognizers { if (_recognizers == null) return false; - if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) + if (_pointerGrabs!.TryGetValue(e.Pointer, out var capture)) { capture.PointerMoved(e); } @@ -108,7 +106,7 @@ namespace Avalonia.Input.GestureRecognizers { if (_recognizers == null) return; - _pointerGrabs.Remove(e.Pointer); + _pointerGrabs!.Remove(e.Pointer); foreach (var r in _recognizers) { r.PointerCaptureLost(e.Pointer); @@ -118,8 +116,8 @@ namespace Avalonia.Input.GestureRecognizers void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer) { pointer.Capture(_inputElement); - _pointerGrabs[pointer] = recognizer; - foreach (var r in _recognizers) + _pointerGrabs![pointer] = recognizer; + foreach (var r in _recognizers!) { if (r != recognizer) r.PointerCaptureLost(pointer); diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs index e022401c8e..3858cc04f2 100644 --- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using Avalonia.Interactivity; using Avalonia.Threading; namespace Avalonia.Input.GestureRecognizers @@ -11,9 +10,9 @@ namespace Avalonia.Input.GestureRecognizers { private bool _scrolling; private Point _trackedRootPoint; - private IPointer _tracking; - private IInputElement _target; - private IGestureRecognizerActionsDispatcher _actions; + private IPointer? _tracking; + private IInputElement? _target; + private IGestureRecognizerActionsDispatcher? _actions; private bool _canHorizontallyScroll; private bool _canVerticallyScroll; private int _gestureId; @@ -95,7 +94,7 @@ namespace Avalonia.Input.GestureRecognizers _scrolling = true; if (_scrolling) { - _actions.Capture(e.Pointer, this); + _actions!.Capture(e.Pointer, this); } } @@ -110,7 +109,7 @@ namespace Avalonia.Input.GestureRecognizers _trackedRootPoint = rootPoint; if (elapsed.TotalSeconds > 0) _inertia = vector / elapsed.TotalSeconds; - _target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); + _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); e.Handled = true; } } @@ -128,7 +127,7 @@ namespace Avalonia.Input.GestureRecognizers { _inertia = default; _scrolling = false; - _target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); + _target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); _gestureId = 0; _lastMoveTimestamp = null; } @@ -165,7 +164,7 @@ namespace Avalonia.Input.GestureRecognizers var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); var distance = speed * elapsedSinceLastTick.TotalSeconds; - _target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); + _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 0efc20b196..5751719d61 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -29,7 +29,9 @@ namespace Avalonia.Input RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. private static WeakReference s_lastPress = new WeakReference(null); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. static Gestures() { @@ -69,6 +71,11 @@ namespace Avalonia.Input private static void PointerPressed(RoutedEventArgs ev) { + if (ev.Source is null) + { + return; + } + if (ev.Route == RoutingStrategies.Bubble) { var e = (PointerPressedEventArgs)ev; @@ -76,7 +83,7 @@ namespace Avalonia.Input if (e.ClickCount <= 1) { - s_lastPress = new WeakReference(e.Source); + s_lastPress = new WeakReference(ev.Source); } else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { diff --git a/src/Avalonia.Input/IAccessKeyHandler.cs b/src/Avalonia.Input/IAccessKeyHandler.cs index 3e6510320f..e484d003c7 100644 --- a/src/Avalonia.Input/IAccessKeyHandler.cs +++ b/src/Avalonia.Input/IAccessKeyHandler.cs @@ -8,7 +8,7 @@ namespace Avalonia.Input /// /// Gets or sets the window's main menu. /// - IMainMenu MainMenu { get; set; } + IMainMenu? MainMenu { get; set; } /// /// Sets the owner of the access key handler. diff --git a/src/Avalonia.Input/IDataObject.cs b/src/Avalonia.Input/IDataObject.cs index 1aa8fd63d5..1db008aa3a 100644 --- a/src/Avalonia.Input/IDataObject.cs +++ b/src/Avalonia.Input/IDataObject.cs @@ -23,17 +23,17 @@ namespace Avalonia.Input /// Returns the dragged text if the DataObject contains any text. /// /// - string GetText(); + string? GetText(); /// /// Returns a list of filenames if the DataObject contains filenames. /// /// - IEnumerable GetFileNames(); + IEnumerable? GetFileNames(); /// /// Tries to get the data of the given DataFormat. /// - object Get(string dataFormat); + object? Get(string dataFormat); } } diff --git a/src/Avalonia.Input/IFocusManager.cs b/src/Avalonia.Input/IFocusManager.cs index 9122cc428d..e1b5087c3d 100644 --- a/src/Avalonia.Input/IFocusManager.cs +++ b/src/Avalonia.Input/IFocusManager.cs @@ -8,12 +8,12 @@ namespace Avalonia.Input /// /// Gets the currently focused . /// - IInputElement Current { get; } + IInputElement? Current { get; } /// /// Gets the current focus scope. /// - IFocusScope Scope { get; } + IFocusScope? Scope { get; } /// /// Focuses a control. @@ -22,7 +22,7 @@ namespace Avalonia.Input /// The method by which focus was changed. /// Any key modifiers active at the time of focus. void Focus( - IInputElement control, + IInputElement? control, NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None); diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index c30d74c965..12fec82368 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -78,7 +78,7 @@ namespace Avalonia.Input /// /// Gets or sets the associated mouse cursor. /// - Cursor Cursor { get; } + Cursor? Cursor { get; } /// /// Gets a value indicating whether this control and all its parents are enabled. diff --git a/src/Avalonia.Input/IInputRoot.cs b/src/Avalonia.Input/IInputRoot.cs index eeb7e4323e..3e2b8cc477 100644 --- a/src/Avalonia.Input/IInputRoot.cs +++ b/src/Avalonia.Input/IInputRoot.cs @@ -20,7 +20,7 @@ namespace Avalonia.Input /// /// Gets or sets the input element that the pointer is currently over. /// - IInputElement PointerOverElement { get; set; } + IInputElement? PointerOverElement { get; set; } /// /// Gets or sets a value indicating whether access keys are shown in the window. @@ -31,6 +31,6 @@ namespace Avalonia.Input /// Gets associated mouse device /// [CanBeNull] - IMouseDevice MouseDevice { get; } + IMouseDevice? MouseDevice { get; } } } diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index ba7e0484ee..9506dc36fb 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -58,10 +58,10 @@ namespace Avalonia.Input public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged { - IInputElement FocusedElement { get; } + IInputElement? FocusedElement { get; } void SetFocusedElement( - IInputElement element, + IInputElement? element, NavigationMethod method, KeyModifiers modifiers); } diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs index a3f051ce7f..7af48cef82 100644 --- a/src/Avalonia.Input/IPointer.cs +++ b/src/Avalonia.Input/IPointer.cs @@ -3,8 +3,8 @@ namespace Avalonia.Input public interface IPointer { int Id { get; } - void Capture(IInputElement control); - IInputElement Captured { get; } + void Capture(IInputElement? control); + IInputElement? Captured { get; } PointerType Type { get; } bool IsPrimary { get; } diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs index bf001dda15..1f82cb1ed7 100644 --- a/src/Avalonia.Input/IPointerDevice.cs +++ b/src/Avalonia.Input/IPointerDevice.cs @@ -6,10 +6,10 @@ namespace Avalonia.Input public interface IPointerDevice : IInputDevice { [Obsolete("Use IPointer")] - IInputElement Captured { get; } + IInputElement? Captured { get; } [Obsolete("Use IPointer")] - void Capture(IInputElement control); + void Capture(IInputElement? control); [Obsolete("Use PointerEventArgs.GetPosition")] Point GetPosition(IVisual relativeTo); diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 0616c70d82..25f2d553d7 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -37,8 +37,8 @@ namespace Avalonia.Input /// /// Gets or sets associated mouse cursor. /// - public static readonly StyledProperty CursorProperty = - AvaloniaProperty.Register(nameof(Cursor), null, true); + public static readonly StyledProperty CursorProperty = + AvaloniaProperty.Register(nameof(Cursor), null, true); /// /// Defines the property. @@ -160,7 +160,7 @@ namespace Avalonia.Input private bool _isFocused; private bool _isFocusVisible; private bool _isPointerOver; - private GestureRecognizerCollection _gestureRecognizers; + private GestureRecognizerCollection? _gestureRecognizers; /// /// Initializes static members of the class. @@ -336,7 +336,7 @@ namespace Avalonia.Input /// /// Gets or sets associated mouse cursor. /// - public Cursor Cursor + public Cursor? Cursor { get { return GetValue(CursorProperty); } set { SetValue(CursorProperty, value); } diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index 4babe711f2..0c7615a472 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -33,7 +33,7 @@ namespace Avalonia.Input /// The element to test. /// The point on . /// The topmost at the specified position. - public static IInputElement InputHitTest(this IInputElement element, Point p) + public static IInputElement? InputHitTest(this IInputElement element, Point p) { Contract.Requires(element != null); diff --git a/src/Avalonia.Input/KeyEventArgs.cs b/src/Avalonia.Input/KeyEventArgs.cs index 267376262b..67cd5a520a 100644 --- a/src/Avalonia.Input/KeyEventArgs.cs +++ b/src/Avalonia.Input/KeyEventArgs.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input { public class KeyEventArgs : RoutedEventArgs { - public IKeyboardDevice Device { get; set; } + public IKeyboardDevice? Device { get; set; } public Key Key { get; set; } diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 0321b0bdf3..187670a26b 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -8,9 +8,9 @@ namespace Avalonia.Input { public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged { - private IInputElement _focusedElement; + private IInputElement? _focusedElement; - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; public static IKeyboardDevice Instance => AvaloniaLocator.Current.GetService(); @@ -18,7 +18,7 @@ namespace Avalonia.Input public IFocusManager FocusManager => AvaloniaLocator.Current.GetService(); - public IInputElement FocusedElement + public IInputElement? FocusedElement { get { @@ -33,7 +33,7 @@ namespace Avalonia.Input } public void SetFocusedElement( - IInputElement element, + IInputElement? element, NavigationMethod method, KeyModifiers keyModifiers) { diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index c425eeeedb..dbefe63789 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -13,7 +13,7 @@ namespace Avalonia.Input /// /// The window to which the handler belongs. /// - private IInputRoot _owner; + private IInputRoot? _owner; /// /// Sets the owner of the keyboard navigation handler. @@ -24,15 +24,12 @@ namespace Avalonia.Input /// public void SetOwner(IInputRoot owner) { - Contract.Requires(owner != null); - if (_owner != null) { throw new InvalidOperationException("AccessKeyHandler owner has already been set."); } - _owner = owner; - + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); _owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown); } @@ -45,11 +42,11 @@ namespace Avalonia.Input /// The next element in the specified direction, or null if /// was the last in the requested direction. /// - public static IInputElement GetNext( + public static IInputElement? GetNext( IInputElement element, NavigationDirection direction) { - Contract.Requires(element != null); + element = element ?? throw new ArgumentNullException(nameof(element)); var customHandler = element.GetSelfAndVisualAncestors() .OfType() @@ -97,7 +94,7 @@ namespace Avalonia.Input NavigationDirection direction, KeyModifiers keyModifiers = KeyModifiers.None) { - Contract.Requires(element != null); + element = element ?? throw new ArgumentNullException(nameof(element)); var next = GetNext(element, direction); diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 188ddd9835..cec5029c18 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -20,7 +20,7 @@ namespace Avalonia.Input private readonly Pointer _pointer; private bool _disposed; - public MouseDevice(Pointer pointer = null) + public MouseDevice(Pointer? pointer = null) { _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); } @@ -34,7 +34,7 @@ namespace Avalonia.Input /// method. /// [Obsolete("Use IPointer instead")] - public IInputElement Captured => _pointer.Captured; + public IInputElement? Captured => _pointer.Captured; /// /// Gets the mouse position, in screen coordinates. @@ -54,7 +54,7 @@ namespace Avalonia.Input /// within the control's bounds or not. The current mouse capture control is exposed /// by the property. /// - public void Capture(IInputElement control) + public void Capture(IInputElement? control) { _pointer.Capture(control); } @@ -66,7 +66,7 @@ namespace Avalonia.Input /// The mouse position in the control's coordinates. public Point GetPosition(IVisual relativeTo) { - Contract.Requires(relativeTo != null); + relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); if (relativeTo.VisualRoot == null) { @@ -75,7 +75,7 @@ namespace Avalonia.Input var rootPoint = relativeTo.VisualRoot.PointToClient(Position); var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); - return rootPoint * transform.Value; + return rootPoint * transform!.Value; } public void ProcessRawEvent(RawInputEventArgs e) @@ -126,7 +126,7 @@ namespace Avalonia.Input private void ProcessRawEvent(RawPointerEventArgs e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); var mouse = (MouseDevice)e.Device; if(mouse._disposed) @@ -173,8 +173,8 @@ namespace Avalonia.Input private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); ClearPointerOver(this, timestamp, root, properties, inputModifiers); } @@ -214,8 +214,8 @@ namespace Avalonia.Input PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var hit = HitTest(root, p); @@ -250,10 +250,10 @@ namespace Avalonia.Input private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); - IInputElement source; + IInputElement? source; if (_pointer.Captured == null) { @@ -265,18 +265,23 @@ namespace Avalonia.Input source = _pointer.Captured; } - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, - p, timestamp, properties, inputModifiers); + if (source is object) + { + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, + p, timestamp, properties, inputModifiers); - source?.RaiseEvent(e); - return e.Handled; + source.RaiseEvent(e); + return e.Handled; + } + + return false; } private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var hit = HitTest(root, p); @@ -298,8 +303,8 @@ namespace Avalonia.Input PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var hit = HitTest(root, p); @@ -317,21 +322,21 @@ namespace Avalonia.Input private IInteractive GetSource(IVisual hit) { - Contract.Requires(hit != null); + hit = hit ?? throw new ArgumentNullException(nameof(hit)); return _pointer.Captured ?? (hit as IInteractive) ?? hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); } - private IInputElement HitTest(IInputElement root, Point p) + private IInputElement? HitTest(IInputElement root, Point p) { - Contract.Requires(root != null); + root = root ?? throw new ArgumentNullException(nameof(root)); return _pointer.Captured ?? root.InputHitTest(p); } - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, + PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, PointerPointProperties properties, KeyModifiers inputModifiers) { @@ -343,8 +348,8 @@ namespace Avalonia.Input PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var element = root.PointerOverElement; var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); @@ -384,12 +389,12 @@ namespace Avalonia.Input } } - private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, + private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var element = root.InputHitTest(p); @@ -412,11 +417,11 @@ namespace Avalonia.Input PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); - Contract.Requires(element != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + element = element ?? throw new ArgumentNullException(nameof(element)); - IInputElement branch = null; + IInputElement? branch = null; var el = element; diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index cd377f1df6..6f6d68940b 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -22,15 +22,17 @@ namespace Avalonia.Input.Navigation /// The next element in the specified direction, or null if /// was the last in the requested direction. /// - public static IInputElement GetNextInTabOrder( + public static IInputElement? GetNextInTabOrder( IInputElement element, NavigationDirection direction, bool outsideElement = false) { - Contract.Requires(element != null); - Contract.Requires( - direction == NavigationDirection.Next || - direction == NavigationDirection.Previous); + element = element ?? throw new ArgumentNullException(nameof(element)); + + if (direction != NavigationDirection.Next && direction != NavigationDirection.Previous) + { + throw new ArgumentException("Invalid direction: must be Next or Previous."); + } var container = element.GetVisualParent(); @@ -110,7 +112,7 @@ namespace Avalonia.Input.Navigation if (customNext.handled) { - yield return customNext.next; + yield return customNext.next!; } else { @@ -143,12 +145,14 @@ namespace Avalonia.Input.Navigation /// If true will not descend into to find next control. /// /// The next element, or null if the element is the last. - private static IInputElement GetNextInContainer( + private static IInputElement? GetNextInContainer( IInputElement element, IInputElement container, NavigationDirection direction, bool outsideElement) { + IInputElement? e = element; + if (direction == NavigationDirection.Next && !outsideElement) { var descendant = GetFocusableDescendants(element, direction).FirstOrDefault(); @@ -167,13 +171,13 @@ namespace Avalonia.Input.Navigation // INavigableContainer. if (navigable != null) { - while (element != null) + while (e != null) { - element = navigable.GetControl(direction, element, false); + e = navigable.GetControl(direction, e, false); - if (element != null && - element.CanFocus() && - KeyboardNavigation.GetIsTabStop((InputElement) element)) + if (e != null && + e.CanFocus() && + KeyboardNavigation.GetIsTabStop((InputElement)e)) { break; } @@ -183,12 +187,12 @@ namespace Avalonia.Input.Navigation { // TODO: Do a spatial search here if the container doesn't implement // INavigableContainer. - element = null; + e = null; } - if (element != null && direction == NavigationDirection.Previous) + if (e != null && direction == NavigationDirection.Previous) { - var descendant = GetFocusableDescendants(element, direction).LastOrDefault(); + var descendant = GetFocusableDescendants(e, direction).LastOrDefault(); if (descendant != null) { @@ -196,7 +200,7 @@ namespace Avalonia.Input.Navigation } } - return element; + return e; } return null; @@ -209,13 +213,13 @@ namespace Avalonia.Input.Navigation /// The container. /// The direction of the search. /// The first element, or null if there are no more elements. - private static IInputElement GetFirstInNextContainer( + private static IInputElement? GetFirstInNextContainer( IInputElement element, IInputElement container, NavigationDirection direction) { var parent = container.GetVisualParent(); - IInputElement next = null; + IInputElement? next = null; if (parent != null) { @@ -268,7 +272,7 @@ namespace Avalonia.Input.Navigation return next; } - private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, + private static (bool handled, IInputElement? next) GetCustomNext(IInputElement element, NavigationDirection direction) { if (element is ICustomKeyboardNavigation custom) diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index 00222e92cf..a477711584 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/src/Avalonia.Input/Pointer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Interactivity; using Avalonia.VisualTree; namespace Avalonia.Input @@ -20,7 +19,7 @@ namespace Avalonia.Input public int Id { get; } - IInputElement FindCommonParent(IInputElement control1, IInputElement control2) + IInputElement? FindCommonParent(IInputElement? control1, IInputElement? control2) { if (control1 == null || control2 == null) return null; @@ -28,12 +27,12 @@ namespace Avalonia.Input return control2.GetSelfAndVisualAncestors().OfType().FirstOrDefault(seen.Contains); } - protected virtual void PlatformCapture(IInputElement element) + protected virtual void PlatformCapture(IInputElement? element) { } - public void Capture(IInputElement control) + public void Capture(IInputElement? control) { if (Captured != null) Captured.DetachedFromVisualTree -= OnCaptureDetached; @@ -66,7 +65,7 @@ namespace Avalonia.Input } - public IInputElement Captured { get; private set; } + public IInputElement? Captured { get; private set; } public PointerType Type { get; } public bool IsPrimary { get; } diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 9cc42ffa69..1cbddf89aa 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -7,14 +7,14 @@ namespace Avalonia.Input { public class PointerEventArgs : RoutedEventArgs { - private readonly IVisual _rootVisual; + private readonly IVisual? _rootVisual; private readonly Point _rootVisualPosition; private readonly PointerPointProperties _properties; public PointerEventArgs(RoutedEvent routedEvent, - IInteractive source, + IInteractive? source, IPointer pointer, - IVisual rootVisual, Point rootVisualPosition, + IVisual? rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers) @@ -40,8 +40,8 @@ namespace Avalonia.Input public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException(); - public IInputElement Captured => _ev.Pointer.Captured; - public void Capture(IInputElement control) + public IInputElement? Captured => _ev.Pointer.Captured; + public void Capture(IInputElement? control) { _ev.Pointer.Capture(control); } @@ -52,7 +52,7 @@ namespace Avalonia.Input public IPointer Pointer { get; } public ulong Timestamp { get; } - private IPointerDevice _device; + private IPointerDevice? _device; [Obsolete("Use Pointer to get pointer-specific information")] public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this)); @@ -76,7 +76,7 @@ namespace Avalonia.Input public KeyModifiers KeyModifiers { get; } - public Point GetPosition(IVisual relativeTo) + public Point GetPosition(IVisual? relativeTo) { if (_rootVisual == null) return default; diff --git a/src/Avalonia.Input/Raw/RawDragEventType.cs b/src/Avalonia.Input/Raw/RawDragEventType.cs index 9635f77467..77f17a5a41 100644 --- a/src/Avalonia.Input/Raw/RawDragEventType.cs +++ b/src/Avalonia.Input/Raw/RawDragEventType.cs @@ -7,4 +7,4 @@ DragLeave, Drop } -} \ No newline at end of file +} diff --git a/src/Avalonia.Input/Raw/RawInputEventArgs.cs b/src/Avalonia.Input/Raw/RawInputEventArgs.cs index b85563b24a..dcc5f27a79 100644 --- a/src/Avalonia.Input/Raw/RawInputEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawInputEventArgs.cs @@ -21,7 +21,7 @@ namespace Avalonia.Input.Raw /// The root from which the event originates. public RawInputEventArgs(IInputDevice device, ulong timestamp, IInputRoot root) { - Contract.Requires(device != null); + device = device ?? throw new ArgumentNullException(nameof(device)); Device = device; Timestamp = timestamp; diff --git a/src/Avalonia.Input/TextInputEventArgs.cs b/src/Avalonia.Input/TextInputEventArgs.cs index 6e763d3b56..cda0103749 100644 --- a/src/Avalonia.Input/TextInputEventArgs.cs +++ b/src/Avalonia.Input/TextInputEventArgs.cs @@ -4,8 +4,8 @@ namespace Avalonia.Input { public class TextInputEventArgs : RoutedEventArgs { - public IKeyboardDevice Device { get; set; } + public IKeyboardDevice? Device { get; set; } - public string Text { get; set; } + public string? Text { get; set; } } } From 86aad32bee708db2b93bcfdac87f1af620c089ca Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 20 Aug 2020 18:23:19 +0200 Subject: [PATCH 04/60] Adding support for regex filter of devtools properties --- .../ViewModels/ControlDetailsViewModel.cs | 31 ++++++------- .../ViewModels/TreePageViewModel.cs | 34 +++++++++++--- .../Diagnostics/Views/ControlDetailsView.xaml | 45 +++++++++++-------- 3 files changed, 67 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 4e7129da41..c8f618580b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Text.RegularExpressions; using Avalonia.Collections; using Avalonia.VisualTree; @@ -12,12 +13,13 @@ namespace Avalonia.Diagnostics.ViewModels private readonly IVisual _control; private readonly IDictionary> _propertyIndex; private AvaloniaPropertyViewModel _selectedProperty; - private string _propertyFilter; - public ControlDetailsViewModel(IVisual control, string propertyFilter) + public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) { _control = control; + TreePage = treePage; + var properties = GetAvaloniaProperties(control) .Concat(GetClrProperties(control)) .OrderBy(x => x, PropertyComparer.Instance) @@ -25,7 +27,6 @@ namespace Avalonia.Diagnostics.ViewModels .ToList(); _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList()); - _propertyFilter = propertyFilter; var view = new DataGridCollectionView(properties); view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); @@ -43,19 +44,9 @@ namespace Avalonia.Diagnostics.ViewModels } } - public DataGridCollectionView PropertiesView { get; } + public TreePageViewModel TreePage { get; } - public string PropertyFilter - { - get => _propertyFilter; - set - { - if (RaiseAndSetIfChanged(ref _propertyFilter, value)) - { - PropertiesView.Refresh(); - } - } - } + public DataGridCollectionView PropertiesView { get; } public AvaloniaPropertyViewModel SelectedProperty { @@ -137,9 +128,15 @@ namespace Avalonia.Diagnostics.ViewModels private bool FilterProperty(object arg) { - if (!string.IsNullOrWhiteSpace(PropertyFilter) && arg is PropertyViewModel property) + if (!string.IsNullOrWhiteSpace(TreePage.PropertyFilter) && arg is PropertyViewModel property) { - return property.Name.IndexOf(PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1; + if (TreePage.UseRegexFilter) + { + var regex = new Regex(TreePage.PropertyFilter); + return regex.IsMatch(property.Name); + } + + return property.Name.IndexOf(TreePage.PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1; } return true; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index ec48cff399..77594570ed 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -8,7 +8,8 @@ namespace Avalonia.Diagnostics.ViewModels { private TreeNode _selectedNode; private ControlDetailsViewModel _details; - private string _propertyFilter; + private string _propertyFilter = string.Empty; + private bool _useRegexFilter; public TreePageViewModel(TreeNode[] nodes) { @@ -34,15 +35,10 @@ namespace Avalonia.Diagnostics.ViewModels get => _selectedNode; private set { - if (Details != null) - { - _propertyFilter = Details.PropertyFilter; - } - if (RaiseAndSetIfChanged(ref _selectedNode, value)) { Details = value != null ? - new ControlDetailsViewModel(value.Visual, _propertyFilter) : + new ControlDetailsViewModel(this, value.Visual) : null; } } @@ -62,6 +58,30 @@ namespace Avalonia.Diagnostics.ViewModels } } + public string PropertyFilter + { + get => _propertyFilter; + set + { + if (RaiseAndSetIfChanged(ref _propertyFilter, value)) + { + Details.PropertiesView.Refresh(); + } + } + } + + public bool UseRegexFilter + { + get => _useRegexFilter; + set + { + if (RaiseAndSetIfChanged(ref _useRegexFilter, value)) + { + Details.PropertiesView.Refresh(); + } + } + } + public void Dispose() { foreach (var node in Nodes) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 29ce26fcdf..b2d3a8bddb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -2,24 +2,31 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters" x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"> - - - - - - - - - - - - + + + + + + + + + + + + + + + From 2ab0f80459101628a00f310d86d10efc0f818c0c Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 20 Aug 2020 18:41:48 +0200 Subject: [PATCH 05/60] Reduce overhead of compiling regex Use INotifyDataErrorInfo to signal error --- .../ViewModels/ControlDetailsViewModel.cs | 3 +- .../ViewModels/TreePageViewModel.cs | 60 ++++++++++++++++--- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index c8f618580b..b4243c6088 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -132,8 +132,7 @@ namespace Avalonia.Diagnostics.ViewModels { if (TreePage.UseRegexFilter) { - var regex = new Regex(TreePage.PropertyFilter); - return regex.IsMatch(property.Name); + return TreePage.FilterRegex?.IsMatch(property.Name) ?? true; } return property.Name.IndexOf(TreePage.PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 77594570ed..da2fd25c0c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -1,11 +1,16 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text.RegularExpressions; using Avalonia.Controls; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class TreePageViewModel : ViewModelBase, IDisposable + internal class TreePageViewModel : ViewModelBase, IDisposable, INotifyDataErrorInfo { + private readonly Dictionary _errors = new Dictionary(); private TreeNode _selectedNode; private ControlDetailsViewModel _details; private string _propertyFilter = string.Empty; @@ -14,17 +19,13 @@ namespace Avalonia.Diagnostics.ViewModels public TreePageViewModel(TreeNode[] nodes) { Nodes = nodes; - Selection = new SelectionModel - { - SingleSelect = true, - Source = Nodes - }; + Selection = new SelectionModel { SingleSelect = true, Source = Nodes }; Selection.SelectionChanged += (s, e) => { SelectedNode = (TreeNode)Selection.SelectedItem; }; - } + } public TreeNode[] Nodes { get; protected set; } @@ -58,6 +59,37 @@ namespace Avalonia.Diagnostics.ViewModels } } + public Regex FilterRegex { get; set; } + + private void UpdateFilterRegex() + { + void ClearError() + { + if (_errors.Remove(nameof(PropertyFilter))) + { + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter))); + } + } + + if (UseRegexFilter) + { + try + { + FilterRegex = new Regex(PropertyFilter); + ClearError(); + } + catch (Exception exception) + { + _errors[nameof(PropertyFilter)] = exception.Message; + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter))); + } + } + else + { + ClearError(); + } + } + public string PropertyFilter { get => _propertyFilter; @@ -65,6 +97,7 @@ namespace Avalonia.Diagnostics.ViewModels { if (RaiseAndSetIfChanged(ref _propertyFilter, value)) { + UpdateFilterRegex(); Details.PropertiesView.Refresh(); } } @@ -77,6 +110,7 @@ namespace Avalonia.Diagnostics.ViewModels { if (RaiseAndSetIfChanged(ref _useRegexFilter, value)) { + UpdateFilterRegex(); Details.PropertiesView.Refresh(); } } @@ -158,5 +192,17 @@ namespace Avalonia.Diagnostics.ViewModels return null; } + + public IEnumerable GetErrors(string propertyName) + { + if (_errors.TryGetValue(propertyName, out var error)) + { + yield return error; + } + } + + public bool HasErrors => _errors.Count > 0; + + public event EventHandler ErrorsChanged; } } From a3b27632ddcf5f801b2358a031d28fc28ed5d9cb Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 20 Aug 2020 23:00:10 +0300 Subject: [PATCH 06/60] Add .Events projects --- global.json | 2 +- .../Avalonia.ReactiveUI.Events.csproj | 12 ++++++++++++ .../Avalonia.ReactiveUI.Events.UnitTests.csproj | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj create mode 100644 tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj diff --git a/global.json b/global.json index a3fdca9bed..e26c2c7f2c 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.101" + "version": "3.1.401" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", diff --git a/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj b/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj new file mode 100644 index 0000000000..b95c8946e2 --- /dev/null +++ b/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj @@ -0,0 +1,12 @@ + + + netstandard2.0 + Avalonia.ReactiveUI + + + + + + + + diff --git a/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj b/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj new file mode 100644 index 0000000000..d162a2abe0 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj @@ -0,0 +1,15 @@ + + + netcoreapp3.1 + + + + + + + + + + + + From d683bbbffc019c57ed3de0e431172bfbbcb08fae Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 20 Aug 2020 23:36:42 +0300 Subject: [PATCH 07/60] Add Avalonia.ReactiveUI.Events based on Pharmacist --- Avalonia.sln | 26 ++++++++++++ nukebuild/Build.cs | 40 ++++++++++++++++++- nukebuild/_build.csproj | 1 + ...valonia.ReactiveUI.Events.UnitTests.csproj | 15 ------- 4 files changed, 66 insertions(+), 16 deletions(-) delete mode 100644 tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj diff --git a/Avalonia.sln b/Avalonia.sln index ffd7b4d4f4..71ed289241 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -224,6 +224,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.Vnc", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -2035,6 +2037,30 @@ Global {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.Build.0 = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.ActiveCfg = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index fbfbf47e1b..8c40a2c45b 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; using System.Xml.Linq; using Nuke.Common; using Nuke.Common.Git; @@ -15,6 +16,7 @@ using Nuke.Common.Tools.MSBuild; using Nuke.Common.Tools.Npm; using Nuke.Common.Utilities; using Nuke.Common.Utilities.Collections; +using Pharmacist.Core; using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; @@ -139,7 +141,7 @@ partial class Build : NukeBuild Target Compile => _ => _ .DependsOn(Clean) .DependsOn(CompileHtmlPreviewer) - .Executes(() => + .Executes(async () => { if (Parameters.IsRunningOnWindows) MsBuildCommon(Parameters.MSBuildSolution, c => c @@ -153,8 +155,44 @@ partial class Build : NukeBuild .AddProperty("PackageVersion", Parameters.Version) .SetConfiguration(Parameters.Configuration) ); + + await CompileReactiveEvents(); }); + async Task CompileReactiveEvents() + { + var avaloniaBuildOutput = Path.Combine(RootDirectory, "packages", "Avalonia", "bin", Parameters.Configuration); + var avaloniaAssemblies = GlobFiles(avaloniaBuildOutput, "**/Avalonia*.dll") + .Where(file => !file.Contains("Avalonia.Build.Tasks") && + !file.Contains("Avalonia.Remote.Protocol")); + + var eventsDirectory = GlobDirectories($"{RootDirectory}/src/**/Avalonia.ReactiveUI.Events").First(); + var eventsBuildFile = Path.Combine(eventsDirectory, "Events_Avalonia.cs"); + if (File.Exists(eventsBuildFile)) + File.Delete(eventsBuildFile); + + using (var stream = File.Create(eventsBuildFile)) + using (var writer = new StreamWriter(stream)) + { + await ObservablesForEventGenerator.ExtractEventsFromAssemblies( + writer, avaloniaAssemblies, new string[0], "netstandard2.0" + ); + } + + var eventsProject = Path.Combine(eventsDirectory, "Avalonia.ReactiveUI.Events.csproj"); + if (Parameters.IsRunningOnWindows) + MsBuildCommon(eventsProject, c => c + .SetArgumentConfigurator(a => a.Add("/r")) + .AddTargets("Build") + ); + else + DotNetBuild(c => c + .SetProjectFile(eventsProject) + .AddProperty("PackageVersion", Parameters.Version) + .SetConfiguration(Parameters.Configuration) + ); + } + void RunCoreTest(string projectName) { Information($"Running tests from {projectName}"); diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 4c64d4ff93..b06e49f2eb 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -17,6 +17,7 @@ + diff --git a/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj b/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj deleted file mode 100644 index d162a2abe0..0000000000 --- a/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - netcoreapp3.1 - - - - - - - - - - - - From e3e001e12b174569c25b11353a1aecae5dc961bb Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 21 Aug 2020 13:56:24 +0300 Subject: [PATCH 08/60] Use latest SDK and runtime --- azure-pipelines.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 54645e461e..172fe9cb2b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -35,16 +35,16 @@ jobs: vmImage: 'macOS-10.14' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.101' + displayName: 'Use .NET Core SDK 3.1.401' inputs: packageType: sdk - version: 3.1.101 + version: 3.1.401 - task: UseDotNet@2 - displayName: 'Use .NET Core Runtime 3.1.1' + displayName: 'Use .NET Core Runtime 3.1.7' inputs: packageType: runtime - version: 3.1.1 + version: 3.1.7 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -112,6 +112,18 @@ jobs: pool: vmImage: 'windows-2019' steps: + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 3.1.401' + inputs: + packageType: sdk + version: 3.1.401 + + - task: UseDotNet@2 + displayName: 'Use .NET Core Runtime 3.1.7' + inputs: + packageType: runtime + version: 3.1.7 + - task: CmdLine@2 displayName: 'Install Nuke' inputs: From ded3a566c0e301bdbf871d3cedd7196c87f5d89d Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 21 Aug 2020 14:42:44 +0300 Subject: [PATCH 09/60] Apply DotNetCoreInstaller workaround --- azure-pipelines.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 172fe9cb2b..a267452ff4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -112,18 +112,11 @@ jobs: pool: vmImage: 'windows-2019' steps: - - task: UseDotNet@2 + - task: DotNetCoreInstaller@0 displayName: 'Use .NET Core SDK 3.1.401' inputs: - packageType: sdk version: 3.1.401 - - task: UseDotNet@2 - displayName: 'Use .NET Core Runtime 3.1.7' - inputs: - packageType: runtime - version: 3.1.7 - - task: CmdLine@2 displayName: 'Install Nuke' inputs: From 9dd34660bf3b236eb7006d80498e790e4b6d95c1 Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 21 Aug 2020 15:15:26 +0300 Subject: [PATCH 10/60] Try using .NET Core 3.1.302 --- azure-pipelines.yml | 17 +++++------------ global.json | 2 +- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a267452ff4..20278ddc71 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,17 +34,10 @@ jobs: pool: vmImage: 'macOS-10.14' steps: - - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.401' - inputs: - packageType: sdk - version: 3.1.401 - - - task: UseDotNet@2 - displayName: 'Use .NET Core Runtime 3.1.7' + - task: DotNetCoreInstaller@0 + displayName: 'Use .NET Core SDK 3.1.302' inputs: - packageType: runtime - version: 3.1.7 + version: 3.1.302 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -113,9 +106,9 @@ jobs: vmImage: 'windows-2019' steps: - task: DotNetCoreInstaller@0 - displayName: 'Use .NET Core SDK 3.1.401' + displayName: 'Use .NET Core SDK 3.1.302' inputs: - version: 3.1.401 + version: 3.1.302 - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/global.json b/global.json index e26c2c7f2c..2f594d9596 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.401" + "version": "3.1.302" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", From 59b0f81aad47b9bea2f32b23c754e6cb8eea8c7b Mon Sep 17 00:00:00 2001 From: artyom Date: Sat, 22 Aug 2020 21:56:59 +0300 Subject: [PATCH 11/60] Add a smoke test --- Avalonia.sln | 3 ++ dirs.proj | 1 + nukebuild/Build.cs | 1 + ...valonia.ReactiveUI.Events.UnitTests.csproj | 15 +++++++ .../BasicControlEventsTest.cs | 44 +++++++++++++++++++ 5 files changed, 64 insertions(+) create mode 100644 tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj create mode 100644 tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs diff --git a/Avalonia.sln b/Avalonia.sln index 71ed289241..551eb5925e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -226,6 +226,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Markup.Xaml.Loader EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -2120,6 +2122,7 @@ Global {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/dirs.proj b/dirs.proj index 26c8f54b23..086a22a77b 100644 --- a/dirs.proj +++ b/dirs.proj @@ -7,6 +7,7 @@ + diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 8c40a2c45b..a46b55f6f3 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -240,6 +240,7 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.Visuals.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.ReactiveUI.Events.UnitTests"); }); Target RunRenderTests => _ => _ diff --git a/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj b/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj new file mode 100644 index 0000000000..19a6fd138e --- /dev/null +++ b/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj @@ -0,0 +1,15 @@ + + + netcoreapp3.1 + + + + + + + + + + + + diff --git a/tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs b/tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs new file mode 100644 index 0000000000..1092c98246 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs @@ -0,0 +1,44 @@ +using System; +using System.Reactive.Linq; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.ReactiveUI.Events.UnitTests +{ + public class BasicControlEventsTest + { + public class EventsControl : UserControl + { + public bool IsAttached { get; private set; } + + public EventsControl() + { + var attached = this + .Events() + .AttachedToVisualTree + .Select(args => true); + + this.Events() + .DetachedFromVisualTree + .Select(args => false) + .Merge(attached) + .Subscribe(marker => IsAttached = marker); + } + } + + [Fact] + public void Should_Generate_Events_Wrappers() + { + var root = new TestRoot(); + var control = new EventsControl(); + Assert.False(control.IsAttached); + + root.Child = control; + Assert.True(control.IsAttached); + + root.Child = null; + Assert.False(control.IsAttached); + } + } +} From ba8e6f029ec3befb5514ee860627844e91cab984 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 28 Aug 2020 11:43:11 +0200 Subject: [PATCH 12/60] Compile regex --- .../Diagnostics/ViewModels/ControlDetailsViewModel.cs | 1 - .../Diagnostics/ViewModels/TreePageViewModel.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index b4243c6088..764d699658 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Text.RegularExpressions; using Avalonia.Collections; using Avalonia.VisualTree; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 53abba8906..bd65a3b06b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -70,7 +70,7 @@ namespace Avalonia.Diagnostics.ViewModels { try { - FilterRegex = new Regex(PropertyFilter); + FilterRegex = new Regex(PropertyFilter, RegexOptions.Compiled); ClearError(); } catch (Exception exception) From bc0ada009461679ef8517cc30ad1b2e5c0165576 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Mon, 31 Aug 2020 18:35:40 +0200 Subject: [PATCH 13/60] Preferring user-provided string conversion over TypeDescriptor --- src/Avalonia.Controls/ColumnDefinitions.cs | 10 ++++- .../ViewModels/PropertyViewModel.cs | 37 ++++++++++++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/ColumnDefinitions.cs b/src/Avalonia.Controls/ColumnDefinitions.cs index ed4f9dbe99..7e355ab357 100644 --- a/src/Avalonia.Controls/ColumnDefinitions.cs +++ b/src/Avalonia.Controls/ColumnDefinitions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Specialized; using System.Linq; +using System.Text; using Avalonia.Collections; namespace Avalonia.Controls @@ -13,7 +14,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public ColumnDefinitions() : base () + public ColumnDefinitions() { } @@ -27,6 +28,11 @@ namespace Avalonia.Controls AddRange(GridLength.ParseLengths(s).Select(x => new ColumnDefinition(x))); } + public override string ToString() + { + return string.Join(",", this.Select(x => x.Width)); + } + /// /// Parses a string representation of column definitions collection. /// @@ -34,4 +40,4 @@ namespace Avalonia.Controls /// The . public static ColumnDefinitions Parse(string s) => new ColumnDefinitions(s); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index ddbdae7ed9..2d69764ae8 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Reflection; @@ -8,8 +9,8 @@ namespace Avalonia.Diagnostics.ViewModels internal abstract class PropertyViewModel : ViewModelBase { private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; - private static readonly Type[] StringParameter = new[] { typeof(string) }; - private static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) }; + private static readonly Type[] StringParameter = { typeof(string) }; + private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; public abstract object Key { get; } public abstract string Name { get; } @@ -25,19 +26,37 @@ namespace Avalonia.Diagnostics.ViewModels return "(null)"; } - var converter = TypeDescriptor.GetConverter(value); - return converter?.ConvertToString(value) ?? value.ToString(); + //Check if there's an user provided ToString(), prefer that over the TypeDescriptor conversion + if (value.GetType().GetMethod(nameof(ToString), System.Type.EmptyTypes) + .DeclaringType != typeof(object)) + { + return value.ToString(); + } + + try + { + var converter = TypeDescriptor.GetConverter(value); + + return converter.ConvertToString(value); + } + catch + { + return value.ToString(); + } } protected static object ConvertFromString(string s, Type targetType) { - var converter = TypeDescriptor.GetConverter(targetType); - - if (converter != null && converter.CanConvertFrom(typeof(string))) + try { - return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); + var converter = TypeDescriptor.GetConverter(targetType); + + if (converter.CanConvertFrom(typeof(string))) + { + return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); + } } - else + catch { var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); From a2a0fba1c2134b552080c83b0fdcc9f4872847e5 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 1 Sep 2020 07:44:38 +0200 Subject: [PATCH 14/60] Ignore invisible characters --- src/Avalonia.Visuals/ApiCompatBaseline.txt | 5 ++++ .../Media/TextFormatting/TextLayout.cs | 6 ++--- .../Media/TextFormatting/TextLine.cs | 4 +-- .../Media/TextFormatting/TextLineImpl.cs | 17 +++++++++--- .../Media/TextFormatting/TextLineTests.cs | 26 +++++++++++++++++++ 5 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 src/Avalonia.Visuals/ApiCompatBaseline.txt diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt new file mode 100644 index 0000000000..5058cff26d --- /dev/null +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -0,0 +1,5 @@ +Compat issues with assembly Avalonia.Visuals: +CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.LineBreak.get()' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak.get()' is abstract in the implementation but is missing in the contract. +Total Issues: 3 diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index fa7d6cb4bf..df1ecb4067 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -221,7 +221,7 @@ namespace Avalonia.Media.TextFormatting while (currentPosition < _text.Length) { var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth, - _paragraphProperties, previousLine?.LineBreak); + _paragraphProperties, previousLine?.TextLineBreak); currentPosition += textLine.TextRange.Length; @@ -230,7 +230,7 @@ namespace Avalonia.Media.TextFormatting if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) && height + textLine.LineMetrics.Size.Height > MaxHeight) { - if (previousLine?.LineBreak != null && _textTrimming != TextTrimming.None) + if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None) { var collapsedLine = previousLine.Collapse(GetCollapsingProperties(MaxWidth)); @@ -255,7 +255,7 @@ namespace Avalonia.Media.TextFormatting previousLine = textLine; - if (currentPosition != _text.Length || textLine.LineBreak == null) + if (currentPosition != _text.Length || textLine.TextLineBreak == null) { continue; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs index 423ca9fb7f..c052fb8948 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs @@ -35,9 +35,9 @@ namespace Avalonia.Media.TextFormatting /// Gets the state of the line when broken by line breaking process. /// /// - /// A value that represents the line break. + /// A value that represents the line break. /// - public abstract TextLineBreak LineBreak { get; } + public abstract TextLineBreak TextLineBreak { get; } /// /// Gets a value that indicates whether the line is collapsed. diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index 08d9107bb1..51092cddda 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -13,7 +13,7 @@ namespace Avalonia.Media.TextFormatting { _textRuns = textRuns; LineMetrics = lineMetrics; - LineBreak = lineBreak; + TextLineBreak = lineBreak; HasCollapsed = hasCollapsed; } @@ -27,7 +27,7 @@ namespace Avalonia.Media.TextFormatting public override TextLineMetrics LineMetrics { get; } /// - public override TextLineBreak LineBreak { get; } + public override TextLineBreak TextLineBreak { get; } /// public override bool HasCollapsed { get; } @@ -122,7 +122,7 @@ namespace Avalonia.Media.TextFormatting textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height), LineMetrics.TextBaseline, textRange, false); - return new TextLineImpl(shapedTextCharacters, textLineMetrics, LineBreak, true); + return new TextLineImpl(shapedTextCharacters, textLineMetrics, TextLineBreak, true); } availableWidth -= currentRun.GlyphRun.Bounds.Width; @@ -268,6 +268,17 @@ namespace Avalonia.Media.TextFormatting var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == TextRange.Length; + var characterIndex = codepointIndex - run.Text.Start; + + var codepoint = Codepoint.ReadAt(run.GlyphRun.Characters, characterIndex, out _); + + if (codepoint.IsBreakChar) + { + foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(codepointIndex - 1, out _); + + isAtEnd = true; + } + nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ? foundCharacterHit : new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 7abfe29f11..8f1bd5979a 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Globalization; using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; @@ -331,6 +333,30 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Ignore_Invisible_Characters() + { + using (Start()) + { + var defaultTextRunProperties = + new GenericTextRunProperties(Typeface.Default); + + const string text = "01234567🎉\n"; + + var source = new SingleBufferTextSource(text, defaultTextRunProperties); + + var textParagraphProperties = new GenericTextParagraphProperties(defaultTextRunProperties); + + var formatter = TextFormatter.Current; + + var textLine = formatter.FormatLine(source, 0, double.PositiveInfinity, textParagraphProperties); + + var nextCharacterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(8, 2)); + + Assert.Equal(new CharacterHit(8, 2), nextCharacterHit); + } + } + private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface From 3cdfa305b59e2e9eb2784b4da470eb5008ca14c1 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 1 Sep 2020 10:27:30 +0200 Subject: [PATCH 15/60] typo, removed using --- .../Diagnostics/ViewModels/PropertyViewModel.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index 2d69764ae8..bd60a46b95 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Reflection; @@ -26,7 +25,7 @@ namespace Avalonia.Diagnostics.ViewModels return "(null)"; } - //Check if there's an user provided ToString(), prefer that over the TypeDescriptor conversion + //Check if there's an user-provided ToString(), prefer that over the TypeDescriptor conversion if (value.GetType().GetMethod(nameof(ToString), System.Type.EmptyTypes) .DeclaringType != typeof(object)) { From faad15571ac2beca8b78063c04391ab9b2a809d3 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 1 Sep 2020 15:48:16 +0200 Subject: [PATCH 16/60] Filter out CollectionConverter Making sure to call Parse when conversion with TypeConverter is not possible --- .../ViewModels/PropertyViewModel.cs | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index bd60a46b95..e23d6f1471 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -25,54 +25,47 @@ namespace Avalonia.Diagnostics.ViewModels return "(null)"; } - //Check if there's an user-provided ToString(), prefer that over the TypeDescriptor conversion - if (value.GetType().GetMethod(nameof(ToString), System.Type.EmptyTypes) - .DeclaringType != typeof(object)) + var converter = TypeDescriptor.GetConverter(value); + + //CollectionConverter does not deliver any important information. It just displays "(Collection)". + if (!converter.CanConvertTo(typeof(string)) || + converter.GetType() == typeof(CollectionConverter)) { return value.ToString(); } - try - { - var converter = TypeDescriptor.GetConverter(value); + return converter.ConvertToString(value); + } + + private static object InvokeParse(string s, Type targetType) + { + var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); - return converter.ConvertToString(value); + if (method != null) + { + return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture }); } - catch + + method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null); + + if (method != null) { - return value.ToString(); + return method.Invoke(null, new object[] { s }); } + + throw new InvalidCastException("Unable to convert value."); } protected static object ConvertFromString(string s, Type targetType) { - try - { - var converter = TypeDescriptor.GetConverter(targetType); + var converter = TypeDescriptor.GetConverter(targetType); - if (converter.CanConvertFrom(typeof(string))) - { - return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); - } - } - catch + if (converter.CanConvertFrom(typeof(string))) { - var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); - - if (method != null) - { - return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture }); - } - - method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null); - - if (method != null) - { - return method.Invoke(null, new object[] { s }); - } + return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); } - throw new InvalidCastException("Unable to convert value."); + return InvokeParse(s, targetType); } } } From 525dbbb2e7e16afb2e86b4f1a64bc209168ed3e6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 1 Sep 2020 16:54:17 +0200 Subject: [PATCH 17/60] Addeding failing selection tests. --- .../Primitives/SelectingItemsControlTests.cs | 63 +++++++++++++++++++ .../Selection/SelectionModelTests_Multiple.cs | 32 ++++++++++ .../Selection/SelectionModelTests_Single.cs | 31 +++++++++ 3 files changed, 126 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 33744949c3..00d148093a 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1335,6 +1335,47 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.False(model.SingleSelect); } + [Fact] + public void Does_The_Best_It_Can_With_AutoSelecting_ViewModel() + { + // Tests the following scenario: + // + // - Items changes from empty to having 1 item + // - ViewModel auto-selects item 0 in CollectionChanged + // - SelectionModel receives CollectionChanged + // - And so adjusts the selected item from 0 to 1, which is past the end of the items. + // + // There's not much we can do about this situation because the order in which + // CollectionChanged handlers are called can't be known (the problem also exists with + // WPF). The best we can do is not select an invalid index. + var vm = new SelectionViewModel(); + + vm.Items.CollectionChanged += (s, e) => + { + if (vm.SelectedIndex == -1 && vm.Items.Count > 0) + { + vm.SelectedIndex = 0; + } + }; + + var target = new ListBox + { + [!ListBox.ItemsProperty] = new Binding("Items"), + [!ListBox.SelectedIndexProperty] = new Binding("SelectedIndex"), + DataContext = vm, + }; + + Prepare(target); + + vm.Items.Add("foo"); + vm.Items.Add("bar"); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { 0 }, target.Selection.SelectedIndexes); + Assert.Equal("foo", target.SelectedItem); + Assert.Equal(new[] { "foo" }, target.SelectedItems); + } + private static void Prepare(SelectingItemsControl target) { var root = new TestRoot @@ -1397,6 +1438,28 @@ namespace Avalonia.Controls.UnitTests.Primitives public int SelectedIndex { get; set; } } + private class SelectionViewModel : NotifyingBase + { + private int _selectedIndex = -1; + + public SelectionViewModel() + { + Items = new ObservableCollection(); + } + + public int SelectedIndex + { + get => _selectedIndex; + set + { + _selectedIndex = value; + RaisePropertyChanged(); + } + } + + public ObservableCollection Items { get; } + } + private class RootWithItems : TestRoot { public List Items { get; set; } = new List() { "a", "b", "c", "d", "e" }; diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs index 3640faf7cb..3eddd35465 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs @@ -1230,6 +1230,38 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(1, resetRaised); Assert.Equal(1, selectedIndexRaised); } + + [Fact] + public void Handles_Selection_Made_In_CollectionChanged() + { + // Tests the following scenario: + // + // - Items changes from empty to having 2 items + // - ViewModel auto-selects range 0..1 in CollectionChanged + // - SelectionModel receives CollectionChanged + // - And so adjusts the selected item from 0..1 to 2..4, which is past the end of + // the items. + // + // There's not much we can do about this situation because the order in which + // CollectionChanged handlers are called can't be known (the problem also exists with + // WPF). The best we can do is not select an invalid index. + var target = CreateTarget(createData: false); + var data = new AvaloniaList(); + + data.CollectionChanged += (s, e) => + { + target.SelectRange(0, 1); + }; + + target.Source = data; + data.AddRange(new[] { "foo", "bar" }); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { 0, 1 }, target.SelectedIndexes); + Assert.Equal("foo", target.SelectedItem); + Assert.Equal(new[] { "foo", "bar" }, target.SelectedItems); + Assert.Equal(0, target.AnchorIndex); + } } public class BatchUpdate diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index 345518e729..1b37730797 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs @@ -1052,6 +1052,37 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(1, resetRaised); Assert.Equal(1, selectedIndexRaised); } + + [Fact] + public void Handles_Selection_Made_In_CollectionChanged() + { + // Tests the following scenario: + // + // - Items changes from empty to having 1 item + // - ViewModel auto-selects item 0 in CollectionChanged + // - SelectionModel receives CollectionChanged + // - And so adjusts the selected item from 0 to 1, which is past the end of the items. + // + // There's not much we can do about this situation because the order in which + // CollectionChanged handlers are called can't be known (the problem also exists with + // WPF). The best we can do is not select an invalid index. + var target = CreateTarget(createData: false); + var data = new AvaloniaList(); + + data.CollectionChanged += (s, e) => + { + target.Select(0); + }; + + target.Source = data; + data.Add("foo"); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { 0 }, target.SelectedIndexes); + Assert.Equal("foo", target.SelectedItem); + Assert.Equal(new[] { "foo" }, target.SelectedItems); + Assert.Equal(0, target.AnchorIndex); + } } public class BatchUpdate From 8dfc65d17be00b7f7c96c294dabe7616916951b2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 1 Sep 2020 18:16:55 +0200 Subject: [PATCH 18/60] Try to handle selections made in CollectionChanged. There's not much we can do about this except not select invalid indexes. --- .../Selection/SelectionModel.cs | 23 +++++++++++++ .../Selection/SelectionNodeBase.cs | 34 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 7ce2624d02..2556bd4c4c 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -436,6 +436,29 @@ namespace Avalonia.Controls.Selection } } + private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e) + { + if (!base.IsValidCollectionChange(e)) + { + return false; + } + + if (ItemsView is object && e.Action == NotifyCollectionChangedAction.Add) + { + if (e.NewStartingIndex <= _selectedIndex) + { + return _selectedIndex + e.NewItems.Count < ItemsView.Count; + } + + if (e.NewStartingIndex <= _anchorIndex) + { + return _anchorIndex + e.NewItems.Count < ItemsView.Count; + } + } + + return true; + } + protected override void OnSourceCollectionChangeFinished() { if (_operation is object) diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs index ff3b8f43a8..230575074a 100644 --- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs +++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Controls.Utils; #nullable enable @@ -234,6 +235,11 @@ namespace Avalonia.Controls.Selection var shiftIndex = -1; List? removed = null; + if (!IsValidCollectionChange(e)) + { + return; + } + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -276,6 +282,34 @@ namespace Avalonia.Controls.Selection } } + private protected virtual bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e) + { + // If the selection is modified in a CollectionChanged handler before the selection + // model's CollectionChanged handler has had chance to run then we can end up with + // a selected index that refers to the *new* state of the Source intermixed with + // indexes that reference an old state of the source. + // + // There's not much we can do in this situation, so detect whether shifting the + // current selected indexes would result in an invalid index in the source, and if + // so bail. + // + // See unit test Handles_Selection_Made_In_CollectionChanged for more details. + if (ItemsView is object && + RangesEnabled && + Ranges.Count > 0 && + e.Action == NotifyCollectionChangedAction.Add) + { + var lastIndex = Ranges.Last().End; + + if (e.NewStartingIndex <= lastIndex) + { + return lastIndex + e.NewItems.Count < ItemsView.Count; + } + } + + return true; + } + private protected struct CollectionChangeState { public int ShiftIndex; From f4b1b9dc958018b2b3c21d0c6e733e465cce9141 Mon Sep 17 00:00:00 2001 From: "Artyom V. Gorchakov" Date: Tue, 1 Sep 2020 20:30:56 +0300 Subject: [PATCH 19/60] UseDotNet@2 --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 20278ddc71..6652d1712a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,7 +34,7 @@ jobs: pool: vmImage: 'macOS-10.14' steps: - - task: DotNetCoreInstaller@0 + - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.302' inputs: version: 3.1.302 @@ -105,7 +105,7 @@ jobs: pool: vmImage: 'windows-2019' steps: - - task: DotNetCoreInstaller@0 + - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.302' inputs: version: 3.1.302 From 21fc8620f204233268d6066bfb4714332ea4f6fd Mon Sep 17 00:00:00 2001 From: artyom Date: Tue, 1 Sep 2020 21:36:41 +0300 Subject: [PATCH 20/60] Try using 3.1.401 --- azure-pipelines.yml | 8 ++++---- global.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6652d1712a..89504a498e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -35,9 +35,9 @@ jobs: vmImage: 'macOS-10.14' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.302' + displayName: 'Use .NET Core SDK 3.1.401' inputs: - version: 3.1.302 + version: 3.1.401 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -106,9 +106,9 @@ jobs: vmImage: 'windows-2019' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.302' + displayName: 'Use .NET Core SDK 3.1.401' inputs: - version: 3.1.302 + version: 3.1.401 - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/global.json b/global.json index 3371f05a4d..b2b2da7c4f 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.302" + "version": "3.1.401" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", From 74827aa05c36e79c55882bffebd7571de8194d3f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 1 Sep 2020 20:28:45 +0100 Subject: [PATCH 21/60] update harfbuzz --- build/HarfBuzzSharp.props | 4 ++-- samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 88c4d36282..e636461ad9 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index 2dbc095156..69abbedbec 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -173,8 +173,10 @@ {d0a739b9-3c68-4ba6-a328-41606954b6bd} ControlCatalog + + From cf0ea3581a18002be7f357be651d13516226b4d4 Mon Sep 17 00:00:00 2001 From: amwx Date: Tue, 1 Sep 2020 16:54:44 -0500 Subject: [PATCH 22/60] Fix SplitView pane to work with popups --- .../ControlCatalog/Pages/SplitViewPage.xaml | 10 ++- src/Avalonia.Controls/SplitView.cs | 62 +++---------------- 2 files changed, 18 insertions(+), 54 deletions(-) diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml b/samples/ControlCatalog/Pages/SplitViewPage.xaml index 7e629db2da..6902b27715 100644 --- a/samples/ControlCatalog/Pages/SplitViewPage.xaml +++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml @@ -58,12 +58,18 @@ + - + + + + + + @@ -76,7 +82,7 @@ - + diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index fc3ff51f24..b71858f796 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -145,7 +145,7 @@ namespace Avalonia.Controls private bool _isPaneOpen; private Panel _pane; - private CompositeDisposable _pointerDisposables; + private IDisposable _pointerDisposable; public SplitView() { @@ -320,37 +320,14 @@ namespace Avalonia.Controls var topLevel = this.VisualRoot; if (topLevel is Window window) { - //Logic adapted from Popup - //Basically if we're using an overlay DisplayMode, close the pane if we don't click on the pane - IDisposable subscribeToEventHandler(T target, TEventHandler handler, - Action subscribe, Action unsubscribe) - { - subscribe(target, handler); - return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler)); - } - - _pointerDisposables = new CompositeDisposable( - window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel), - InputManager.Instance?.Process.Subscribe(OnNonClientClick), - subscribeToEventHandler(window, Window_Deactivated, - (x, handler) => x.Deactivated += handler, (x, handler) => x.Deactivated -= handler), - subscribeToEventHandler(window.PlatformImpl, OnWindowLostFocus, - (x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler)); + _pointerDisposable = window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); } } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - _pointerDisposables?.Dispose(); - } - - private void OnWindowLostFocus() - { - if (IsPaneOpen && ShouldClosePane()) - { - IsPaneOpen = false; - } + _pointerDisposable?.Dispose(); } private void PointerPressedOutside(object sender, PointerPressedEventArgs e) @@ -371,7 +348,12 @@ namespace Avalonia.Controls var src = e.Source as IVisual; while (src != null) { - if (src == _pane) + // Make assumption that if Popup is in visual tree, + // owning control is within pane + // This works because if pane is triggered to close + // when clicked anywhere else in Window, the pane + // would close before the popup is opened + if (src == _pane || src is PopupRoot) { closePane = false; break; @@ -385,31 +367,7 @@ namespace Avalonia.Controls e.Handled = true; } } - - private void OnNonClientClick(RawInputEventArgs obj) - { - if (!IsPaneOpen) - { - return; - } - - var mouse = obj as RawPointerEventArgs; - if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) - - { - if (ShouldClosePane()) - IsPaneOpen = false; - } - } - - private void Window_Deactivated(object sender, EventArgs e) - { - if (IsPaneOpen && ShouldClosePane()) - { - IsPaneOpen = false; - } - } - + private bool ShouldClosePane() { return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay); From 706cdf818af98f370602261ba0602821a27815e7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 2 Sep 2020 10:52:45 +0100 Subject: [PATCH 23/60] fix emulator for ios. --- samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index 69abbedbec..db1e16166a 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -9,6 +9,10 @@ ControlCatalog.iOS Resources ControlCatalogiOS + true + NSUrlSessionHandler + PackageReference + automatic true @@ -19,8 +23,8 @@ prompt 4 false - i386 - SdkOnly + x86_64 + None True 9.1 False @@ -43,7 +47,7 @@ prompt 4 None - i386 + x86_64 false From 4722767a9a5da6d9a170976e10cd0de13848d201 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 2 Sep 2020 11:02:39 +0100 Subject: [PATCH 24/60] remove purge resource calls. --- src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index a90faf1448..081db5d26a 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -35,7 +35,6 @@ namespace Avalonia.Skia IGlPlatformSurfaceRenderingSession glSession) { GrContext = grContext; - GrContext.PurgeResources(); _backendRenderTarget = backendRenderTarget; _surface = surface; _glSession = glSession; @@ -46,7 +45,6 @@ namespace Avalonia.Skia _surface.Dispose(); _backendRenderTarget.Dispose(); GrContext.Flush(); - GrContext.PurgeResources(); _glSession.Dispose(); } From e2d45fd9087cf302617686cb308201330c0a34f7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 2 Sep 2020 12:23:27 +0100 Subject: [PATCH 25/60] OSX fix dev tools ctrl + shift (implement keyevents on modifier key change events.) --- native/Avalonia.Native/src/OSX/window.mm | 60 ++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index dd241409c7..fb945a105e 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1209,6 +1209,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _queuedDisplayFromThread; NSTrackingArea* _area; bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver; + AvnInputModifiers _modifierState; NSEvent* _lastMouseDownEvent; bool _lastKeyHandled; AvnPixelSize _lastPixelSize; @@ -1251,6 +1252,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _lastPixelSize.Height = 100; _lastPixelSize.Width = 100; [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; + + _modifierState = AvnInputModifiersNone; return self; } @@ -1594,6 +1597,63 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return result; } +- (void)flagsChanged:(NSEvent *)event +{ + auto newModifierState = [self getModifiers:[event modifierFlags]]; + + bool isAltCurrentlyPressed = (_modifierState & Alt) == Alt; + bool isControlCurrentlyPressed = (_modifierState & Control) == Control; + bool isShiftCurrentlyPressed = (_modifierState & Shift) == Shift; + bool isCommandCurrentlyPressed = (_modifierState & Windows) == Windows; + + bool isAltPressed = (newModifierState & Alt) == Alt; + bool isControlPressed = (newModifierState & Control) == Control; + bool isShiftPressed = (newModifierState & Shift) == Shift; + bool isCommandPressed = (newModifierState & Windows) == Windows; + + + if (isAltPressed && !isAltCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if (isAltCurrentlyPressed && !isAltPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + if (isControlPressed && !isControlCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if (isControlCurrentlyPressed && !isControlPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + if (isShiftPressed && !isShiftCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if(isShiftCurrentlyPressed && !isShiftPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + if(isCommandPressed && !isCommandCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if(isCommandCurrentlyPressed && ! isCommandPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + _modifierState = newModifierState; + + [[self inputContext] handleEvent:event]; + [super flagsChanged:event]; +} + - (void)keyDown:(NSEvent *)event { [self keyboardEvent:event withType:KeyDown]; From 9e7c4d1b0943f10a600d03a8722bb19407e92337 Mon Sep 17 00:00:00 2001 From: ShadowDancer Date: Wed, 2 Sep 2020 23:20:01 +0200 Subject: [PATCH 26/60] Update Visual.cs documentation `OnAttachedToVisualTree`/`OnDetachedFromVisualTree` is called only for root visual tree. Keep xmldoc in line with event description. --- src/Avalonia.Visuals/Visual.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index cd6eb6aac7..283d9deb52 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -455,7 +455,7 @@ namespace Avalonia } /// - /// Called when the control is added to a visual tree. + /// Called when the control is added to a rooted visual tree. /// /// The event args. protected virtual void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) @@ -463,7 +463,7 @@ namespace Avalonia } /// - /// Called when the control is removed from a visual tree. + /// Called when the control is removed from a rooted visual tree. /// /// The event args. protected virtual void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) From c4ebf899e50f079be187c227cfba61c4c9d70ae9 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 3 Sep 2020 15:05:28 +0200 Subject: [PATCH 27/60] Add option to enable/disable dirty rects --- .../Diagnostics/ViewModels/MainViewModel.cs | 27 ++++++++++++++----- .../Diagnostics/Views/MainView.xaml | 7 +++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 1c49b24f52..1bd4d711ab 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -9,7 +9,7 @@ namespace Avalonia.Diagnostics.ViewModels { internal class MainViewModel : ViewModelBase, IDisposable { - private readonly IControl _root; + private readonly TopLevel _root; private readonly TreePageViewModel _logicalTree; private readonly TreePageViewModel _visualTree; private readonly EventsPageViewModel _events; @@ -19,8 +19,9 @@ namespace Avalonia.Diagnostics.ViewModels private string _focusedControl; private string _pointerOverElement; private bool _shouldVisualizeMarginPadding = true; + private bool _shouldVisualizeDirtyRects; - public MainViewModel(IControl root) + public MainViewModel(TopLevel root) { _root = root; _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); @@ -40,6 +41,22 @@ namespace Avalonia.Diagnostics.ViewModels get => _shouldVisualizeMarginPadding; set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); } + + public bool ShouldVisualizeDirtyRects + { + get => _shouldVisualizeDirtyRects; + set + { + RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); + + _root.Renderer.DrawDirtyRects = value; + } + } + + public void ToggleVisualizeDirtyRects() + { + ShouldVisualizeDirtyRects = !ShouldVisualizeDirtyRects; + } public void ToggleVisualizeMarginPadding() { @@ -128,10 +145,7 @@ namespace Avalonia.Diagnostics.ViewModels { var tree = Content as TreePageViewModel; - if (tree != null) - { - tree.SelectControl(control); - } + tree?.SelectControl(control); } public void Dispose() @@ -140,6 +154,7 @@ namespace Avalonia.Diagnostics.ViewModels _pointerOverSubscription.Dispose(); _logicalTree.Dispose(); _visualTree.Dispose(); + _root.Renderer.DrawDirtyRects = false; } private void UpdateFocusedControl() diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 0165398718..e0b73957bf 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -24,6 +24,13 @@ IsEnabled="False"/> + + + + + From 2bd8156fae206d321a446836e1ce33b45caca931 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 3 Sep 2020 16:28:29 +0200 Subject: [PATCH 28/60] Also add option to show fps overlay --- .../Diagnostics/ViewModels/MainViewModel.cs | 20 +++++++++++++++++-- .../Diagnostics/Views/MainView.xaml | 7 +++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 1bd4d711ab..bf7d0e232a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -20,6 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels private string _pointerOverElement; private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeDirtyRects; + private bool _showFpsOverlay; public MainViewModel(TopLevel root) { @@ -47,9 +48,8 @@ namespace Avalonia.Diagnostics.ViewModels get => _shouldVisualizeDirtyRects; set { - RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); - _root.Renderer.DrawDirtyRects = value; + RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); } } @@ -63,6 +63,21 @@ namespace Avalonia.Diagnostics.ViewModels ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding; } + public bool ShowFpsOverlay + { + get => _showFpsOverlay; + set + { + _root.Renderer.DrawFps = value; + RaiseAndSetIfChanged(ref _showFpsOverlay, value); + } + } + + public void ToggleFpsOverlay() + { + ShowFpsOverlay = !ShowFpsOverlay; + } + public ConsoleViewModel Console { get; } public ViewModelBase Content @@ -155,6 +170,7 @@ namespace Avalonia.Diagnostics.ViewModels _logicalTree.Dispose(); _visualTree.Dispose(); _root.Renderer.DrawDirtyRects = false; + _root.Renderer.DrawFps = false; } private void UpdateFocusedControl() diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index e0b73957bf..8c4db33f91 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -31,6 +31,13 @@ IsEnabled="False"/> + + + + + From ac24e28cc1105d4c79d2a094d99136d0e1d2234d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 3 Sep 2020 20:39:10 +0100 Subject: [PATCH 29/60] Fix IOSurface leak. --- native/Avalonia.Native/src/OSX/rendertarget.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm index 1565417c1a..93a33bbbb0 100644 --- a/native/Avalonia.Native/src/OSX/rendertarget.mm +++ b/native/Avalonia.Native/src/OSX/rendertarget.mm @@ -110,7 +110,7 @@ if(_renderbuffer != 0) glDeleteRenderbuffers(1, &_renderbuffer); } - IOSurfaceDecrementUseCount(surface); + CFRelease(surface); } @end From d0f0d02b923980f3c113b130035d2796d6535fdf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 3 Sep 2020 22:00:25 +0100 Subject: [PATCH 30/60] install typescript with npm. --- nukebuild/Build.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index a46b55f6f3..5b4e5a5bad 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -131,6 +131,7 @@ partial class Build : NukeBuild var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp"; NpmTasks.NpmInstall(c => c + .AddPackages("typescript") .SetWorkingDirectory(webappDir) .SetArgumentConfigurator(a => a.Add("--silent"))); NpmTasks.NpmRun(c => c From 6eeeb6c8eaa9f4969986ef878b75ce26817c6660 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 3 Sep 2020 22:25:43 +0100 Subject: [PATCH 31/60] skip building web previewer on osx. --- azure-pipelines.yml | 2 +- nukebuild/Build.cs | 2 +- nukebuild/BuildParameters.cs | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 89504a498e..29d2c62caf 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -81,7 +81,7 @@ jobs: export PATH="$PATH:$HOME/.dotnet/tools" dotnet --info printenv - nuke --target CiAzureOSX --configuration Release + nuke --target CiAzureOSX --configuration Release --skip-previewer - task: PublishTestResults@2 inputs: diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 5b4e5a5bad..24398accbb 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -126,12 +126,12 @@ partial class Build : NukeBuild Target CompileHtmlPreviewer => _ => _ .DependsOn(Clean) + .OnlyWhenStatic(() => !Parameters.SkipPreviewer) .Executes(() => { var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp"; NpmTasks.NpmInstall(c => c - .AddPackages("typescript") .SetWorkingDirectory(webappDir) .SetArgumentConfigurator(a => a.Add("--silent"))); NpmTasks.NpmRun(c => c diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 149716b416..a167e9d892 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -19,10 +19,14 @@ public partial class Build [Parameter("force-nuget-version")] public string ForceNugetVersion { get; set; } + [Parameter("skip-previewer")] + public bool SkipPreviewer { get; set; } + public class BuildParameters { public string Configuration { get; } public bool SkipTests { get; } + public bool SkipPreviewer {get;} public string MainRepo { get; } public string MasterBranch { get; } public string RepositoryName { get; } @@ -63,6 +67,7 @@ public partial class Build // ARGUMENTS Configuration = b.Configuration ?? "Release"; SkipTests = b.SkipTests; + SkipPreviewer = b.SkipPreviewer; // CONFIGURATION MainRepo = "https://github.com/AvaloniaUI/Avalonia"; From 67a87e09cf617f0e8c7c4916da50c610cf3cb677 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 15:48:27 +0100 Subject: [PATCH 32/60] use directx11. --- src/Avalonia.OpenGL/AngleOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index 4b9c04f4e6..56435d2705 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -12,7 +12,7 @@ namespace Avalonia.OpenGL public List AllowedPlatformApis = new List { - PlatformApi.DirectX9 + PlatformApi.DirectX11 }; } } From 6ed1669803d62f7a02f75d89b0e0ad66b8d5d983 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 15:49:04 +0100 Subject: [PATCH 33/60] set a sensible cache size for skia.. based on what flutter uses. --- src/Skia/Avalonia.Skia/SkiaOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index cbe0b5ef42..d14120204c 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -16,6 +16,6 @@ namespace Avalonia /// /// The maximum number of bytes for video memory to store textures and resources. /// - public long? MaxGpuResourceSizeBytes { get; set; } + public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures. } } From 1eda1b6c1fbee01f7a09cc47d78c30f33a259111 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 16:13:18 +0100 Subject: [PATCH 34/60] better way to set default directx apis. --- src/Avalonia.OpenGL/AngleOptions.cs | 5 +---- src/Avalonia.OpenGL/EglDisplay.cs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index 56435d2705..af1d1177ed 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -10,9 +10,6 @@ namespace Avalonia.OpenGL DirectX11 } - public List AllowedPlatformApis = new List - { - PlatformApi.DirectX11 - }; + public List AllowedPlatformApis { get; set; } = null; } } diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 0436f6ac52..cbc9bbfb85 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -36,7 +36,7 @@ namespace Avalonia.OpenGL throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new List {AngleOptions.PlatformApi.DirectX9}; + ?? new List {AngleOptions.PlatformApi.DirectX11}; foreach (var platformApi in allowedApis) { From 531b64e4665184d9df041f2b6b4e911b22151861 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 16:35:43 +0100 Subject: [PATCH 35/60] dont allocate lists for angleoptions. --- src/Avalonia.OpenGL/AngleOptions.cs | 2 +- src/Avalonia.OpenGL/EglDisplay.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index af1d1177ed..84744288ed 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -10,6 +10,6 @@ namespace Avalonia.OpenGL DirectX11 } - public List AllowedPlatformApis { get; set; } = null; + public IList AllowedPlatformApis { get; set; } = null; } } diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index cbc9bbfb85..2c3cadd620 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -36,7 +36,7 @@ namespace Avalonia.OpenGL throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new List {AngleOptions.PlatformApi.DirectX11}; + ?? new [] {AngleOptions.PlatformApi.DirectX11}; foreach (var platformApi in allowedApis) { From a752c398bb809db466f68a5365142f20e40baa93 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 16:38:47 +0100 Subject: [PATCH 36/60] allow fallback to dx9. --- src/Avalonia.OpenGL/EglDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 2c3cadd620..89dee4fc37 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -36,7 +36,7 @@ namespace Avalonia.OpenGL throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new [] {AngleOptions.PlatformApi.DirectX11}; + ?? new [] {AngleOptions.PlatformApi.DirectX11, AngleOptions.PlatformApi.DirectX9}; foreach (var platformApi in allowedApis) { From 8614c060028ded20b6fd5e72103425f929134cb8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 16:47:25 +0100 Subject: [PATCH 37/60] add WGL option. --- src/Avalonia.OpenGL/AngleOptions.cs | 3 ++- src/Avalonia.OpenGL/EglDisplay.cs | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index 84744288ed..40a673b574 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -7,7 +7,8 @@ namespace Avalonia.OpenGL public enum PlatformApi { DirectX9, - DirectX11 + DirectX11, + WGL } public IList AllowedPlatformApis { get; set; } = null; diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 89dee4fc37..c84ef4533d 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -36,7 +36,12 @@ namespace Avalonia.OpenGL throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new [] {AngleOptions.PlatformApi.DirectX11, AngleOptions.PlatformApi.DirectX9}; + ?? new [] + { + AngleOptions.PlatformApi.WGL, + AngleOptions.PlatformApi.DirectX11, + AngleOptions.PlatformApi.DirectX9 + }; foreach (var platformApi in allowedApis) { @@ -45,6 +50,8 @@ namespace Avalonia.OpenGL dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; else if (platformApi == AngleOptions.PlatformApi.DirectX11) dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; + else if (platformApi == AngleOptions.PlatformApi.WGL) + dapi = EGL_PLATFORM_ANGLE_TYPE_ANGLE; else continue; From fc12b9fee9ba06e48702340878ee79fe3ebb9a87 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 16:50:41 +0100 Subject: [PATCH 38/60] define correct consts for opengl. --- src/Avalonia.OpenGL/EglConsts.cs | 3 +++ src/Avalonia.OpenGL/EglDisplay.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.OpenGL/EglConsts.cs b/src/Avalonia.OpenGL/EglConsts.cs index 62fb3faef6..5cc732ef6a 100644 --- a/src/Avalonia.OpenGL/EglConsts.cs +++ b/src/Avalonia.OpenGL/EglConsts.cs @@ -186,6 +186,9 @@ namespace Avalonia.OpenGL public const int EGL_PLATFORM_ANGLE_TYPE_DEFAULT_ANGLE = 0x3206; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_HARDWARE_ANGLE = 0x320A; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_NULL_ANGLE = 0x345E; + + public const int EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE = 0x320D; + public const int EGL_PLATFORM_ANGLE_TYPE_OPENGLES_ANGLE = 0x320E; //EGL_ANGLE_platform_angle_d3d public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE = 0x3209; diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index c84ef4533d..55d0ebb59a 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -51,7 +51,7 @@ namespace Avalonia.OpenGL else if (platformApi == AngleOptions.PlatformApi.DirectX11) dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; else if (platformApi == AngleOptions.PlatformApi.WGL) - dapi = EGL_PLATFORM_ANGLE_TYPE_ANGLE; + dapi = EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE; else continue; From 126737fd1bd5d182bdf803adfa7de949b77be0a1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 4 Sep 2020 18:04:06 +0200 Subject: [PATCH 39/60] Update ScrollBar visibility with local value priority. Using `SetValue` with `Style` priority caused a new entry to be added to the `IsVisible` priority store each time the value was updated, causing a leak. Setting it with local value priority subtly changes its semantics but hopefully not so that anyone notices. --- src/Avalonia.Controls/Primitives/ScrollBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index fc82fcc7a7..477d24dc99 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -141,7 +141,7 @@ namespace Avalonia.Controls.Primitives _ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.") }; - SetValue(IsVisibleProperty, isVisible, BindingPriority.Style); + SetValue(IsVisibleProperty, isVisible); } protected override void OnKeyDown(KeyEventArgs e) From 39a82ce66e2c8e2d11226d6a0e7a2557b20380d4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 5 Sep 2020 12:21:20 +0100 Subject: [PATCH 40/60] Remove WGL option. --- src/Avalonia.OpenGL/AngleOptions.cs | 3 +-- src/Avalonia.OpenGL/EglDisplay.cs | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index 40a673b574..84744288ed 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -7,8 +7,7 @@ namespace Avalonia.OpenGL public enum PlatformApi { DirectX9, - DirectX11, - WGL + DirectX11 } public IList AllowedPlatformApis { get; set; } = null; diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 55d0ebb59a..ce5fd50a07 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -38,7 +38,6 @@ namespace Avalonia.OpenGL var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis ?? new [] { - AngleOptions.PlatformApi.WGL, AngleOptions.PlatformApi.DirectX11, AngleOptions.PlatformApi.DirectX9 }; @@ -50,8 +49,6 @@ namespace Avalonia.OpenGL dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; else if (platformApi == AngleOptions.PlatformApi.DirectX11) dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; - else if (platformApi == AngleOptions.PlatformApi.WGL) - dapi = EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE; else continue; From 3c62c43cc689a2e727d3e15f7137f82fd26bfef7 Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Sun, 6 Sep 2020 04:37:37 +0300 Subject: [PATCH 41/60] Hid maximize box on Windows for `CanResize = false` --- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ddc0cc4e42..4929283874 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1007,10 +1007,12 @@ namespace Avalonia.Win32 if (newProperties.IsResizable) { style |= WindowStyles.WS_SIZEFRAME; + style |= WindowStyles.WS_MAXIMIZEBOX; } else { style &= ~WindowStyles.WS_SIZEFRAME; + style &= ~WindowStyles.WS_MAXIMIZEBOX; } SetStyle(style); From 8079ebe1dc3d96e4f2a23897071b16e26a9e505a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Sep 2020 10:49:48 +0200 Subject: [PATCH 42/60] Added failing test for #4599. --- tests/Avalonia.LeakTests/ControlTests.cs | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 0b81276240..d61813b50b 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -552,6 +552,42 @@ namespace Avalonia.LeakTests } } + [Fact] + public void ItemsRepeater_Is_Freed() + { + using (Start()) + { + var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; + + Func run = () => + { + var window = new Window + { + Content = new ItemsRepeater(), + }; + + window.Show(); + + window.LayoutManager.ExecuteInitialLayoutPass(); + Assert.IsType(window.Presenter.Child); + + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + + // We are keeping geometry alive to simulate a resource that outlives the control. + GC.KeepAlive(geometry); + } + } + private IDisposable Start() { return UnitTestApplication.Start(TestServices.StyledWindow.With( From 60c0a44a8737da790d68c73f9d8036b72396aad3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Sep 2020 10:51:52 +0200 Subject: [PATCH 43/60] Clean up EffectiveViewportChanged earlier. Clean up the `EffectiveViewportChanged` subscriptions before calling `OnDetachedFromVisualTree` so that (un)subscribing to the `EffectiveViewportChanged` event in `OnDetachedFromVisualTree` doesn't cause a leak. --- src/Avalonia.Layout/Layoutable.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index e62e22f8ec..aca2965ea6 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -758,8 +758,6 @@ namespace Avalonia.Layout protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { - base.OnDetachedFromVisualTreeCore(e); - if (e.Root is ILayoutRoot r) { if (_layoutUpdated is object) @@ -772,6 +770,8 @@ namespace Avalonia.Layout r.LayoutManager.UnregisterEffectiveViewportListener(this); } } + + base.OnDetachedFromVisualTreeCore(e); } /// From 68504d212d068cf3786eb97199359be1222d4259 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Sep 2020 13:20:22 +0200 Subject: [PATCH 44/60] Use weak event handlers. Seems that C++/WinRT uses weak event handlers by default so we need to do the same here. --- .../Repeater/ItemsRepeater.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 8bc356bdec..772a17e4ea 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -7,10 +7,10 @@ using System; using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Templates; -using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -681,8 +681,15 @@ namespace Avalonia.Controls if (oldValue != null) { oldValue.UninitializeForContext(LayoutContext); - oldValue.MeasureInvalidated -= InvalidateMeasureForLayout; - oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout; + + WeakEventHandlerManager.Unsubscribe( + oldValue, + nameof(newValue.MeasureInvalidated), + InvalidateMeasureForLayout); + WeakEventHandlerManager.Unsubscribe( + oldValue, + nameof(newValue.ArrangeInvalidated), + InvalidateArrangeForLayout); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -699,8 +706,15 @@ namespace Avalonia.Controls if (newValue != null) { newValue.InitializeForContext(LayoutContext); - newValue.MeasureInvalidated += InvalidateMeasureForLayout; - newValue.ArrangeInvalidated += InvalidateArrangeForLayout; + + WeakEventHandlerManager.Subscribe( + newValue, + nameof(newValue.MeasureInvalidated), + InvalidateMeasureForLayout); + WeakEventHandlerManager.Subscribe( + newValue, + nameof(newValue.ArrangeInvalidated), + InvalidateArrangeForLayout); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; From e579e61f3820cd37e33258b3688cfc974477aa9c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Sep 2020 13:46:25 +0200 Subject: [PATCH 45/60] Teaks and remove copy-pasta. --- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 8 ++++---- tests/Avalonia.LeakTests/ControlTests.cs | 5 ----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 772a17e4ea..40f1b8dbb9 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -684,11 +684,11 @@ namespace Avalonia.Controls WeakEventHandlerManager.Unsubscribe( oldValue, - nameof(newValue.MeasureInvalidated), + nameof(AttachedLayout.MeasureInvalidated), InvalidateMeasureForLayout); WeakEventHandlerManager.Unsubscribe( oldValue, - nameof(newValue.ArrangeInvalidated), + nameof(AttachedLayout.ArrangeInvalidated), InvalidateArrangeForLayout); // Walk through all the elements and make sure they are cleared @@ -709,11 +709,11 @@ namespace Avalonia.Controls WeakEventHandlerManager.Subscribe( newValue, - nameof(newValue.MeasureInvalidated), + nameof(AttachedLayout.MeasureInvalidated), InvalidateMeasureForLayout); WeakEventHandlerManager.Subscribe( newValue, - nameof(newValue.ArrangeInvalidated), + nameof(AttachedLayout.ArrangeInvalidated), InvalidateArrangeForLayout); } diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index d61813b50b..0c7b966f29 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -557,8 +557,6 @@ namespace Avalonia.LeakTests { using (Start()) { - var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; - Func run = () => { var window = new Window @@ -582,9 +580,6 @@ namespace Avalonia.LeakTests dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); - - // We are keeping geometry alive to simulate a resource that outlives the control. - GC.KeepAlive(geometry); } } From 04c781ca1431b3bc3d741d9ea864ad00569c33a9 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 8 Sep 2020 14:55:10 +0200 Subject: [PATCH 46/60] Make sure GlyphTypefaces are always cached --- Avalonia.sln | 28 +++++++- .../Presenters/TextPresenter.cs | 4 +- src/Avalonia.Controls/TextBlock.cs | 2 +- .../HeadlessPlatformStubs.cs | 4 +- src/Avalonia.Visuals/ApiCompatBaseline.txt | 12 +++- src/Avalonia.Visuals/Media/FontManager.cs | 67 ++++++------------- src/Avalonia.Visuals/Media/Fonts/FontKey.cs | 40 ----------- .../Media/TextFormatting/TextCharacters.cs | 7 +- src/Avalonia.Visuals/Media/Typeface.cs | 31 ++------- .../Platform/IFontManagerImpl.cs | 5 +- .../Rendering/RendererBase.cs | 2 +- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 7 +- .../Avalonia.Skia/SKTypefaceCollection.cs | 19 +++--- .../SKTypefaceCollectionCache.cs | 2 +- .../Media/FontManagerImpl.cs | 7 +- .../Media/FormattedTextImplTests.cs | 2 +- .../Media/CustomFontManagerImpl.cs | 8 +-- .../TextFormatting/TextFormatterTests.cs | 2 +- .../Avalonia.UnitTests/MockFontManagerImpl.cs | 5 +- .../Media/FontManagerTests.cs | 7 +- 20 files changed, 103 insertions(+), 158 deletions(-) delete mode 100644 src/Avalonia.Visuals/Media/Fonts/FontKey.cs diff --git a/Avalonia.sln b/Avalonia.sln index 66777f33eb..ddcd61408d 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -222,9 +222,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -2040,6 +2040,30 @@ Global {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.Build.0 = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.ActiveCfg = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.Build.0 = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 37490c3ef3..cb7bee1d33 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -282,7 +282,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Constraint = constraint, - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), + Typeface = new Typeface(FontFamily, FontStyle, FontWeight), FontSize = FontSize, Text = text ?? string.Empty, TextAlignment = TextAlignment, @@ -499,7 +499,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Text = "X", - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), + Typeface = new Typeface(FontFamily, FontStyle, FontWeight), FontSize = FontSize, TextAlignment = TextAlignment, Constraint = availableSize, diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 7e5287f81f..3b9e9c4751 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -452,7 +452,7 @@ namespace Avalonia.Controls return new TextLayout( text ?? string.Empty, - FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), + new Typeface(FontFamily, FontStyle, FontWeight), FontSize, Foreground, TextAlignment, diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 763d192693..4c0e2982f4 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -155,9 +155,9 @@ namespace Avalonia.Headless return new List { "Arial" }; } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out Typeface typeface) { - fontKey = new FontKey("Arial", fontStyle, fontWeight); + typeface = new Typeface("Arial", fontStyle, fontWeight); return true; } } diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index 5058cff26d..5aa497861d 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -1,5 +1,15 @@ Compat issues with assembly Avalonia.Visuals: +MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.GetOrAddTypeface(Avalonia.Media.FontFamily, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.MatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract. +CannotSealType : Type 'Avalonia.Media.Typeface' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +TypeCannotChangeClassification : Type 'Avalonia.Media.Typeface' is a 'struct' in the implementation but is a 'class' in the contract. +CannotMakeMemberNonVirtual : Member 'public System.Boolean Avalonia.Media.Typeface.Equals(System.Object)' is non-virtual in the implementation but is virtual in the contract. +CannotMakeMemberNonVirtual : Member 'public System.Int32 Avalonia.Media.Typeface.GetHashCode()' is non-virtual in the implementation but is virtual in the contract. +TypesMustExist : Type 'Avalonia.Media.Fonts.FontKey' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak' is abstract in the implementation but is missing in the contract. MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.LineBreak.get()' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak.get()' is abstract in the implementation but is missing in the contract. -Total Issues: 3 +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' is present in the implementation but not in the contract. +Total Issues: 13 diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index ad3fee7eb7..db87c7c6c4 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -13,8 +13,8 @@ namespace Avalonia.Media /// public sealed class FontManager { - private readonly ConcurrentDictionary _typefaceCache = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); private readonly FontFamily _defaultFontFamily; public FontManager(IFontManagerImpl platformImpl) @@ -76,79 +76,52 @@ namespace Avalonia.Media PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); /// - /// Returns a new typeface, or an existing one if a matching typeface exists. + /// Returns a new , or an existing one if a matching exists. /// - /// The font family. - /// The font style. - /// The font weight. + /// The typeface. /// - /// The typeface. + /// The . /// - public Typeface GetOrAddTypeface(FontFamily fontFamily, FontStyle fontStyle = FontStyle.Normal, - FontWeight fontWeight = FontWeight.Normal) + public GlyphTypeface GetOrAddGlyphTypeface(Typeface typeface) { while (true) { - if (fontFamily.IsDefault) + if (_glyphTypefaceCache.TryGetValue(typeface, out var glyphTypeface)) { - fontFamily = _defaultFontFamily; + return glyphTypeface; } - var key = new FontKey(fontFamily.Name, fontStyle, fontWeight); + glyphTypeface = new GlyphTypeface(typeface); - if (_typefaceCache.TryGetValue(key, out var typeface)) + if (_glyphTypefaceCache.TryAdd(typeface, glyphTypeface)) { - return typeface; + return glyphTypeface; } - typeface = new Typeface(fontFamily, fontStyle, fontWeight); - - if (_typefaceCache.TryAdd(key, typeface)) - { - return typeface; - } - - if (fontFamily == _defaultFontFamily) + if (typeface.FontFamily == _defaultFontFamily) { return null; } - fontFamily = _defaultFontFamily; + typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight); } } /// - /// Tries to match a specified character to a typeface that supports specified font properties. - /// Returns null if no fallback was found. + /// Tries to match a specified character to a that supports specified font properties. /// /// The codepoint to match against. /// The font style. /// The font weight. /// The font family. This is optional and used for fallback lookup. /// The culture. + /// The matching . /// - /// The matched typeface. + /// True, if the could match the character to specified parameters, False otherwise. /// - public Typeface MatchCharacter(int codepoint, - FontStyle fontStyle = FontStyle.Normal, - FontWeight fontWeight = FontWeight.Normal, - FontFamily fontFamily = null, CultureInfo culture = null) - { - foreach (var cachedTypeface in _typefaceCache.Values) - { - // First try to find a cached typeface by style and weight to avoid redundant glyph index lookup. - if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight - && cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0) - { - return cachedTypeface; - } - } - - var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out var key) ? - _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Style, key.Weight)) : - null; - - return matchedTypeface; - } + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, + FontFamily fontFamily, CultureInfo culture, out Typeface typeface) => + PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out typeface); } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs deleted file mode 100644 index b330db8462..0000000000 --- a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; - -namespace Avalonia.Media.Fonts -{ - public readonly struct FontKey : IEquatable - { - public FontKey(string familyName, FontStyle style, FontWeight weight) - { - FamilyName = familyName; - Style = style; - Weight = weight; - } - - public string FamilyName { get; } - public FontStyle Style { get; } - public FontWeight Weight { get; } - - public override int GetHashCode() - { - var hash = FamilyName.GetHashCode(); - - hash = hash * 31 + (int)Style; - hash = hash * 31 + (int)Weight; - - return hash; - } - - public override bool Equals(object other) - { - return other is FontKey key && Equals(key); - } - - public bool Equals(FontKey other) - { - return FamilyName == other.FamilyName && - Style == other.Style && - Weight == other.Weight; - } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index 47e716982c..b91a50a27c 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -70,10 +70,11 @@ namespace Avalonia.Media.TextFormatting var codepoint = Codepoint.ReadAt(text, count, out _); //ToDo: Fix FontFamily fallback - currentTypeface = - FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.FontFamily); + var matchFound = + FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, + defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); - if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) + if (matchFound && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) { //Fallback found return new ShapeableTextCharacters(text.Take(count), diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 677e930804..17824c3c5e 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -8,17 +8,15 @@ namespace Avalonia.Media /// Represents a typeface. /// [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")] - public class Typeface : IEquatable + public readonly struct Typeface : IEquatable { - private GlyphTypeface _glyphTypeface; - /// /// Initializes a new instance of the class. /// /// The font family. /// The font style. /// The font weight. - public Typeface([NotNull]FontFamily fontFamily, + public Typeface([NotNull] FontFamily fontFamily, FontStyle style = FontStyle.Normal, FontWeight weight = FontWeight.Normal) { @@ -45,7 +43,7 @@ namespace Avalonia.Media { } - public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default); + public static Typeface Default { get; } = new Typeface(FontFamily.Default); /// /// Gets the font family. @@ -68,7 +66,7 @@ namespace Avalonia.Media /// /// The glyph typeface. /// - public GlyphTypeface GlyphTypeface => _glyphTypeface ?? (_glyphTypeface = new GlyphTypeface(this)); + public GlyphTypeface GlyphTypeface => FontManager.Current.GetOrAddGlyphTypeface(this); public static bool operator !=(Typeface a, Typeface b) { @@ -77,32 +75,17 @@ namespace Avalonia.Media public static bool operator ==(Typeface a, Typeface b) { - if (ReferenceEquals(a, b)) - { - return true; - } - - return !(a is null) && a.Equals(b); + return a.Equals(b); } public override bool Equals(object obj) { - if (obj is Typeface typeface) - { - return Equals(typeface); - } - - return false; + return obj is Typeface typeface && Equals(typeface); } public bool Equals(Typeface other) { - if (other is null) - { - return false; - } - - return FontFamily.Equals(other.FontFamily) && Style == other.Style && Weight == other.Weight; + return FontFamily == other.FontFamily && Style == other.Style && Weight == other.Weight; } public override int GetHashCode() diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 59b08aae0a..e562b45ca8 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; -using Avalonia.Media.Fonts; namespace Avalonia.Platform { @@ -26,13 +25,13 @@ namespace Avalonia.Platform /// The font weight. /// The font family. This is optional and used for fallback lookup. /// The culture. - /// The matching font key. + /// The matching typeface. /// /// True, if the could match the character to specified parameters, False otherwise. /// bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontFamily fontFamily, CultureInfo culture, out FontKey fontKey); + FontFamily fontFamily, CultureInfo culture, out Typeface typeface); /// /// Creates a glyph typeface. diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index b37d5d660b..5c9cace4cd 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering _useManualFpsCounting = useManualFpsCounting; _fpsText = new FormattedText { - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default), + Typeface = new Typeface(FontFamily.Default), FontSize = s_fontSize }; } diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 91bc937475..62ec39346a 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Platform; using SkiaSharp; @@ -31,7 +30,7 @@ namespace Avalonia.Skia public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + FontFamily fontFamily, CultureInfo culture, out Typeface fontKey) { SKFontStyle skFontStyle; @@ -81,7 +80,7 @@ namespace Avalonia.Skia continue; } - fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); + fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight); return true; } @@ -92,7 +91,7 @@ namespace Avalonia.Skia if (skTypeface != null) { - fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); + fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight); return true; } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 6c2ac17923..71deb1235f 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -2,31 +2,28 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Avalonia.Media; -using Avalonia.Media.Fonts; using SkiaSharp; namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary _typefaces = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary _typefaces = + new ConcurrentDictionary(); - public void AddTypeface(FontKey key, SKTypeface typeface) + public void AddTypeface(Typeface key, SKTypeface typeface) { _typefaces.TryAdd(key, typeface); } public SKTypeface Get(Typeface typeface) { - var key = new FontKey(typeface.FontFamily.Name, typeface.Style, typeface.Weight); - - return GetNearestMatch(_typefaces, key); + return GetNearestMatch(_typefaces, typeface); } - private static SKTypeface GetNearestMatch(IDictionary typefaces, FontKey key) + private static SKTypeface GetNearestMatch(IDictionary typefaces, Typeface key) { - if (typefaces.TryGetValue(new FontKey(key.FamilyName, key.Style, key.Weight), out var typeface)) + if (typefaces.TryGetValue(key, out var typeface)) { return typeface; } @@ -42,7 +39,7 @@ namespace Avalonia.Skia { if (weight - j >= 100) { - if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) + if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) { return typeface; } @@ -53,7 +50,7 @@ namespace Avalonia.Skia continue; } - if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) + if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) { return typeface; } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 7ca44e7282..de77c9186e 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -56,7 +56,7 @@ namespace Avalonia.Skia continue; } - var key = new FontKey(fontFamily.Name, typeface.FontSlant.ToAvalonia(), + var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(), (FontWeight)typeface.FontWeight); typeFaceCollection.AddTypeface(key, typeface); diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 33af15076d..6d95d759ec 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Platform; using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; @@ -34,7 +33,7 @@ namespace Avalonia.Direct2D1.Media public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + FontFamily fontFamily, CultureInfo culture, out Typeface typeface) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -51,12 +50,12 @@ namespace Avalonia.Direct2D1.Media var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - fontKey = new FontKey(fontFamilyName, fontStyle, fontWeight); + typeface = new Typeface(fontFamilyName, fontStyle, fontWeight); return true; } - fontKey = default; + typeface = default; return false; } diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index 8683da9a01..7528424521 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -51,7 +51,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var r = AvaloniaLocator.Current.GetService(); return r.CreateFormattedText(text, - FontManager.Current.GetOrAddTypeface(fontFamily, fontStyle, fontWeight), + new Typeface(fontFamily, fontStyle, fontWeight), fontSize, textAlignment, wrapping, diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index f36d6d9e4a..a0fe348166 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -39,7 +39,7 @@ namespace Avalonia.Skia.UnitTests.Media private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, - CultureInfo culture, out FontKey fontKey) + CultureInfo culture, out Typeface typeface) { foreach (var customTypeface in _customTypefaces) { @@ -48,7 +48,7 @@ namespace Avalonia.Skia.UnitTests.Media continue; } - fontKey = new FontKey(customTypeface.FontFamily.Name, fontStyle, fontWeight); + typeface = new Typeface(customTypeface.FontFamily.Name, fontStyle, fontWeight); return true; } @@ -56,7 +56,7 @@ namespace Avalonia.Skia.UnitTests.Media var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint); - fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); + typeface = new Typeface(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); return true; } @@ -73,13 +73,13 @@ namespace Avalonia.Skia.UnitTests.Media skTypeface = typefaceCollection.Get(typeface); break; } - case "Noto Sans": { var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily); skTypeface = typefaceCollection.Get(typeface); break; } + case FontFamily.DefaultFontFamilyName: case "Noto Mono": { var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 4a88b259bc..ea806e01fe 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -130,7 +130,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - const string text = "1234الدولي"; + const string text = "ぁぁぁぁالدولي"; var defaultProperties = new GenericTextRunProperties(Typeface.Default); diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs index e614c60310..ba3b346f1b 100644 --- a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Platform; namespace Avalonia.UnitTests @@ -26,9 +25,9 @@ namespace Avalonia.UnitTests } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, - CultureInfo culture, out FontKey fontKey) + CultureInfo culture, out Typeface fontKey) { - fontKey = default; + fontKey = new Typeface(_defaultFamilyName); return false; } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs index 81a4ca6495..2b0ffa4ed6 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Media; -using Avalonia.Platform; using Avalonia.UnitTests; using Xunit; @@ -15,9 +14,11 @@ namespace Avalonia.Visuals.UnitTests.Media { var fontFamily = new FontFamily("MyFont"); - var typeface = FontManager.Current.GetOrAddTypeface(fontFamily); + var typeface = new Typeface(fontFamily); - Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily)); + var glyphTypeface = FontManager.Current.GetOrAddGlyphTypeface(typeface); + + Assert.Same(glyphTypeface, FontManager.Current.GetOrAddGlyphTypeface(typeface)); } } From cf7b2966fe126845cc7019f5700c34eaf8652c85 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 8 Sep 2020 19:34:08 +0100 Subject: [PATCH 47/60] brew extract. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 29d2c62caf..92e4d9b7fb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -61,7 +61,7 @@ jobs: inputs: script: | brew update - brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb + brew extract castxml https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb - task: CmdLine@2 displayName: 'Install Nuke' From 71eaddfedbf4d5abd951966f076862a03d64e68c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 8 Sep 2020 19:47:51 +0100 Subject: [PATCH 48/60] dont install castxml on osx. --- azure-pipelines.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 92e4d9b7fb..721a0415f4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -56,13 +56,6 @@ jobs: xcodeVersion: '10' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - - task: CmdLine@2 - displayName: 'Install CastXML' - inputs: - script: | - brew update - brew extract castxml https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb - - task: CmdLine@2 displayName: 'Install Nuke' inputs: From 588d4377eb85bca48b647b5f826e47f739079c29 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 5 Sep 2020 16:53:07 +0100 Subject: [PATCH 49/60] Radio button and checkbox vertically center the content. --- src/Avalonia.Themes.Fluent/CheckBox.xaml | 2 +- src/Avalonia.Themes.Fluent/RadioButton.xaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index 5f82fed08c..0ba6fb81c2 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -12,7 +12,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml index acde4ea0be..35cc902d91 100644 --- a/src/Avalonia.Themes.Fluent/RadioButton.xaml +++ b/src/Avalonia.Themes.Fluent/RadioButton.xaml @@ -17,7 +17,7 @@ - + From dd9c4666a97620571c93536e9543cd5a11aee32d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 8 Sep 2020 21:16:43 +0100 Subject: [PATCH 50/60] clarify max skia cache size property. --- src/Skia/Avalonia.Skia/SkiaOptions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index d14120204c..afd17acf97 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -15,6 +15,8 @@ namespace Avalonia /// /// The maximum number of bytes for video memory to store textures and resources. + /// This is set by default to the recommended value for Avalonia. + /// Setting this to null will give you the default Skia value. /// public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures. } From d26445fdcdbb2970e4c61ed51c7c41f7fd55fc1b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 8 Sep 2020 21:31:47 +0100 Subject: [PATCH 51/60] use remarks tag. --- src/Skia/Avalonia.Skia/SkiaOptions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index afd17acf97..493263677d 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -15,9 +15,11 @@ namespace Avalonia /// /// The maximum number of bytes for video memory to store textures and resources. + /// + /// /// This is set by default to the recommended value for Avalonia. /// Setting this to null will give you the default Skia value. - /// + /// public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures. } } From c8e5c1762d7e4ef3806f47f8b47cd45f34156f18 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 9 Sep 2020 09:51:16 +0100 Subject: [PATCH 52/60] budget surfaces. --- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 27b29c6e1e..ed9b06cc97 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -51,7 +51,7 @@ namespace Avalonia.Skia { var imageInfo = MakeImageInfo(width, height, format); if (gpu != null) - return SKSurface.Create(gpu, false, imageInfo); + return SKSurface.Create(gpu, true, imageInfo); return SKSurface.Create(imageInfo); } From ff0a6f0518570169c3e9363afdb4e49421c8feb6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 9 Sep 2020 10:36:52 +0100 Subject: [PATCH 53/60] dispose old layer before creating new one. --- src/Avalonia.Visuals/Rendering/RenderLayer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/RenderLayer.cs b/src/Avalonia.Visuals/Rendering/RenderLayer.cs index d6676e25ff..ddf5f4e5cf 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayer.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayer.cs @@ -30,12 +30,13 @@ namespace Avalonia.Rendering { if (Size != size || Scaling != scaling) { + Bitmap.Dispose(); var resized = RefCountable.Create(drawingContext.CreateLayer(size)); using (var context = resized.Item.CreateDrawingContext(null)) { context.Clear(Colors.Transparent); - Bitmap.Dispose(); + Bitmap = resized; Scaling = scaling; Size = size; From a8100d9df53a7159a7aa6aa8ae48b959e726928e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 9 Sep 2020 10:59:04 +0100 Subject: [PATCH 54/60] Revert "budget surfaces." This reverts commit c8e5c1762d7e4ef3806f47f8b47cd45f34156f18. --- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index ed9b06cc97..27b29c6e1e 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -51,7 +51,7 @@ namespace Avalonia.Skia { var imageInfo = MakeImageInfo(width, height, format); if (gpu != null) - return SKSurface.Create(gpu, true, imageInfo); + return SKSurface.Create(gpu, false, imageInfo); return SKSurface.Create(imageInfo); } From 54fb09a304de8f748f7dbfa0dabb7033e818ca14 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 9 Sep 2020 15:40:36 +0200 Subject: [PATCH 55/60] Fix unit test --- .../Media/TextFormatting/TextFormatterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index ea806e01fe..adcc79e029 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -130,7 +130,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - const string text = "ぁぁぁぁالدولي"; + const string text = "ABCDالدولي"; var defaultProperties = new GenericTextRunProperties(Typeface.Default); From 556b934d77877b9bb546ce4b0fcc6910963ffb84 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 10 Sep 2020 08:58:36 +0200 Subject: [PATCH 56/60] Store args in ClassicDesktopStyleApplicationLifetime. --- .../ClassicDesktopStyleApplicationLifetime.cs | 14 ++++++++++---- .../IClassicDesktopStyleApplicationLifetime.cs | 7 +++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 6dd5b8cc81..e2c8e7e8e2 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -47,6 +47,11 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public event EventHandler Exit; + /// + /// Gets the arguments passed to the AppBuilder Start method. + /// + public string[] Args { get; set; } + /// public ShutdownMode ShutdownMode { get; set; } @@ -68,9 +73,6 @@ namespace Avalonia.Controls.ApplicationLifetimes else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) Shutdown(); } - - - public void Shutdown(int exitCode = 0) { @@ -123,7 +125,11 @@ namespace Avalonia this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) where T : AppBuilderBase, new() { - var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = shutdownMode}; + var lifetime = new ClassicDesktopStyleApplicationLifetime() + { + Args = args, + ShutdownMode = shutdownMode + }; builder.SetupWithLifetime(lifetime); return lifetime.Start(args); } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index a1006d907b..212f0b8617 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -8,6 +8,13 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime { + /// + /// Gets the arguments passed to the + /// + /// method. + /// + string[] Args { get; } + /// /// Gets or sets the . This property indicates whether the application is shutdown explicitly or implicitly. /// If is set to OnExplicitShutdown the application is only closes if Shutdown is called. From 7b8a6226028657d9c2486d59e24aad5064b24611 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 10 Sep 2020 11:16:00 +0200 Subject: [PATCH 57/60] Update API compat baseline. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 11708b360f..af88c569a6 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -12,7 +12,9 @@ MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.Interactivity.RoutedEvent Avalonia.Controls.TreeView.SelectionChangedEvent' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.TreeView.Selection.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.TreeView.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.String[] Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.Args' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.String[] Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.Args.get()' is present in the implementation but not in the contract. MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.DirectProperty Avalonia.Controls.Primitives.SelectingItemsControl.SelectionProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected Avalonia.Controls.ISelectionModel Avalonia.Controls.Primitives.SelectingItemsControl.Selection.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract. -Total Issues: 16 +Total Issues: 18 From ed7a75acbcb72c890932bace587fe55326804d65 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 10 Sep 2020 12:51:55 +0200 Subject: [PATCH 58/60] Ensure windows set to fullscreen are shown. Previously, `UnmanagedMethods.ShowWindow` wasn't called when trying to show a window with `WindowState="FullScreen"`. This is because win32 conflates showing a window and setting a window state, and setting all windows states except fullscreen require a call to `ShowWindow`, except fullscreen which does *not*. However it does require a call to `ShowWindow` when we are actually showing the window ;) --- src/Windows/Avalonia.Win32/WindowImpl.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 4929283874..9c6bce1c90 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -853,7 +853,7 @@ namespace Avalonia.Win32 private void ShowWindow(WindowState state) { - ShowWindowCommand command; + ShowWindowCommand? command; var newWindowProperties = _windowProperties; @@ -875,8 +875,8 @@ namespace Avalonia.Win32 case WindowState.FullScreen: newWindowProperties.IsFullScreen = true; - UpdateWindowProperties(newWindowProperties); - return; + command = IsWindowVisible(_hwnd) ? (ShowWindowCommand?)null : ShowWindowCommand.Restore; + break; default: throw new ArgumentException("Invalid WindowState."); @@ -884,7 +884,10 @@ namespace Avalonia.Win32 UpdateWindowProperties(newWindowProperties); - UnmanagedMethods.ShowWindow(_hwnd, command); + if (command.HasValue) + { + UnmanagedMethods.ShowWindow(_hwnd, command.Value); + } if (state == WindowState.Maximized) { From ecfd4bf56107b6235a316611d0a11673cd63db32 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 10 Sep 2020 17:14:49 +0100 Subject: [PATCH 59/60] remove padding from checkbox and radiobutton. --- src/Avalonia.Themes.Fluent/CheckBox.xaml | 1 - src/Avalonia.Themes.Fluent/RadioButton.xaml | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index 0ba6fb81c2..04799b3bc2 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -8,7 +8,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml index 35cc902d91..8fd569816d 100644 --- a/src/Avalonia.Themes.Fluent/RadioButton.xaml +++ b/src/Avalonia.Themes.Fluent/RadioButton.xaml @@ -13,7 +13,6 @@ - From ca5ec1fba49884af7edba49a982e0b5d849bd8a9 Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Fri, 11 Sep 2020 00:22:56 -0400 Subject: [PATCH 60/60] Add left padding for radiobutton and checkbox --- src/Avalonia.Themes.Fluent/CheckBox.xaml | 1 + src/Avalonia.Themes.Fluent/RadioButton.xaml | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index 04799b3bc2..73c44c02ab 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -8,6 +8,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml index 8fd569816d..078f51c87a 100644 --- a/src/Avalonia.Themes.Fluent/RadioButton.xaml +++ b/src/Avalonia.Themes.Fluent/RadioButton.xaml @@ -13,6 +13,7 @@ +