From 8cfa7c175c4e9094d52448137db7731532a50c26 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Fri, 17 May 2019 15:58:36 +0200 Subject: [PATCH 01/60] Add blank cursor --- src/Avalonia.Input/Cursors.cs | 1 + src/Avalonia.X11/X11CursorFactory.cs | 22 ++++++++++++++++++++- src/Gtk/Avalonia.Gtk3/CursorFactory.cs | 7 ++++--- src/Gtk/Avalonia.Gtk3/GdkCursor.cs | 1 + src/Windows/Avalonia.Win32/CursorFactory.cs | 5 +++-- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs index d3618f30f3..8139af1659 100644 --- a/src/Avalonia.Input/Cursors.cs +++ b/src/Avalonia.Input/Cursors.cs @@ -38,6 +38,7 @@ namespace Avalonia.Input DragMove, DragCopy, DragLink, + None, // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 40b01117e3..c5566318a2 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -8,6 +8,8 @@ namespace Avalonia.X11 { class X11CursorFactory : IStandardCursorFactory { + private static IntPtr _nullCursor; + private readonly IntPtr _display; private Dictionary _cursors; @@ -42,16 +44,34 @@ namespace Avalonia.X11 public X11CursorFactory(IntPtr display) { _display = display; + _nullCursor = GetNullCursor(display); _cursors = Enum.GetValues(typeof(CursorFontShape)).Cast() .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } public IPlatformHandle GetCursor(StandardCursorType cursorType) { - var handle = s_mapping.TryGetValue(cursorType, out var shape) + IntPtr handle; + if (cursorType == StandardCursorType.None) + { + handle = _nullCursor; + } + else + { + handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; + } return new PlatformHandle(handle, "XCURSOR"); } + + private static IntPtr GetNullCursor(IntPtr display) + { + XColor color = new XColor(); + byte[] data = new byte[] { 0 }; + IntPtr window = XLib.XRootWindow(display, 0); + IntPtr pixmap = XLib.XCreatePixmapFromBitmapData(display, window, data, 1, 1, IntPtr.Zero, IntPtr.Zero, 0); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + } } } diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index a28b1cbb1a..f36e6b986d 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -12,7 +12,8 @@ namespace Avalonia.Gtk3 private static readonly Dictionary CursorTypeMapping = new Dictionary { - {StandardCursorType.AppStarting, CursorType.Watch}, + {StandardCursorType.None, CursorType.Blank}, + { StandardCursorType.AppStarting, CursorType.Watch}, {StandardCursorType.Arrow, CursorType.LeftPtr}, {StandardCursorType.Cross, CursorType.Cross}, {StandardCursorType.Hand, CursorType.Hand1}, @@ -36,7 +37,7 @@ namespace Avalonia.Gtk3 {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}, {StandardCursorType.DragCopy, CursorType.CenterPtr}, {StandardCursorType.DragMove, CursorType.Fleur}, - {StandardCursorType.DragLink, CursorType.Cross}, + {StandardCursorType.DragLink, CursorType.Cross}, }; private static readonly Dictionary Cache = @@ -80,4 +81,4 @@ namespace Avalonia.Gtk3 return rv; } } -} \ No newline at end of file +} diff --git a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs index 4fad8208b3..aa0f8cde0d 100644 --- a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs +++ b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs @@ -2,6 +2,7 @@ { enum GdkCursorType { + Blank = -2, CursorIsPixmap = -1, XCursor = 0, Arrow = 2, diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index e582b5fb82..df413addef 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -41,7 +41,8 @@ namespace Avalonia.Win32 private static readonly Dictionary CursorTypeMapping = new Dictionary { - {StandardCursorType.AppStarting, 32650}, + {StandardCursorType.None, 0}, + { StandardCursorType.AppStarting, 32650}, {StandardCursorType.Arrow, 32512}, {StandardCursorType.Cross, 32515}, {StandardCursorType.Hand, 32649}, @@ -69,7 +70,7 @@ namespace Avalonia.Win32 // Fallback, should have been loaded from ole32.dll {StandardCursorType.DragMove, 32516}, {StandardCursorType.DragCopy, 32516}, - {StandardCursorType.DragLink, 32516}, + {StandardCursorType.DragLink, 32516}, }; private static readonly Dictionary Cache = From 4ad1acd24eeb836ff3b4fb06cb83011ed635d1e0 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Mon, 20 May 2019 12:05:49 +0200 Subject: [PATCH 02/60] Fix X11 NullCursor --- src/Avalonia.X11/X11CursorFactory.cs | 8 ++++---- src/Avalonia.X11/XLib.cs | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index c5566318a2..c020c44662 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -51,7 +51,7 @@ namespace Avalonia.X11 public IPlatformHandle GetCursor(StandardCursorType cursorType) { - IntPtr handle; + IntPtr handle; if (cursorType == StandardCursorType.None) { handle = _nullCursor; @@ -66,12 +66,12 @@ namespace Avalonia.X11 } private static IntPtr GetNullCursor(IntPtr display) - { + { XColor color = new XColor(); byte[] data = new byte[] { 0 }; IntPtr window = XLib.XRootWindow(display, 0); - IntPtr pixmap = XLib.XCreatePixmapFromBitmapData(display, window, data, 1, 1, IntPtr.Zero, IntPtr.Zero, 0); - return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, data, 1, 1); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } } } diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 8a146f922d..3c41f7bdde 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -321,6 +321,9 @@ namespace Avalonia.X11 public static extern IntPtr XCreatePixmapCursor(IntPtr display, IntPtr source, IntPtr mask, ref XColor foreground_color, ref XColor background_color, int x_hot, int y_hot); + [DllImport(libX11)] + public static extern IntPtr XCreateBitmapFromData(IntPtr display, IntPtr drawable, byte[] data, int width, int height); + [DllImport(libX11)] public static extern IntPtr XCreatePixmapFromBitmapData(IntPtr display, IntPtr drawable, byte[] data, int width, int height, IntPtr fg, IntPtr bg, int depth); From 3860fafa000e9127f436745b998f6d8994cdb715 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Mon, 20 May 2019 12:10:49 +0200 Subject: [PATCH 03/60] Fix formatting --- src/Avalonia.X11/X11CursorFactory.cs | 10 +++++----- src/Gtk/Avalonia.Gtk3/CursorFactory.cs | 4 ++-- src/Windows/Avalonia.Win32/CursorFactory.cs | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index c020c44662..0a8b1ee9c4 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -9,7 +9,7 @@ namespace Avalonia.X11 class X11CursorFactory : IStandardCursorFactory { private static IntPtr _nullCursor; - + private readonly IntPtr _display; private Dictionary _cursors; @@ -48,10 +48,10 @@ namespace Avalonia.X11 _cursors = Enum.GetValues(typeof(CursorFontShape)).Cast() .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } - + public IPlatformHandle GetCursor(StandardCursorType cursorType) { - IntPtr handle; + IntPtr handle; if (cursorType == StandardCursorType.None) { handle = _nullCursor; @@ -61,7 +61,7 @@ namespace Avalonia.X11 handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; - } + } return new PlatformHandle(handle, "XCURSOR"); } @@ -71,7 +71,7 @@ namespace Avalonia.X11 byte[] data = new byte[] { 0 }; IntPtr window = XLib.XRootWindow(display, 0); IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, data, 1, 1); - return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } } } diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index f36e6b986d..95fa3ba9e3 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -13,7 +13,7 @@ namespace Avalonia.Gtk3 { {StandardCursorType.None, CursorType.Blank}, - { StandardCursorType.AppStarting, CursorType.Watch}, + {StandardCursorType.AppStarting, CursorType.Watch}, {StandardCursorType.Arrow, CursorType.LeftPtr}, {StandardCursorType.Cross, CursorType.Cross}, {StandardCursorType.Hand, CursorType.Hand1}, @@ -37,7 +37,7 @@ namespace Avalonia.Gtk3 {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}, {StandardCursorType.DragCopy, CursorType.CenterPtr}, {StandardCursorType.DragMove, CursorType.Fleur}, - {StandardCursorType.DragLink, CursorType.Cross}, + {StandardCursorType.DragLink, CursorType.Cross}, }; private static readonly Dictionary Cache = diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index df413addef..f1fd74f931 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -42,7 +42,7 @@ namespace Avalonia.Win32 { {StandardCursorType.None, 0}, - { StandardCursorType.AppStarting, 32650}, + {StandardCursorType.AppStarting, 32650}, {StandardCursorType.Arrow, 32512}, {StandardCursorType.Cross, 32515}, {StandardCursorType.Hand, 32649}, @@ -70,7 +70,7 @@ namespace Avalonia.Win32 // Fallback, should have been loaded from ole32.dll {StandardCursorType.DragMove, 32516}, {StandardCursorType.DragCopy, 32516}, - {StandardCursorType.DragLink, 32516}, + {StandardCursorType.DragLink, 32516}, }; private static readonly Dictionary Cache = From c72195f45b3cb68ac259dba472889827973295ca Mon Sep 17 00:00:00 2001 From: ahopper Date: Mon, 20 May 2019 15:15:30 +0100 Subject: [PATCH 04/60] Fix use of CoerceCaretIndex on Text change --- src/Avalonia.Controls/TextBox.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 04b088e35c..d43957313e 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -214,9 +214,9 @@ namespace Avalonia.Controls if (!_ignoreTextChanges) { var caretIndex = CaretIndex; - SelectionStart = CoerceCaretIndex(SelectionStart, value?.Length ?? 0); - SelectionEnd = CoerceCaretIndex(SelectionEnd, value?.Length ?? 0); - CaretIndex = CoerceCaretIndex(caretIndex, value?.Length ?? 0); + SelectionStart = CoerceCaretIndex(SelectionStart, value); + SelectionEnd = CoerceCaretIndex(SelectionEnd, value); + CaretIndex = CoerceCaretIndex(caretIndex, value); if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing) { @@ -677,11 +677,15 @@ namespace Avalonia.Controls } } - private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text?.Length ?? 0); + private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text); - private int CoerceCaretIndex(int value, int length) + private int CoerceCaretIndex(int value, string text) { - var text = Text; + if (text == null) + { + return 0; + } + var length = text.Length; if (value < 0) { @@ -691,7 +695,7 @@ namespace Avalonia.Controls { return length; } - else if (value > 0 && text[value - 1] == '\r' && text[value] == '\n') + else if (value > 0 && text[value - 1] == '\r' && value < length && text[value] == '\n') { return value + 1; } From 8a9e997c6c9b4302c22ebee4cfe729453e83e231 Mon Sep 17 00:00:00 2001 From: ahopper Date: Tue, 21 May 2019 07:13:26 +0100 Subject: [PATCH 05/60] unit test added --- tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 0d87f6d0fe..9b62509138 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -385,6 +385,21 @@ namespace Avalonia.Controls.UnitTests Assert.True(target.SelectionEnd <= "123".Length); } } + [Fact] + public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123456789\r" + }; + target.CaretIndex = 11; + + Assert.True(true); + } + } private static TestServices Services => TestServices.MockThreadingInterface.With( standardCursorFactory: Mock.Of()); From c3f142af2e75c83c57262e9ab27efa7c25e02043 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 18:35:47 +0300 Subject: [PATCH 06/60] Add ViewModelViewHost and TransitioningUserControl --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 143 +++--------------- .../TransitioningUserControl.cs | 127 ++++++++++++++++ src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 75 +++++++++ 3 files changed, 219 insertions(+), 126 deletions(-) create mode 100644 src/Avalonia.ReactiveUI/TransitioningUserControl.cs create mode 100644 src/Avalonia.ReactiveUI/ViewModelViewHost.cs diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 4bd86a67c0..bf295aafca 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -53,33 +53,13 @@ namespace Avalonia.ReactiveUI /// ReactiveUI routing documentation website for more info. /// /// - public class RoutedViewHost : UserControl, IActivatable, IEnableLogger + public class RoutedViewHost : TransitioningUserControl, IActivatable, IEnableLogger { /// - /// The router dependency property. + /// for the property. /// public static readonly AvaloniaProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); - - /// - /// The default content property. - /// - public static readonly AvaloniaProperty DefaultContentProperty = - AvaloniaProperty.Register(nameof(DefaultContent)); - - /// - /// Fade in animation property. - /// - public static readonly AvaloniaProperty FadeInAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); - - /// - /// Fade out animation property. - /// - public static readonly AvaloniaProperty FadeOutAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); /// /// Initializes a new instance of the class. @@ -104,42 +84,6 @@ namespace Avalonia.ReactiveUI set => SetValue(RouterProperty, value); } - /// - /// Gets or sets the content displayed whenever there is no page currently routed. - /// - public object DefaultContent - { - get => GetValue(DefaultContentProperty); - set => SetValue(DefaultContentProperty, value); - } - - /// - /// Gets or sets the animation played when page appears. - /// - public IAnimation FadeInAnimation - { - get => GetValue(FadeInAnimationProperty); - set => SetValue(FadeInAnimationProperty, value); - } - - /// - /// Gets or sets the animation played when page disappears. - /// - public IAnimation FadeOutAnimation - { - get => GetValue(FadeOutAnimationProperty); - set => SetValue(FadeOutAnimationProperty, value); - } - - /// - /// Duplicates the Content property with a private setter. - /// - public new object Content - { - get => base.Content; - private set => base.Content = value; - } - /// /// Gets or sets the ReactiveUI view locator used by this router. /// @@ -149,82 +93,29 @@ namespace Avalonia.ReactiveUI /// Invoked when ReactiveUI router navigates to a view model. /// /// ViewModel to which the user navigates. - /// - /// Thrown when ViewLocator is unable to find the appropriate view. - /// - private void NavigateToViewModel(IRoutableViewModel viewModel) + private void NavigateToViewModel(object viewModel) { if (viewModel == null) { - this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(DefaultContent); + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; return; } var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var view = viewLocator.ResolveView(viewModel); - if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } - this.Log().Info($"Ready to show {view} with autowired {viewModel}."); - view.ViewModel = viewModel; - if (view is IStyledElement styled) + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) styled.DataContext = viewModel; - UpdateContent(view); - } - - /// - /// Updates the content with transitions. - /// - /// New content to set. - private async void UpdateContent(object newContent) - { - if (FadeOutAnimation != null) - await FadeOutAnimation.RunAsync(this, Clock); - Content = newContent; - if (FadeInAnimation != null) - await FadeInAnimation.RunAsync(this, Clock); - } - - /// - /// Creates opacity animation for this routed view host. - /// - /// Opacity to start from. - /// Opacity to finish with. - /// Duration of the animation. - /// Animation object instance. - private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) - { - return new Avalonia.Animation.Animation - { - Duration = duration, - Children = - { - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = from - } - }, - Cue = new Cue(0d) - }, - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = to - } - }, - Cue = new Cue(1d) - } - } - }; + Content = viewInstance; } } -} +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/TransitioningUserControl.cs b/src/Avalonia.ReactiveUI/TransitioningUserControl.cs new file mode 100644 index 0000000000..fb5258f2e4 --- /dev/null +++ b/src/Avalonia.ReactiveUI/TransitioningUserControl.cs @@ -0,0 +1,127 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.ReactiveUI +{ + /// + /// A ContentControl that animates the transition when its content is changed. + /// + public class TransitioningUserControl : UserControl + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty FadeInAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); + + /// + /// for the property. + /// + public static readonly AvaloniaProperty FadeOutAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); + + /// + /// for the property. + /// + public static readonly AvaloniaProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + /// + /// Gets or sets the animation played when content appears. + /// + public IAnimation FadeInAnimation + { + get => GetValue(FadeInAnimationProperty); + set => SetValue(FadeInAnimationProperty, value); + } + + /// + /// Gets or sets the animation played when content disappears. + /// + public IAnimation FadeOutAnimation + { + get => GetValue(FadeOutAnimationProperty); + set => SetValue(FadeOutAnimationProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Gets or sets the content with animation. + /// + public new object Content + { + get => base.Content; + set => UpdateContentWithTransition(value); + } + + /// + /// Updates the content with transitions. + /// + /// New content to set. + private async void UpdateContentWithTransition(object content) + { + if (FadeOutAnimation != null) + await FadeOutAnimation.RunAsync(this, Clock); + base.Content = content; + if (FadeInAnimation != null) + await FadeInAnimation.RunAsync(this, Clock); + } + + /// + /// Creates opacity animation for this routed view host. + /// + /// Opacity to start from. + /// Opacity to finish with. + /// Duration of the animation. + /// Animation object instance. + private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) + { + return new Avalonia.Animation.Animation + { + Duration = duration, + Children = + { + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = from + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = to + } + }, + Cue = new Cue(1d) + } + } + }; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs new file mode 100644 index 0000000000..84108a9b52 --- /dev/null +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -0,0 +1,75 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using ReactiveUI; +using Splat; + +namespace Avalonia.ReactiveUI +{ + /// + /// This content control will automatically load the View associated with + /// the ViewModel property and display it. This control is very useful + /// inside a DataTemplate to display the View associated with a ViewModel. + /// + public class ViewModelViewHost : TransitioningUserControl, IViewFor, IEnableLogger + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty ViewModelProperty = + AvaloniaProperty.Register(nameof(ViewModel)); + + /// + /// Gets or sets the ViewModel to display. + /// + public object ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + /// Gets or sets the view locator. + /// + public IViewLocator ViewLocator { get; set; } + + /// + /// Updates the Content when ViewModel changes. + /// + /// Property changed event arguments. + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Property.Name == nameof(ViewModel)) NavigateToViewModel(e.NewValue); + base.OnPropertyChanged(e); + } + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + private void NavigateToViewModel(object viewModel) + { + if (viewModel == null) + { + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; + return; + } + + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } + + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) + styled.DataContext = viewModel; + Content = viewInstance; + } + } +} \ No newline at end of file From 97b44c02d25d07c0e2adfecb842e016c54d2e434 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 19:09:51 +0300 Subject: [PATCH 07/60] Add a unit test for ViewModelViewHost --- src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 27 ++++--- .../Attributes.cs | 6 ++ .../AvaloniaActivationForViewFetcherTest.cs | 2 +- .../RoutedViewHostTest.cs | 2 +- .../ViewModelViewHostTest.cs | 72 +++++++++++++++++++ 5 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs index 84108a9b52..6f80bff4b8 100644 --- a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Reactive.Disposables; using ReactiveUI; using Splat; @@ -18,7 +20,20 @@ namespace Avalonia.ReactiveUI /// public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty.Register(nameof(ViewModel)); - + + /// + /// Initializes a new instance of the class. + /// + public ViewModelViewHost() + { + this.WhenActivated(disposables => + { + this.WhenAnyValue(x => x.ViewModel) + .Subscribe(NavigateToViewModel) + .DisposeWith(disposables); + }); + } + /// /// Gets or sets the ViewModel to display. /// @@ -33,16 +48,6 @@ namespace Avalonia.ReactiveUI /// public IViewLocator ViewLocator { get; set; } - /// - /// Updates the Content when ViewModel changes. - /// - /// Property changed event arguments. - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Property.Name == nameof(ViewModel)) NavigateToViewModel(e.NewValue); - base.OnPropertyChanged(e); - } - /// /// Invoked when ReactiveUI router navigates to a view model. /// diff --git a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs new file mode 100644 index 0000000000..79e58f63e9 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs @@ -0,0 +1,6 @@ +using Xunit; + +// Required to avoid InvalidOperationException sometimes thrown +// from Splat.MemoizingMRUCache.cs which is not thread-safe. +// Thrown when trying to access WhenActivated concurrently. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index d9f1ce47dd..f4dffdc0c3 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -13,7 +13,7 @@ using Splat; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Avalonia +namespace Avalonia.ReactiveUI.UnitTests { public class AvaloniaActivationForViewFetcherTest { diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index 401d169896..c6017d3f5f 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -16,7 +16,7 @@ using System.Threading.Tasks; using System.Reactive; using Avalonia.ReactiveUI; -namespace Avalonia +namespace Avalonia.ReactiveUI.UnitTests { public class RoutedViewHostTest { diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs new file mode 100644 index 0000000000..5d5d15358e --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -0,0 +1,72 @@ +using Avalonia.Controls; +using Avalonia.UnitTests; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class ViewModelViewHostTest + { + public class FirstViewModel { } + + public class FirstView : ReactiveUserControl { } + + public class SecondViewModel : ReactiveObject { } + + public class SecondView : ReactiveUserControl { } + + public ViewModelViewHostTest() + { + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor)); + } + + [Fact] + public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel() + { + var defaultContent = new TextBlock(); + var host = new ViewModelViewHost + { + DefaultContent = defaultContent, + FadeOutAnimation = null, + FadeInAnimation = null + }; + + var root = new TestRoot + { + Child = host + }; + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + var first = new FirstViewModel(); + host.ViewModel = first; + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstView), host.Content.GetType()); + Assert.Equal(first, ((FirstView)host.Content).DataContext); + Assert.Equal(first, ((FirstView)host.Content).ViewModel); + + var second = new SecondViewModel(); + host.ViewModel = second; + Assert.NotNull(host.Content); + Assert.Equal(typeof(SecondView), host.Content.GetType()); + Assert.Equal(second, ((SecondView)host.Content).DataContext); + Assert.Equal(second, ((SecondView)host.Content).ViewModel); + + host.ViewModel = null; + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + host.ViewModel = first; + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstView), host.Content.GetType()); + Assert.Equal(first, ((FirstView)host.Content).DataContext); + Assert.Equal(first, ((FirstView)host.Content).ViewModel); + } + } +} \ No newline at end of file From 724a5da8963ea83d616fce8691a9ded211ccb4ef Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 19:10:40 +0300 Subject: [PATCH 08/60] Add license headers --- tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs | 3 +++ .../AvaloniaActivationForViewFetcherTest.cs | 3 +++ tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs | 3 +++ tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs | 3 +++ 4 files changed, 12 insertions(+) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs index 79e58f63e9..a79647d355 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Xunit; // Required to avoid InvalidOperationException sometimes thrown diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index f4dffdc0c3..2c81e8fea3 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index c6017d3f5f..e873c60e36 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs index 5d5d15358e..a21bf34ef5 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Controls; using Avalonia.UnitTests; using ReactiveUI; From e8c0012c318522529f0e572a7535ab60d628d96e Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 19:26:29 +0300 Subject: [PATCH 09/60] Unit tests for ReactiveWindow and ReactiveUserControl --- .../ReactiveUserControlTest.cs | 34 +++++++++++++++++ .../ReactiveWindowTest.cs | 37 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs new file mode 100644 index 0000000000..328b749ba1 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.UnitTests; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class ReactiveUserControlTest + { + public class ExampleViewModel : ReactiveObject { } + + public class ExampleView : ReactiveUserControl { } + + [Fact] + public void Data_Context_Should_Stay_In_Sync_With_Reactive_User_Control_View_Model() + { + var view = new ExampleView(); + var viewModel = new ExampleViewModel(); + Assert.Null(view.ViewModel); + + view.DataContext = viewModel; + Assert.Equal(view.ViewModel, viewModel); + Assert.Equal(view.DataContext, viewModel); + + view.DataContext = null; + Assert.Null(view.ViewModel); + Assert.Null(view.DataContext); + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs new file mode 100644 index 0000000000..ff77de8d28 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs @@ -0,0 +1,37 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.UnitTests; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class ReactiveWindowTest + { + public class ExampleViewModel : ReactiveObject { } + + public class ExampleWindow : ReactiveWindow { } + + [Fact] + public void Data_Context_Should_Stay_In_Sync_With_Reactive_Window_View_Model() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var view = new ExampleWindow(); + var viewModel = new ExampleViewModel(); + Assert.Null(view.ViewModel); + + view.DataContext = viewModel; + Assert.Equal(view.ViewModel, viewModel); + Assert.Equal(view.DataContext, viewModel); + + view.DataContext = null; + Assert.Null(view.ViewModel); + Assert.Null(view.DataContext); + } + } + } +} \ No newline at end of file From fe7e7054fb7012b8f901de4d7abc8664bc9f40e9 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 20:40:26 +0300 Subject: [PATCH 10/60] Add initial AutoDataTemplateBindingHook implementation --- .../AutoDataTemplateBindingHook.cs | 60 +++++++++++++++++++ .../AutoDataTemplateBindingHookTest.cs | 56 +++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs new file mode 100644 index 0000000000..bd156a5884 --- /dev/null +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Templates; +using ReactiveUI; + +namespace Avalonia.ReactiveUI +{ + /// + /// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls + /// that don't have DataTemplates, and assigns a default DataTemplate that + /// loads the View associated with each ViewModel. + /// + public class AutoDataTemplateBindingHook : IPropertyBindingHook + { + private static Lazy DefaultItemTemplate { get; } = new Lazy(() => + { + var template = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + return (DataTemplate)loader.Load(template); + }); + + /// + public bool ExecuteHook( + object source, object target, + Func[]> getCurrentViewModelProperties, + Func[]> getCurrentViewProperties, + BindingDirection direction) + { + var viewProperties = getCurrentViewProperties(); + var lastViewProperty = viewProperties.LastOrDefault(); + if (lastViewProperty == null) + return true; + + var itemsControl = lastViewProperty.Sender as ItemsControl; + if (itemsControl == null) + return true; + + var propertyName = viewProperties.Last().GetPropertyName(); + if (propertyName != "Items" && + propertyName != "ItemsSource") + return true; + + if (itemsControl.ItemTemplate != null) + return true; + + itemsControl.ItemTemplate = DefaultItemTemplate.Value; + return true; + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs new file mode 100644 index 0000000000..2391daece2 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -0,0 +1,56 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Xunit; +using ReactiveUI; +using Avalonia.ReactiveUI; +using Avalonia.UnitTests; +using Avalonia.Controls; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Splat; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class AutoDataTemplateBindingHookTest + { + public class NestedViewModel : ReactiveObject { } + + public class NestedView : ReactiveUserControl { } + + public class ExampleViewModel : ReactiveObject + { + public ObservableCollection Items { get; } = new ObservableCollection(); + } + + public class ExampleView : ReactiveUserControl + { + public ListBox List { get; } = new ListBox(); + + public ExampleView() + { + Content = List; + ViewModel = new ExampleViewModel(); + this.OneWayBind(ViewModel, x => x.Items, x => x.List.Items); + } + } + + public AutoDataTemplateBindingHookTest() + { + Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); + Locator.CurrentMutable.Register(() => new ExampleView(), typeof(IViewFor)); + } + + [Fact] + public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set() + { + var view = new ExampleView(); + var root = new TestRoot + { + Child = view + }; + Assert.NotNull(view.List.ItemTemplate); + } + } +} \ No newline at end of file From b486112d296354f0d9d46f4fea72ea8bdb5c2bef Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 22:06:31 +0300 Subject: [PATCH 11/60] Use null propagation --- src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index bd156a5884..bb660fd76a 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -38,10 +38,7 @@ namespace Avalonia.ReactiveUI { var viewProperties = getCurrentViewProperties(); var lastViewProperty = viewProperties.LastOrDefault(); - if (lastViewProperty == null) - return true; - - var itemsControl = lastViewProperty.Sender as ItemsControl; + var itemsControl = lastViewProperty?.Sender as ItemsControl; if (itemsControl == null) return true; From 72fe95272a09644f72fd34c7d14acf42ef576819 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 22:08:52 +0300 Subject: [PATCH 12/60] Register auto data template binding hook --- src/Avalonia.ReactiveUI/AppBuilderExtensions.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index f67cb7f40a..ced26a3004 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -21,9 +21,8 @@ namespace Avalonia.ReactiveUI return builder.AfterSetup(_ => { RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; - Locator.CurrentMutable.Register( - () => new AvaloniaActivationForViewFetcher(), - typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); }); } } From d2574b383a1880bdd3aebfc94b9ee26ce653629d Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 22:47:18 +0300 Subject: [PATCH 13/60] Use FuncDataTemplate --- .../AutoDataTemplateBindingHook.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index bb660fd76a..d7cfebf1d8 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -1,6 +1,8 @@ using System; using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Layout; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Templates; using ReactiveUI; @@ -14,19 +16,18 @@ namespace Avalonia.ReactiveUI /// public class AutoDataTemplateBindingHook : IPropertyBindingHook { - private static Lazy DefaultItemTemplate { get; } = new Lazy(() => + private static Lazy DefaultItemTemplate { get; } = new Lazy(() => { - var template = @" - - -"; - - var loader = new AvaloniaXamlLoader(); - return (DataTemplate)loader.Load(template); + return new FuncDataTemplate(x => + { + var control = new ViewModelViewHost(); + var context = control.GetObservable(Control.DataContextProperty); + control.Bind(ViewModelViewHost.ViewModelProperty, context); + control.HorizontalContentAlignment = HorizontalAlignment.Stretch; + control.VerticalContentAlignment = VerticalAlignment.Stretch; + return control; + }, + true); }); /// From 65f5f58a07f29c5baf4fe80218cc2a83deba5dc6 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 23 May 2019 00:36:53 +0300 Subject: [PATCH 14/60] ViewModelViewHost type resolution assertion --- .../AutoDataTemplateBindingHookTest.cs | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs index 2391daece2..4ff3e1fed6 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -6,9 +6,12 @@ using ReactiveUI; using Avalonia.ReactiveUI; using Avalonia.UnitTests; using Avalonia.Controls; +using Avalonia.Controls.Templates; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.VisualTree; +using Avalonia.Controls.Presenters; using Splat; namespace Avalonia.ReactiveUI.UnitTests @@ -26,7 +29,7 @@ namespace Avalonia.ReactiveUI.UnitTests public class ExampleView : ReactiveUserControl { - public ListBox List { get; } = new ListBox(); + public ItemsControl List { get; } = new ItemsControl(); public ExampleView() { @@ -40,17 +43,48 @@ namespace Avalonia.ReactiveUI.UnitTests { Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); Locator.CurrentMutable.Register(() => new ExampleView(), typeof(IViewFor)); + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); } [Fact] public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set() { var view = new ExampleView(); - var root = new TestRoot - { - Child = view - }; Assert.NotNull(view.List.ItemTemplate); } + + [Fact] + public void Should_Use_View_Model_View_Host_As_Data_Template() + { + var view = new ExampleView(); + view.ViewModel.Items.Add(new NestedViewModel()); + + view.List.Template = GetTemplate(); + view.List.ApplyTemplate(); + view.List.Presenter.ApplyTemplate(); + + var child = view.List.Presenter.Panel.Children[0]; + var container = (ContentPresenter) child; + container.UpdateChild(); + + Assert.IsType(container.Child); + } + + private FuncControlTemplate GetTemplate() + { + return new FuncControlTemplate(parent => + { + return new Border + { + Background = new Media.SolidColorBrush(0xffffffff), + Child = new ItemsPresenter + { + Name = "PART_ItemsPresenter", + MemberSelector = parent.MemberSelector, + [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + } + }; + }); + } } } \ No newline at end of file From 8a2d1662fdcd92057d39aa39a2e8296e134ab0bd Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 23 May 2019 01:20:36 +0300 Subject: [PATCH 15/60] Remove LazyT, test new template resolver --- .../AutoDataTemplateBindingHook.cs | 23 +++++++-------- .../AutoDataTemplateBindingHookTest.cs | 28 ++++++++++++++++++- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index d7cfebf1d8..3f41f54363 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -16,19 +16,16 @@ namespace Avalonia.ReactiveUI /// public class AutoDataTemplateBindingHook : IPropertyBindingHook { - private static Lazy DefaultItemTemplate { get; } = new Lazy(() => + private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate(x => { - return new FuncDataTemplate(x => - { - var control = new ViewModelViewHost(); - var context = control.GetObservable(Control.DataContextProperty); - control.Bind(ViewModelViewHost.ViewModelProperty, context); - control.HorizontalContentAlignment = HorizontalAlignment.Stretch; - control.VerticalContentAlignment = VerticalAlignment.Stretch; - return control; - }, - true); - }); + var control = new ViewModelViewHost(); + var context = control.GetObservable(Control.DataContextProperty); + control.Bind(ViewModelViewHost.ViewModelProperty, context); + control.HorizontalContentAlignment = HorizontalAlignment.Stretch; + control.VerticalContentAlignment = VerticalAlignment.Stretch; + return control; + }, + true); /// public bool ExecuteHook( @@ -51,7 +48,7 @@ namespace Avalonia.ReactiveUI if (itemsControl.ItemTemplate != null) return true; - itemsControl.ItemTemplate = DefaultItemTemplate.Value; + itemsControl.ItemTemplate = DefaultItemTemplate; return true; } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs index 4ff3e1fed6..667462eb91 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -13,6 +13,8 @@ using System.Linq; using Avalonia.VisualTree; using Avalonia.Controls.Presenters; using Splat; +using System.Threading.Tasks; +using System; namespace Avalonia.ReactiveUI.UnitTests { @@ -42,8 +44,8 @@ namespace Avalonia.ReactiveUI.UnitTests public AutoDataTemplateBindingHookTest() { Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); - Locator.CurrentMutable.Register(() => new ExampleView(), typeof(IViewFor)); Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.Register(() => new NestedView(), typeof(IViewFor)); } [Fact] @@ -70,6 +72,30 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.IsType(container.Child); } + [Fact] + public void Should_Resolve_And_Embedd_Appropriate_View_Model() + { + var view = new ExampleView(); + var root = new TestRoot { Child = view }; + view.ViewModel.Items.Add(new NestedViewModel()); + + view.List.Template = GetTemplate(); + view.List.ApplyTemplate(); + view.List.Presenter.ApplyTemplate(); + + var child = view.List.Presenter.Panel.Children[0]; + var container = (ContentPresenter) child; + container.UpdateChild(); + + var host = (ViewModelViewHost) container.Child; + Assert.IsType(host.ViewModel); + Assert.IsType(host.DataContext); + + host.DataContext = "changed context"; + Assert.IsType(host.ViewModel); + Assert.IsType(host.DataContext); + } + private FuncControlTemplate GetTemplate() { return new FuncControlTemplate(parent => From 34358e0a2635258f0531f1c43496ac3b59295ae9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 May 2019 20:18:24 +0100 Subject: [PATCH 16/60] dont raise gestures if the events were already handled. --- src/Avalonia.Input/Gestures.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 8a514ca685..23b0ad466e 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -38,7 +38,10 @@ namespace Avalonia.Input } else if (s_lastPress?.IsAlive == true && e.ClickCount == 2 && s_lastPress.Target == e.Source) { - e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); + if (!ev.Handled) + { + e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); + } } } } @@ -51,7 +54,10 @@ namespace Avalonia.Input if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source) { - ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent)); + if (!ev.Handled) + { + ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent)); + } } } } From 1fd30a91c3733c7264f3bb65fc89cf2b307259c8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 May 2019 20:31:54 +0100 Subject: [PATCH 17/60] add a unit test for gestures not being raised when parent handles event. --- .../GestureTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs index 931c1b5268..bebf7f9bc9 100644 --- a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs @@ -78,5 +78,36 @@ namespace Avalonia.Interactivity.UnitTests Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); } + + [Fact] + public void DoubleTapped_Should_Not_Be_Rasied_if_Pressed_is_Handled() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var result = new List(); + + decorator.AddHandler(Border.PointerPressedEvent, (s, e) => + { + result.Add("dp"); + e.Handled = true; + }); + + decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt")); + border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); + border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); + + border.RaiseEvent(new PointerPressedEventArgs()); + border.RaiseEvent(new PointerReleasedEventArgs()); + border.RaiseEvent(new PointerPressedEventArgs { ClickCount = 2 }); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp" }, result); + } } } From f556db57b439f8474d8ed36834a27577ea974442 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 28 May 2019 23:33:54 +0300 Subject: [PATCH 18/60] Refactor pointer events to support touch events --- .../Calendar/CalendarButton.cs | 13 -- .../Calendar/CalendarDayButton.cs | 13 -- .../Calendar/CalendarItem.cs | 32 +---- src/Avalonia.Controls/MenuItem.cs | 18 +-- src/Avalonia.Input/IMouseDevice.cs | 2 + src/Avalonia.Input/IPointer.cs | 17 +++ src/Avalonia.Input/IPointerDevice.cs | 2 - src/Avalonia.Input/MouseDevice.cs | 134 ++++++++++-------- src/Avalonia.Input/PointerEventArgs.cs | 98 ++++++++++--- src/Avalonia.Input/PointerPoint.cs | 45 ++++++ src/Avalonia.Input/PointerWheelEventArgs.cs | 11 ++ src/Avalonia.Input/Raw/RawMouseEventArgs.cs | 3 + src/Avalonia.Input/Raw/RawTouchEventArgs.cs | 15 ++ .../ButtonTests.cs | 106 +++++--------- .../ComboBoxTests.cs | 14 +- .../ContextMenuTests.cs | 43 ++---- .../ListBoxTests.cs | 9 +- .../ListBoxTests_Single.cs | 41 ++---- .../MouseTestHelper.cs | 114 +++++++++++++++ .../DefaultMenuInteractionHandlerTests.cs | 52 +++++-- .../Primitives/SelectingItemsControlTests.cs | 15 +- .../TreeViewTests.cs | 89 +++++------- .../Avalonia.Interactivity.UnitTests.csproj | 2 + .../GestureTests.cs | 14 +- .../Xaml/XamlIlTests.cs | 13 +- 25 files changed, 515 insertions(+), 400 deletions(-) create mode 100644 src/Avalonia.Input/IPointer.cs create mode 100644 src/Avalonia.Input/PointerPoint.cs create mode 100644 src/Avalonia.Input/Raw/RawTouchEventArgs.cs create mode 100644 tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index 224d9b782b..53852defb3 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -176,18 +176,5 @@ namespace Avalonia.Controls.Primitives if (e.MouseButton == MouseButton.Left) CalendarLeftMouseButtonUp?.Invoke(this, e); } - - /// - /// We need to simulate the MouseLeftButtonUp event for the - /// CalendarButton that stays in Pressed state after MouseCapture is - /// released since there is no actual MouseLeftButtonUp event for the - /// release. - /// - /// Event arguments. - internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e) - { - e.Handled = false; - base.OnPointerReleased(e); - } } } diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index 1b36f92fd3..cb2a98e5ca 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -234,18 +234,5 @@ namespace Avalonia.Controls.Primitives if (e.MouseButton == MouseButton.Left) CalendarDayButtonMouseUp?.Invoke(this, e); } - - /// - /// We need to simulate the MouseLeftButtonUp event for the - /// CalendarDayButton that stays in Pressed state after MouseCapture is - /// released since there is no actual MouseLeftButtonUp event for the - /// release. - /// - /// Event arguments. - internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e) - { - e.Handled = false; - base.OnPointerReleased(e); - } } } diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index fb6dacaf81..8232697c18 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -934,22 +934,6 @@ namespace Avalonia.Controls.Primitives // The button is in Pressed state. Change the state to normal. if (e.Device.Captured == b) e.Device.Capture(null); - // null check is added for unit tests - if (_downEventArg != null) - { - var arg = - new PointerReleasedEventArgs() - { - Device = _downEventArg.Device, - MouseButton = _downEventArg.MouseButton, - Handled = _downEventArg.Handled, - InputModifiers = _downEventArg.InputModifiers, - Route = _downEventArg.Route, - Source = _downEventArg.Source - }; - - b.SendMouseLeftButtonUp(arg); - } _lastCalendarDayButton = b; } } @@ -1221,21 +1205,7 @@ namespace Avalonia.Controls.Primitives if (e.Device.Captured == b) e.Device.Capture(null); //b.ReleaseMouseCapture(); - if (_downEventArgYearView != null) - { - var args = - new PointerReleasedEventArgs() - { - Device = _downEventArgYearView.Device, - MouseButton = _downEventArgYearView.MouseButton, - Handled = _downEventArgYearView.Handled, - InputModifiers = _downEventArgYearView.InputModifiers, - Route = _downEventArgYearView.Route, - Source = _downEventArgYearView.Source - }; - - b.SendMouseLeftButtonUp(args); - } + _lastCalendarButton = b; } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index d8473dc613..d0ab0a0c8b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -337,12 +337,9 @@ namespace Avalonia.Controls { base.OnPointerEnter(e); - RaiseEvent(new PointerEventArgs - { - Device = e.Device, - RoutedEvent = PointerEnterItemEvent, - Source = this, - }); + var point = e.GetPointerPoint(null); + RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position, + point.Properties, e.InputModifiers)); } /// @@ -350,12 +347,9 @@ namespace Avalonia.Controls { base.OnPointerLeave(e); - RaiseEvent(new PointerEventArgs - { - Device = e.Device, - RoutedEvent = PointerLeaveItemEvent, - Source = this, - }); + var point = e.GetPointerPoint(null); + RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position, + point.Properties, e.InputModifiers)); } /// diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs index a1d1bb3eb8..76be8ba308 100644 --- a/src/Avalonia.Input/IMouseDevice.cs +++ b/src/Avalonia.Input/IMouseDevice.cs @@ -12,5 +12,7 @@ namespace Avalonia.Input /// Gets the mouse position, in screen coordinates. /// PixelPoint Position { get; } + + void SceneInvalidated(IInputRoot root, Rect rect); } } diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs new file mode 100644 index 0000000000..15f1fef531 --- /dev/null +++ b/src/Avalonia.Input/IPointer.cs @@ -0,0 +1,17 @@ +namespace Avalonia.Input +{ + public interface IPointer + { + void Capture(IInputElement control); + IInputElement Captured { get; } + PointerType Type { get; } + bool IsPrimary { get; } + + } + + public enum PointerType + { + Mouse, + Touch + } +} diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs index 932cbc989f..8613717f60 100644 --- a/src/Avalonia.Input/IPointerDevice.cs +++ b/src/Avalonia.Input/IPointerDevice.cs @@ -12,7 +12,5 @@ namespace Avalonia.Input void Capture(IInputElement control); Point GetPosition(IVisual relativeTo); - - void SceneInvalidated(IInputRoot root, Rect rect); } } diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index c195209305..352c69a726 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -14,14 +14,17 @@ namespace Avalonia.Input /// /// Represents a mouse device. /// - public class MouseDevice : IMouseDevice + public class MouseDevice : IMouseDevice, IPointer { private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; private IInputElement _captured; private IDisposable _capturedSubscription; - + + PointerType IPointer.Type => PointerType.Mouse; + bool IPointer.IsPrimary => true; + /// /// Gets the control that is currently capturing by the mouse, if any. /// @@ -117,6 +120,18 @@ namespace Avalonia.Input } } + int ButtonCount(PointerPointProperties props) + { + var rv = 0; + if (props.IsLeftButtonPressed) + rv++; + if (props.IsMiddleButtonPressed) + rv++; + if (props.IsRightButtonPressed) + rv++; + return rv; + } + private void ProcessRawEvent(RawMouseEventArgs e) { Contract.Requires(e != null); @@ -124,7 +139,7 @@ namespace Avalonia.Input var mouse = (IMouseDevice)e.Device; Position = e.Root.PointToScreen(e.Position); - + var props = CreateProperties(e); switch (e.Type) { case RawMouseEventType.LeaveWindow: @@ -133,26 +148,25 @@ namespace Avalonia.Input case RawMouseEventType.LeftButtonDown: case RawMouseEventType.RightButtonDown: case RawMouseEventType.MiddleButtonDown: - e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - e.Type == RawMouseEventType.LeftButtonDown - ? MouseButton.Left - : e.Type == RawMouseEventType.RightButtonDown ? MouseButton.Right : MouseButton.Middle, - e.InputModifiers); + if (ButtonCount(props) > 1) + e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers); + else + e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, + props, e.InputModifiers); break; case RawMouseEventType.LeftButtonUp: case RawMouseEventType.RightButtonUp: case RawMouseEventType.MiddleButtonUp: - e.Handled = MouseUp(mouse, e.Root, e.Position, - e.Type == RawMouseEventType.LeftButtonUp - ? MouseButton.Left - : e.Type == RawMouseEventType.RightButtonUp ? MouseButton.Right : MouseButton.Middle, - e.InputModifiers); + if (ButtonCount(props) != 0) + e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers); + else + e.Handled = MouseUp(mouse, e.Root, e.Position, props, e.InputModifiers); break; case RawMouseEventType.Move: - e.Handled = MouseMove(mouse, e.Root, e.Position, e.InputModifiers); + e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers); break; case RawMouseEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Root, e.Position, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers); + e.Handled = MouseWheel(mouse, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers); break; } } @@ -165,7 +179,30 @@ namespace Avalonia.Input ClearPointerOver(this, root, inputModifiers); } - private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, MouseButton button, InputModifiers inputModifiers) + + PointerPointProperties CreateProperties(RawMouseEventArgs args) + { + var rv = new PointerPointProperties(args.InputModifiers); + + if (args.Type == RawMouseEventType.LeftButtonDown) + rv.IsLeftButtonPressed = true; + if (args.Type == RawMouseEventType.MiddleButtonDown) + rv.IsMiddleButtonPressed = true; + if (args.Type == RawMouseEventType.RightButtonDown) + rv.IsRightButtonPressed = true; + if (args.Type == RawMouseEventType.LeftButtonUp) + rv.IsLeftButtonPressed = false; + if (args.Type == RawMouseEventType.MiddleButtonUp) + rv.IsMiddleButtonPressed = false; + if (args.Type == RawMouseEventType.RightButtonDown) + rv.IsRightButtonPressed = false; + return rv; + } + + private MouseButton _lastMouseDownButton; + private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, + PointerPointProperties properties, + InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -190,16 +227,8 @@ namespace Avalonia.Input _lastClickTime = timestamp; _lastClickRect = new Rect(p, new Size()) .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2)); - - var e = new PointerPressedEventArgs - { - Device = this, - RoutedEvent = InputElement.PointerPressedEvent, - Source = source, - ClickCount = _clickCount, - MouseButton = button, - InputModifiers = inputModifiers - }; + _lastMouseDownButton = properties.GetObsoleteMouseButton(); + var e = new PointerPressedEventArgs(source, this, root, p, properties, inputModifiers, _clickCount); source.RaiseEvent(e); return e.Handled; @@ -209,7 +238,8 @@ namespace Avalonia.Input return false; } - private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, InputModifiers inputModifiers) + private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, PointerPointProperties properties, + InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -226,19 +256,15 @@ namespace Avalonia.Input source = Captured; } - var e = new PointerEventArgs - { - Device = this, - RoutedEvent = InputElement.PointerMovedEvent, - Source = source, - InputModifiers = inputModifiers - }; + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, this, root, + p, properties, inputModifiers); source?.RaiseEvent(e); return e.Handled; } - private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, MouseButton button, InputModifiers inputModifiers) + private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, PointerPointProperties props, + InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -248,14 +274,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerReleasedEventArgs - { - Device = this, - RoutedEvent = InputElement.PointerReleasedEvent, - Source = source, - MouseButton = button, - InputModifiers = inputModifiers - }; + var e = new PointerReleasedEventArgs(source, this, root, p, props, inputModifiers, _lastMouseDownButton); source?.RaiseEvent(e); return e.Handled; @@ -264,7 +283,9 @@ namespace Avalonia.Input return false; } - private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p, Vector delta, InputModifiers inputModifiers) + private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -274,14 +295,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerWheelEventArgs - { - Device = this, - RoutedEvent = InputElement.PointerWheelChangedEvent, - Source = source, - Delta = delta, - InputModifiers = inputModifiers - }; + var e = new PointerWheelEventArgs(source, this, root, p, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; @@ -306,18 +320,19 @@ namespace Avalonia.Input return Captured ?? root.InputHitTest(p); } + PointerEventArgs CreateSimpleEvent(RoutedEvent ev, IInteractive source, InputModifiers inputModifiers) + { + return new PointerEventArgs(ev, source, this, null, default, + new PointerPointProperties(inputModifiers), inputModifiers); + } + private void ClearPointerOver(IPointerDevice device, IInputRoot root, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var element = root.PointerOverElement; - var e = new PointerEventArgs - { - RoutedEvent = InputElement.PointerLeaveEvent, - Device = device, - InputModifiers = inputModifiers - }; + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, element, inputModifiers); if (element!=null && !element.IsAttachedToVisualTree) { @@ -384,7 +399,6 @@ namespace Avalonia.Input IInputElement branch = null; - var e = new PointerEventArgs { Device = device, InputModifiers = inputModifiers }; var el = element; while (el != null) @@ -398,8 +412,8 @@ namespace Avalonia.Input } el = root.PointerOverElement; - e.RoutedEvent = InputElement.PointerLeaveEvent; + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, el, inputModifiers); if (el!=null && branch!=null && !el.IsAttachedToVisualTree) { ClearChildrenPointerOver(e,branch,false); diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 7b2497c460..91bfb694a0 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -8,25 +10,68 @@ namespace Avalonia.Input { public class PointerEventArgs : RoutedEventArgs { - public PointerEventArgs() - { + private readonly IVisual _rootVisual; + private readonly Point _rootVisualPosition; + private readonly PointerPointProperties _properties; + public PointerEventArgs(RoutedEvent routedEvent, + IInteractive source, + IPointer pointer, + IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, + InputModifiers modifiers) + : base(routedEvent) + { + Source = source; + _rootVisual = rootVisual; + _rootVisualPosition = rootVisualPosition; + _properties = properties; + Pointer = pointer; + InputModifiers = modifiers; +#pragma warning disable 612 +#pragma warning disable 618 + Device = new EmulatedDevice(this); +#pragma warning restore 618 +#pragma warning restore 612 } - public PointerEventArgs(RoutedEvent routedEvent) - : base(routedEvent) + class EmulatedDevice : IPointerDevice { + private readonly PointerEventArgs _ev; + public EmulatedDevice(PointerEventArgs ev) + { + _ev = ev; + } + + public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException(); + + public IInputElement Captured => _ev.Pointer.Captured; + public void Capture(IInputElement control) + { + _ev.Pointer.Capture(control); + } + + public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo); } - public IPointerDevice Device { get; set; } + public IPointer Pointer { get; } + + [Obsolete("Use Pointer to get pointer-specific information")] + public IPointerDevice Device { get; } - public InputModifiers InputModifiers { get; set; } + public InputModifiers InputModifiers { get; } public Point GetPosition(IVisual relativeTo) { - return Device.GetPosition(relativeTo); + if (_rootVisual == null) + return default; + if (relativeTo == null) + return _rootVisualPosition; + return _rootVisualPosition * _rootVisual.TransformToVisual(relativeTo) ?? default; } + + public PointerPoint GetPointerPoint(IVisual relativeTo) + => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); } public enum MouseButton @@ -39,32 +84,39 @@ namespace Avalonia.Input public class PointerPressedEventArgs : PointerEventArgs { - public PointerPressedEventArgs() - : base(InputElement.PointerPressedEvent) - { - } + private readonly int _obsoleteClickCount; - public PointerPressedEventArgs(RoutedEvent routedEvent) - : base(routedEvent) + public PointerPressedEventArgs( + IInteractive source, + IPointer pointer, + IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, + InputModifiers modifiers, + int obsoleteClickCount = 1) + : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, properties, + modifiers) { + _obsoleteClickCount = obsoleteClickCount; } - public int ClickCount { get; set; } - public MouseButton MouseButton { get; set; } + [Obsolete("Use DoubleTapped or DoubleRightTapped event instead")] + public int ClickCount => _obsoleteClickCount; + + [Obsolete] public MouseButton MouseButton => GetPointerPoint(null).Properties.GetObsoleteMouseButton(); } public class PointerReleasedEventArgs : PointerEventArgs { - public PointerReleasedEventArgs() - : base(InputElement.PointerReleasedEvent) - { - } - - public PointerReleasedEventArgs(RoutedEvent routedEvent) - : base(routedEvent) + public PointerReleasedEventArgs( + IInteractive source, IPointer pointer, + IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, InputModifiers modifiers, + MouseButton obsoleteMouseButton) + : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition, + properties, modifiers) { + MouseButton = obsoleteMouseButton; } - public MouseButton MouseButton { get; set; } + [Obsolete()] + public MouseButton MouseButton { get; private set; } } } diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs new file mode 100644 index 0000000000..7117b5709c --- /dev/null +++ b/src/Avalonia.Input/PointerPoint.cs @@ -0,0 +1,45 @@ +namespace Avalonia.Input +{ + public sealed class PointerPoint + { + public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties) + { + Pointer = pointer; + Position = position; + Properties = properties; + } + public IPointer Pointer { get; } + public PointerPointProperties Properties { get; } + public Point Position { get; } + } + + public sealed class PointerPointProperties + { + public bool IsLeftButtonPressed { get; set; } + public bool IsMiddleButtonPressed { get; set; } + public bool IsRightButtonPressed { get; set; } + + public PointerPointProperties() + { + + } + + public PointerPointProperties(InputModifiers modifiers) + { + IsLeftButtonPressed = modifiers.HasFlag(InputModifiers.LeftMouseButton); + IsMiddleButtonPressed = modifiers.HasFlag(InputModifiers.MiddleMouseButton); + IsRightButtonPressed = modifiers.HasFlag(InputModifiers.RightMouseButton); + } + + public MouseButton GetObsoleteMouseButton() + { + if (IsLeftButtonPressed) + return MouseButton.Left; + if (IsMiddleButtonPressed) + return MouseButton.Middle; + if (IsRightButtonPressed) + return MouseButton.Right; + return MouseButton.None; + } + } +} diff --git a/src/Avalonia.Input/PointerWheelEventArgs.cs b/src/Avalonia.Input/PointerWheelEventArgs.cs index 401d1d847c..b409cc81bd 100644 --- a/src/Avalonia.Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Input/PointerWheelEventArgs.cs @@ -1,10 +1,21 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Interactivity; +using Avalonia.VisualTree; + namespace Avalonia.Input { public class PointerWheelEventArgs : PointerEventArgs { public Vector Delta { get; set; } + + public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + Point rootVisualPosition, + PointerPointProperties properties, InputModifiers modifiers, Vector delta) + : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, properties, modifiers) + { + Delta = delta; + } } } diff --git a/src/Avalonia.Input/Raw/RawMouseEventArgs.cs b/src/Avalonia.Input/Raw/RawMouseEventArgs.cs index c5637d66cc..42dca6f3a6 100644 --- a/src/Avalonia.Input/Raw/RawMouseEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawMouseEventArgs.cs @@ -17,6 +17,9 @@ namespace Avalonia.Input.Raw Move, Wheel, NonClientLeftButtonDown, + TouchBegin, + TouchUpdate, + TouchEnd } /// diff --git a/src/Avalonia.Input/Raw/RawTouchEventArgs.cs b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs new file mode 100644 index 0000000000..9a266d090d --- /dev/null +++ b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Input.Raw +{ + public class RawTouchEventArgs : RawMouseEventArgs + { + public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root, + RawMouseEventType type, Point position, InputModifiers inputModifiers, + long touchPointId) + : base(device, timestamp, root, type, position, inputModifiers) + { + TouchPointId = touchPointId; + } + + public long TouchPointId { get; set; } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 9a751d4953..20b2ac5d9d 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -14,6 +14,8 @@ namespace Avalonia.Controls.UnitTests { public class ButtonTests { + private MouseTestHelper _helper = new MouseTestHelper(); + [Fact] public void Button_Is_Disabled_When_Command_Is_Disabled() { @@ -102,12 +104,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Button_Raises_Click() { - var mouse = Mock.Of(); var renderer = Mock.Of(); - IInputElement captured = null; - Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(50, 50)); - Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); - Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + var pt = new Point(50, 50); Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]); @@ -122,15 +120,15 @@ namespace Avalonia.Controls.UnitTests target.Click += (s, e) => clicked = true; - RaisePointerEnter(target, mouse); - RaisePointerMove(target, mouse); - RaisePointerPressed(target, mouse, 1, MouseButton.Left); + RaisePointerEnter(target); + RaisePointerMove(target, pt); + RaisePointerPressed(target, 1, MouseButton.Left, pt); - Assert.Equal(captured, target); + Assert.Equal(_helper.Captured, target); - RaisePointerReleased(target, mouse, MouseButton.Left); + RaisePointerReleased(target, MouseButton.Left, pt); - Assert.Equal(captured, null); + Assert.Equal(_helper.Captured, null); Assert.True(clicked); } @@ -138,12 +136,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside() { - var mouse = Mock.Of(); var renderer = Mock.Of(); - IInputElement captured = null; - Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(200, 50)); - Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); - Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]); @@ -158,16 +152,16 @@ namespace Avalonia.Controls.UnitTests target.Click += (s, e) => clicked = true; - RaisePointerEnter(target, mouse); - RaisePointerMove(target, mouse); - RaisePointerPressed(target, mouse, 1, MouseButton.Left); - RaisePointerLeave(target, mouse); + RaisePointerEnter(target); + RaisePointerMove(target, new Point(50,50)); + RaisePointerPressed(target, 1, MouseButton.Left, new Point(50, 50)); + RaisePointerLeave(target); - Assert.Equal(captured, target); + Assert.Equal(_helper.Captured, target); - RaisePointerReleased(target, mouse, MouseButton.Left); + RaisePointerReleased(target, MouseButton.Left, new Point(200, 50)); - Assert.Equal(captured, null); + Assert.Equal(_helper.Captured, null); Assert.False(clicked); } @@ -175,12 +169,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Button_With_RenderTransform_Raises_Click() { - var mouse = Mock.Of(); var renderer = Mock.Of(); - IInputElement captured = null; - Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(150, 50)); - Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); - Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + var pt = new Point(150, 50); Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ? @@ -204,15 +194,15 @@ namespace Avalonia.Controls.UnitTests target.Click += (s, e) => clicked = true; - RaisePointerEnter(target, mouse); - RaisePointerMove(target, mouse); - RaisePointerPressed(target, mouse, 1, MouseButton.Left); + RaisePointerEnter(target); + RaisePointerMove(target, pt); + RaisePointerPressed(target, 1, MouseButton.Left, pt); - Assert.Equal(captured, target); + Assert.Equal(_helper.Captured, target); - RaisePointerReleased(target, mouse, MouseButton.Left); + RaisePointerReleased(target, MouseButton.Left, pt); - Assert.Equal(captured, null); + Assert.Equal(_helper.Captured, null); Assert.True(clicked); } @@ -278,57 +268,29 @@ namespace Avalonia.Controls.UnitTests public PixelPoint PointToScreen(Point p) => throw new NotImplementedException(); } - private void RaisePointerPressed(Button button, IMouseDevice device, int clickCount, MouseButton mouseButton) + private void RaisePointerPressed(Button button, int clickCount, MouseButton mouseButton, Point position) { - button.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - Source = button, - MouseButton = mouseButton, - ClickCount = clickCount, - Device = device, - }); + _helper.Down(button, mouseButton, position, clickCount: clickCount); } - private void RaisePointerReleased(Button button, IMouseDevice device, MouseButton mouseButton) + private void RaisePointerReleased(Button button, MouseButton mouseButton, Point pt) { - button.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - Source = button, - MouseButton = mouseButton, - Device = device, - }); + _helper.Up(button, mouseButton, pt); } - private void RaisePointerEnter(Button button, IMouseDevice device) + private void RaisePointerEnter(Button button) { - button.RaiseEvent(new PointerEventArgs - { - RoutedEvent = InputElement.PointerEnterEvent, - Source = button, - Device = device, - }); + _helper.Enter(button); } - private void RaisePointerLeave(Button button, IMouseDevice device) + private void RaisePointerLeave(Button button) { - button.RaiseEvent(new PointerEventArgs - { - RoutedEvent = InputElement.PointerLeaveEvent, - Source = button, - Device = device, - }); + _helper.Leave(button); } - private void RaisePointerMove(Button button, IMouseDevice device) + private void RaisePointerMove(Button button, Point pos) { - button.RaiseEvent(new PointerEventArgs - { - RoutedEvent = InputElement.PointerMovedEvent, - Source = button, - Device = device, - }); + _helper.Move(button, pos); } private class TestCommand : ICommand diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index fdbca70350..70ec6c1408 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -15,6 +15,8 @@ namespace Avalonia.Controls.UnitTests { public class ComboBoxTests { + MouseTestHelper _helper = new MouseTestHelper(); + [Fact] public void Clicking_On_Control_Toggles_IsDropDownOpen() { @@ -23,17 +25,11 @@ namespace Avalonia.Controls.UnitTests Items = new[] { "Foo", "Bar" }, }; - target.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - }); - + _helper.Down(target); + _helper.Up(target); Assert.True(target.IsDropDownOpen); - target.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - }); + _helper.Down(target); Assert.False(target.IsDropDownOpen); } diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 834c49ba6b..067c66969f 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -14,6 +14,7 @@ namespace Avalonia.Controls.UnitTests public class ContextMenuTests { private Mock popupImpl; + private MouseTestHelper _mouse = new MouseTestHelper(); [Fact] public void Clicking_On_Control_Toggles_ContextMenu() @@ -31,19 +32,11 @@ namespace Avalonia.Controls.UnitTests new Window { Content = target }; - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.None - }); + _mouse.Click(target); Assert.False(sut.IsOpen); popupImpl.Verify(x => x.Show(), Times.Once); @@ -69,19 +62,11 @@ namespace Avalonia.Controls.UnitTests Avalonia.Application.Current.MainWindow = window; - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); popupImpl.Verify(x => x.Hide(), Times.Once); @@ -106,11 +91,7 @@ namespace Avalonia.Controls.UnitTests sut.ContextMenuOpening += (c, e) => { eventCalled = true; e.Cancel = true; }; - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(eventCalled); Assert.False(sut.IsOpen); @@ -136,19 +117,11 @@ namespace Avalonia.Controls.UnitTests sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; }; - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.None - }); + _mouse.Click(target, MouseButton.Right); Assert.True(eventCalled); Assert.True(sut.IsOpen); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 343d8d41f3..238e214a5d 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -16,6 +16,8 @@ namespace Avalonia.Controls.UnitTests { public class ListBoxTests { + private MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] public void Should_Use_ItemTemplate_To_Create_Item_Content() { @@ -225,12 +227,7 @@ namespace Avalonia.Controls.UnitTests private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton) { - listBox.RaiseEvent(new PointerPressedEventArgs - { - Source = item, - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = mouseButton - }); + _mouse.Click(listBox, item, mouseButton); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index 70d59e82c8..de34558ad1 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -18,6 +18,8 @@ namespace Avalonia.Controls.UnitTests { public class ListBoxTests_Single { + MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] public void Focusing_Item_With_Tab_Should_Not_Select_It() { @@ -68,12 +70,7 @@ namespace Avalonia.Controls.UnitTests }; ApplyTemplate(target); - - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -90,11 +87,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); target.SelectedIndex = 0; - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -111,11 +104,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -133,11 +122,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); target.SelectedIndex = 0; - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(-1, target.SelectedIndex); } @@ -155,11 +140,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); target.SelectedIndex = 0; - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -177,11 +158,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); target.SelectedIndex = 1; - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -306,4 +283,4 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs new file mode 100644 index 0000000000..8b61f82c28 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs @@ -0,0 +1,114 @@ +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.UnitTests +{ + public class MouseTestHelper + { + + class TestPointer : IPointer + { + public void Capture(IInputElement control) + { + Captured = control; + } + + public IInputElement Captured { get; set; } + public PointerType Type => PointerType.Mouse; + public bool IsPrimary => true; + } + + TestPointer _pointer = new TestPointer(); + + private InputModifiers _pressedButtons; + public IInputElement Captured => _pointer.Captured; + + InputModifiers Convert(MouseButton mouseButton) + => (mouseButton == MouseButton.Left ? InputModifiers.LeftMouseButton + : mouseButton == MouseButton.Middle ? InputModifiers.MiddleMouseButton + : mouseButton == MouseButton.Right ? InputModifiers.RightMouseButton : InputModifiers.None); + + int ButtonCount(PointerPointProperties props) + { + var rv = 0; + if (props.IsLeftButtonPressed) + rv++; + if (props.IsMiddleButtonPressed) + rv++; + if (props.IsRightButtonPressed) + rv++; + return rv; + } + + private MouseButton _pressedButton; + + InputModifiers GetModifiers(InputModifiers modifiers) => modifiers | _pressedButtons; + + public void Down(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default, + InputModifiers modifiers = default, int clickCount = 1) + => Down(target, target, mouseButton, position, modifiers, clickCount); + + public void Down(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left, + Point position = default, InputModifiers modifiers = default, int clickCount = 1) + { + _pressedButtons |= Convert(mouseButton); + var props = new PointerPointProperties(_pressedButtons); + if (ButtonCount(props) > 1) + Move(target, source, position); + else + { + _pressedButton = mouseButton; + target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, props, + GetModifiers(modifiers), clickCount)); + } + } + + public void Move(IInteractive target, in Point position, InputModifiers modifiers = default) => Move(target, target, position, modifiers); + public void Move(IInteractive target, IInteractive source, in Point position, InputModifiers modifiers = default) + { + target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (IVisual)target, position, + new PointerPointProperties(_pressedButtons), GetModifiers(modifiers))); + } + + public void Up(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default, + InputModifiers modifiers = default) + => Up(target, target, mouseButton, position, modifiers); + + public void Up(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left, + Point position = default, InputModifiers modifiers = default) + { + var conv = Convert(mouseButton); + _pressedButtons = (_pressedButtons | conv) ^ conv; + var props = new PointerPointProperties(_pressedButtons); + if (ButtonCount(props) == 0) + target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, props, + GetModifiers(modifiers), _pressedButton)); + else + Move(target, source, position); + } + + public void Click(IInteractive target, MouseButton button = MouseButton.Left, Point position = default, + InputModifiers modifiers = default) + => Click(target, target, button, position, modifiers); + public void Click(IInteractive target, IInteractive source, MouseButton button = MouseButton.Left, + Point position = default, InputModifiers modifiers = default) + { + Down(target, source, button, position, modifiers); + Up(target, source, button, position, modifiers); + } + + public void Enter(IInteractive target) + { + target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnterEvent, target, _pointer, (IVisual)target, default, + new PointerPointProperties(_pressedButtons), _pressedButtons)); + } + + public void Leave(IInteractive target) + { + target.RaiseEvent(new PointerEventArgs(InputElement.PointerLeaveEvent, target, _pointer, (IVisual)target, default, + new PointerPointProperties(_pressedButtons), _pressedButtons)); + } + + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index df1846c617..94a95b6db4 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; using Moq; using Xunit; @@ -9,6 +10,28 @@ namespace Avalonia.Controls.UnitTests.Platform { public class DefaultMenuInteractionHandlerTests { + class FakePointer : IPointer + { + public void Capture(IInputElement control) + { + Captured = control; + } + + public IInputElement Captured { get; set; } + public PointerType Type { get; } + public bool IsPrimary { get; } = true; + } + + static PointerEventArgs CreateArgs(RoutedEvent ev, IInteractive source) + => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, new PointerPointProperties(), default); + + static PointerPressedEventArgs CreatePressed(IInteractive source) => new PointerPressedEventArgs(source, + new FakePointer(), (IVisual)source, default, new PointerPointProperties {IsLeftButtonPressed = true}, + default); + + static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source, + new FakePointer(), (IVisual)source, default, new PointerPointProperties(), default, MouseButton.Left); + public class TopLevel { [Fact] @@ -121,7 +144,8 @@ namespace Avalonia.Controls.UnitTests.Platform x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); - var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item }; + + var e = CreatePressed(item); target.PointerPressed(item, e); Mock.Get(menu).Verify(x => x.Close()); @@ -141,7 +165,7 @@ namespace Avalonia.Controls.UnitTests.Platform x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu.Object); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = nextItem }; + var e = CreateArgs(MenuItem.PointerEnterItemEvent, nextItem); menu.SetupGet(x => x.SelectedItem).Returns(item); @@ -161,7 +185,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); menu.SetupGet(x => x.SelectedItem).Returns(item); target.PointerLeave(item, e); @@ -176,7 +200,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); menu.SetupGet(x => x.IsOpen).Returns(true); menu.SetupGet(x => x.SelectedItem).Returns(item); @@ -330,7 +354,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerEnterItemEvent, item); target.PointerEnter(item, e); @@ -346,7 +370,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerEnterItemEvent, item); target.PointerEnter(item, e); Mock.Get(item).Verify(x => x.Open(), Times.Never); @@ -366,7 +390,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); var sibling = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsSubMenuOpen == true); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerEnterItemEvent, item); Mock.Get(parentItem).SetupGet(x => x.SubItems).Returns(new[] { item, sibling }); @@ -386,7 +410,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(item); target.PointerLeave(item, e); @@ -403,7 +427,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); var sibling = Mock.Of(x => x.Parent == parentItem); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(sibling); target.PointerLeave(item, e); @@ -419,7 +443,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsPointerOverSubMenu == true); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); target.PointerLeave(item, e); @@ -434,7 +458,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); - var e = new PointerReleasedEventArgs { MouseButton = MouseButton.Left, Source = item }; + var e = CreateReleased(item); target.PointerReleased(item, e); @@ -452,8 +476,8 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); var childItem = Mock.Of(x => x.Parent == item); - var enter = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; - var leave = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var enter = CreateArgs(MenuItem.PointerEnterItemEvent, item); + var leave = CreateArgs(MenuItem.PointerLeaveItemEvent, item); // Pointer enters item; item is selected. target.PointerEnter(item, enter); @@ -488,7 +512,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); - var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item }; + var e = CreatePressed(item); target.PointerPressed(item, e); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 037c16e231..3b5aa53d56 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -22,6 +22,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class SelectingItemsControlTests { + private MouseTestHelper _helper = new MouseTestHelper(); + [Fact] public void SelectedIndex_Should_Initially_Be_Minus_1() { @@ -675,12 +677,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - - target.Presenter.Panel.Children[1].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _helper.Down((Interactive)target.Presenter.Panel.Children[1]); var panel = target.Presenter.Panel; @@ -703,11 +700,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.Presenter.Panel.Children[1].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _helper.Down(target.Presenter.Panel.Children[1]); items.RemoveAt(1); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 15081b184c..b66d6ed11c 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -19,6 +19,8 @@ namespace Avalonia.Controls.UnitTests { public class TreeViewTests { + MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] public void Items_Should_Be_Created() { @@ -130,11 +132,7 @@ namespace Avalonia.Controls.UnitTests Assert.NotNull(container); - container.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(container); Assert.Equal(item, target.SelectedItem); Assert.True(container.IsSelected); @@ -165,12 +163,7 @@ namespace Avalonia.Controls.UnitTests Assert.True(container.IsSelected); - container.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - InputModifiers = InputModifiers.Control - }); + _mouse.Click(container, modifiers: InputModifiers.Control); Assert.Null(target.SelectedItem); Assert.False(container.IsSelected); @@ -205,13 +198,8 @@ namespace Avalonia.Controls.UnitTests Assert.True(container1.IsSelected); - container2.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - InputModifiers = InputModifiers.Control - }); - + _mouse.Click(container2, modifiers: InputModifiers.Control); + Assert.Equal(item2, target.SelectedItem); Assert.False(container1.IsSelected); Assert.True(container2.IsSelected); @@ -242,15 +230,15 @@ namespace Avalonia.Controls.UnitTests var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); - TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control); + ClickContainer(item1Container, InputModifiers.Control); Assert.True(item1Container.IsSelected); - TreeTestHelper.ClickContainer(item2Container, InputModifiers.Control); + ClickContainer(item2Container, InputModifiers.Control); Assert.True(item2Container.IsSelected); Assert.Equal(new[] {item1, item2}, target.SelectedItems.OfType()); - TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control); + ClickContainer(item1Container, InputModifiers.Control); Assert.False(item1Container.IsSelected); Assert.DoesNotContain(item1, target.SelectedItems.OfType()); @@ -281,12 +269,12 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(fromContainer, InputModifiers.None); Assert.True(fromContainer.IsSelected); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, InputModifiers.Shift); + AssertChildrenSelected(target, rootNode); } [Fact] @@ -314,12 +302,12 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(fromContainer, InputModifiers.None); Assert.True(fromContainer.IsSelected); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, InputModifiers.Shift); + AssertChildrenSelected(target, rootNode); } [Fact] @@ -347,12 +335,12 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(fromContainer, InputModifiers.None); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, InputModifiers.Shift); + AssertChildrenSelected(target, rootNode); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(fromContainer, InputModifiers.None); Assert.True(fromContainer.IsSelected); @@ -656,7 +644,7 @@ namespace Avalonia.Controls.UnitTests target.RaiseEvent(keyEvent); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + AssertChildrenSelected(target, rootNode); } } @@ -687,8 +675,8 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); + ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(toContainer, InputModifiers.Shift); var keymap = AvaloniaLocator.Current.GetService(); var selectAllGesture = keymap.SelectAll.First(); @@ -702,7 +690,7 @@ namespace Avalonia.Controls.UnitTests target.RaiseEvent(keyEvent); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + AssertChildrenSelected(target, rootNode); } } @@ -733,8 +721,8 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); + ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(toContainer, InputModifiers.Shift); var keymap = AvaloniaLocator.Current.GetService(); var selectAllGesture = keymap.SelectAll.First(); @@ -748,7 +736,7 @@ namespace Avalonia.Controls.UnitTests target.RaiseEvent(keyEvent); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + AssertChildrenSelected(target, rootNode); } } @@ -871,29 +859,22 @@ namespace Avalonia.Controls.UnitTests } } - private static class TreeTestHelper + void ClickContainer(IControl container, InputModifiers modifiers) { - public static void ClickContainer(IControl container, InputModifiers modifiers) - { - container.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - InputModifiers = modifiers - }); - } + _mouse.Click(container, modifiers: modifiers); + } - public static void AssertChildrenSelected(TreeView treeView, Node rootNode) + void AssertChildrenSelected(TreeView treeView, Node rootNode) + { + foreach (var child in rootNode.Children) { - foreach (var child in rootNode.Children) - { - var container = (TreeViewItem)treeView.ItemContainerGenerator.Index.ContainerFromItem(child); + var container = (TreeViewItem)treeView.ItemContainerGenerator.Index.ContainerFromItem(child); - Assert.True(container.IsSelected); - } + Assert.True(container.IsSelected); } } + private class Node : NotifyingBase { private IAvaloniaList _children; diff --git a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj index 6b19d81034..7316b1de3d 100644 --- a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj +++ b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj @@ -4,6 +4,7 @@ netcoreapp2.0;net47 Library true + latest @@ -19,6 +20,7 @@ + diff --git a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs index 931c1b5268..d40e08e748 100644 --- a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Controls; +using Avalonia.Controls.UnitTests; using Avalonia.Input; using Xunit; @@ -10,6 +11,8 @@ namespace Avalonia.Interactivity.UnitTests { public class GestureTests { + private MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] public void Tapped_Should_Follow_Pointer_Pressed_Released() { @@ -27,8 +30,7 @@ namespace Avalonia.Interactivity.UnitTests border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - border.RaiseEvent(new PointerPressedEventArgs()); - border.RaiseEvent(new PointerReleasedEventArgs()); + _mouse.Click(border); Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result); } @@ -47,8 +49,7 @@ namespace Avalonia.Interactivity.UnitTests decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - border.RaiseEvent(new PointerPressedEventArgs()); - border.RaiseEvent(new PointerReleasedEventArgs()); + _mouse.Click(border); Assert.Equal(new[] { "bt", "dt" }, result); } @@ -72,9 +73,8 @@ namespace Avalonia.Interactivity.UnitTests border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); - border.RaiseEvent(new PointerPressedEventArgs()); - border.RaiseEvent(new PointerReleasedEventArgs()); - border.RaiseEvent(new PointerPressedEventArgs { ClickCount = 2 }); + _mouse.Click(border); + _mouse.Down(border, clickCount: 2); Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 795aba153a..3511919e39 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -137,8 +137,9 @@ namespace Avalonia.Markup.Xaml.UnitTests .GetVisualChildren().First() .GetVisualChildren().First() .GetVisualChildren().First(); - ((Control)item).RaiseEvent(new PointerPressedEventArgs {ClickCount = 20}); - Assert.Equal(20, w.Args.ClickCount); + + ((Control)item).DataContext = "test"; + Assert.Equal("test", w.SavedContext); } } @@ -161,10 +162,10 @@ namespace Avalonia.Markup.Xaml.UnitTests public class XamlIlBugTestsEventHandlerCodeBehind : Window { - public PointerPressedEventArgs Args; - public void HandlePointerPressed(object sender, PointerPressedEventArgs args) + public object SavedContext; + public void HandleDataContextChanged(object sender, EventArgs args) { - Args = args; + SavedContext = ((Control)sender).DataContext; } public XamlIlBugTestsEventHandlerCodeBehind() @@ -178,7 +179,7 @@ namespace Avalonia.Markup.Xaml.UnitTests -