From 2bdb914dd1cd58d5aca75bb08d6e4ea96b29692b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jul 2020 12:20:46 +0200 Subject: [PATCH 01/79] Show overlay layer when popup StaysOpen = false. We need to stop events getting to parent window. Pointer capture won't work because this now needs an input event in order to take pointer capture, and filtering events on the parent window causes tooltips to continue tooltips to get stuck. Best solution would seem to be using an overlay layer. --- .../Primitives/LightDismissOverlayLayer.cs | 10 ++++++++ src/Avalonia.Controls/Primitives/Popup.cs | 21 +++++++++++++--- .../Primitives/VisualLayerManager.cs | 25 ++++++++++++++++++- 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs diff --git a/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs new file mode 100644 index 0000000000..65a21a563a --- /dev/null +++ b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs @@ -0,0 +1,10 @@ +using System.Linq; +using Avalonia.Rendering; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + public class LightDismissOverlayLayer : Border + { + } +} diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 1fcf8d61bc..01ae6fbf43 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -369,8 +369,6 @@ namespace Avalonia.Controls.Primitives } } - DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel)); - DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick)); var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state => @@ -384,6 +382,23 @@ namespace Avalonia.Controls.Primitives state.popupHost.Dispose(); }); + if (!StaysOpen) + { + var layerManager = placementTarget.FindAncestorOfType(); + var dismissLayer = layerManager?.LightDismissOverlayLayer; + + if (dismissLayer != null) + { + dismissLayer.IsVisible = true; + DeferCleanup(Disposable.Create(() => dismissLayer.IsVisible = false)); + DeferCleanup(SubscribeToEventHandler>( + dismissLayer, + PointerPressedDismissOverlay, + (x, handler) => x.PointerPressed += handler, + (x, handler) => x.PointerPressed -= handler)); + } + } + _openState = new PopupOpenState(topLevel, popupHost, cleanupPopup); WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint); @@ -504,7 +519,7 @@ namespace Avalonia.Controls.Primitives } } - private void PointerPressedOutside(object sender, PointerPressedEventArgs e) + private void PointerPressedDismissOverlay(object sender, PointerPressedEventArgs e) { if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v)) { diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs index 3084d7fa72..a4e230e2f4 100644 --- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Avalonia.LogicalTree; +using Avalonia.Media; namespace Avalonia.Controls.Primitives { @@ -7,7 +8,8 @@ namespace Avalonia.Controls.Primitives { private const int AdornerZIndex = int.MaxValue - 100; private const int ChromeZIndex = int.MaxValue - 99; - private const int OverlayZIndex = int.MaxValue - 98; + private const int LightDismissOverlayZIndex = int.MaxValue - 98; + private const int OverlayZIndex = int.MaxValue - 97; private ILogicalRoot _logicalRoot; private readonly List _layers = new List(); @@ -50,6 +52,27 @@ namespace Avalonia.Controls.Primitives } } + public LightDismissOverlayLayer LightDismissOverlayLayer + { + get + { + if (IsPopup) + return null; + var rv = FindLayer(); + if (rv == null) + { + rv = new LightDismissOverlayLayer + { + Background = Brushes.Transparent, + IsVisible = false + }; + + AddLayer(rv, LightDismissOverlayZIndex); + } + return rv; + } + } + T FindLayer() where T : class { foreach (var layer in _layers) From 529b279549f6570745565b0c2b3d4eabf4dad5e8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jul 2020 22:02:37 +0200 Subject: [PATCH 02/79] Fix failing popup tests. --- .../Primitives/LightDismissOverlayLayer.cs | 33 ++++++++++++++++++- src/Avalonia.Controls/Primitives/Popup.cs | 3 +- .../Primitives/PopupTests.cs | 3 +- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs index 65a21a563a..ede9a9b635 100644 --- a/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs @@ -1,10 +1,41 @@ +using System; using System.Linq; -using Avalonia.Rendering; +using Avalonia.Controls.Templates; +using Avalonia.Styling; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls.Primitives { + /// + /// A layer that is used to dismiss a when the user clicks outside. + /// public class LightDismissOverlayLayer : Border { + /// + /// Returns the light dismiss overlay for a specified visual. + /// + /// The visual. + /// The light dismiss overlay, or null if none found. + public static LightDismissOverlayLayer? GetLightDismissOverlayLayer(IVisual visual) + { + visual = visual ?? throw new ArgumentNullException(nameof(visual)); + + VisualLayerManager? manager; + + if (visual is TopLevel topLevel) + { + manager = topLevel.GetTemplateChildren() + .OfType() + .FirstOrDefault(); + } + else + { + manager = visual.FindAncestorOfType(); + } + + return manager?.LightDismissOverlayLayer; + } } } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 01ae6fbf43..00bb026d0f 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -384,8 +384,7 @@ namespace Avalonia.Controls.Primitives if (!StaysOpen) { - var layerManager = placementTarget.FindAncestorOfType(); - var dismissLayer = layerManager?.LightDismissOverlayLayer; + var dismissLayer = LightDismissOverlayLayer.GetLightDismissOverlayLayer(placementTarget); if (dismissLayer != null) { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index fd06ba295b..5519c11582 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -392,7 +392,8 @@ namespace Avalonia.Controls.UnitTests.Primitives ++raised; }; - window.RaiseEvent(press); + var lightDismissLayer = window.FindDescendantOfType().LightDismissOverlayLayer; + lightDismissLayer.RaiseEvent(press); Assert.Equal(1, raised); } From b2a7339b4bc6246467b3b55f0eb5ff98e23604dd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jul 2020 00:10:18 +0200 Subject: [PATCH 03/79] Added Popup.IsLightDismissEnabled property. And deprecate `StaysOpen` in favor of this property. --- src/Avalonia.Controls/ContextMenu.cs | 2 +- src/Avalonia.Controls/Primitives/Popup.cs | 41 ++++++++++++++----- .../Primitives/PopupClosedEventArgs.cs | 2 +- .../AutoCompleteBox.xaml | 2 +- src/Avalonia.Themes.Default/ComboBox.xaml | 2 +- src/Avalonia.Themes.Default/MenuItem.xaml | 4 +- .../AutoCompleteBox.xaml | 2 +- .../CalendarDatePicker.xaml | 2 +- src/Avalonia.Themes.Fluent/ComboBox.xaml | 2 +- src/Avalonia.Themes.Fluent/DatePicker.xaml | 2 +- src/Avalonia.Themes.Fluent/MenuItem.xaml | 5 +-- src/Avalonia.Themes.Fluent/TimePicker.xaml | 2 +- .../Primitives/PopupTests.cs | 6 +-- 13 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 5929dd39d4..7720011d5e 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -265,7 +265,7 @@ namespace Avalonia.Controls PlacementMode = PlacementMode, PlacementRect = PlacementRect, PlacementTarget = PlacementTarget ?? control, - StaysOpen = false + IsLightDismissEnabled = true, }; _popup.Opened += PopupOpened; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 00bb026d0f..7272a565d9 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -92,6 +92,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty HorizontalOffsetProperty = AvaloniaProperty.Register(nameof(HorizontalOffset)); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsLightDismissEnabledProperty = + AvaloniaProperty.Register(nameof(IsLightDismissEnabled)); + /// /// Defines the property. /// @@ -101,8 +107,13 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty StaysOpenProperty = - AvaloniaProperty.Register(nameof(StaysOpen), true); + [Obsolete("Use IsLightDismissEnabledProperty")] + public static readonly DirectProperty StaysOpenProperty = + AvaloniaProperty.RegisterDirect( + nameof(StaysOpen), + o => o.StaysOpen, + (o, v) => o.StaysOpen = v, + true); /// /// Defines the property. @@ -165,6 +176,15 @@ namespace Avalonia.Controls.Primitives set; } + /// + /// Gets or sets a value that determines how the can be dismissed. + /// + public bool IsLightDismissEnabled + { + get => GetValue(IsLightDismissEnabledProperty); + set => SetValue(IsLightDismissEnabledProperty, value); + } + /// /// Gets or sets a value indicating whether the popup is currently open. /// @@ -268,10 +288,11 @@ namespace Avalonia.Controls.Primitives /// Gets or sets a value indicating whether the popup should stay open when the popup is /// pressed or loses focus. /// + [Obsolete("Use IsLightDismissEnabled")] public bool StaysOpen { - get { return GetValue(StaysOpenProperty); } - set { SetValue(StaysOpenProperty, value); } + get => !IsLightDismissEnabled; + set => IsLightDismissEnabled = !value; } /// @@ -382,7 +403,7 @@ namespace Avalonia.Controls.Primitives state.popupHost.Dispose(); }); - if (!StaysOpen) + if (IsLightDismissEnabled) { var dismissLayer = LightDismissOverlayLayer.GetLightDismissOverlayLayer(placementTarget); @@ -512,7 +533,7 @@ namespace Avalonia.Controls.Primitives { var mouse = e as RawPointerEventArgs; - if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) + if (IsLightDismissEnabled && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) { CloseCore(e); } @@ -520,7 +541,7 @@ namespace Avalonia.Controls.Primitives private void PointerPressedDismissOverlay(object sender, PointerPressedEventArgs e) { - if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v)) + if (IsLightDismissEnabled && e.Source is IVisual v && !IsChildOrThis(v)) { CloseCore(e); } @@ -616,7 +637,7 @@ namespace Avalonia.Controls.Primitives private void WindowDeactivated(object sender, EventArgs e) { - if (!StaysOpen) + if (IsLightDismissEnabled) { Close(); } @@ -624,7 +645,7 @@ namespace Avalonia.Controls.Primitives private void ParentClosed(object sender, EventArgs e) { - if (!StaysOpen) + if (IsLightDismissEnabled) { Close(); } @@ -632,7 +653,7 @@ namespace Avalonia.Controls.Primitives private void WindowLostFocus() { - if(!StaysOpen) + if(IsLightDismissEnabled) Close(); } diff --git a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs index c51543438c..db554b3c82 100644 --- a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs +++ b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.Primitives /// Gets the event that closed the popup, if any. /// /// - /// If is false, then this property will hold details of the + /// If is true, then this property will hold details of the /// interaction that caused the popup to close if the close was caused by e.g. a pointer press /// outside the popup. It can be used to mark the event as handled if the event should not /// be propagated. diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml index 788b60892b..66d0f17ede 100644 --- a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -19,7 +19,7 @@ MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" MaxHeight="{TemplateBinding MaxDropDownHeight}" PlacementTarget="{TemplateBinding}" - StaysOpen="False"> + IsLightDismissEnabled="True"> + IsLightDismissEnabled="True"> diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index d7f367c591..0eb87166b2 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -59,7 +59,6 @@ Grid.Column="4"/> + IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"> diff --git a/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml index 0d5d733cd9..6d83674c43 100644 --- a/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml @@ -47,7 +47,7 @@ WindowManagerAddShadowHint="False" MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" MaxHeight="{TemplateBinding MaxDropDownHeight}" - StaysOpen="False" + IsLightDismissEnabled="True" PlacementTarget="{TemplateBinding}"> + IsLightDismissEnabled="True"> diff --git a/src/Avalonia.Themes.Fluent/ComboBox.xaml b/src/Avalonia.Themes.Fluent/ComboBox.xaml index 2788344842..2d43a2afe5 100644 --- a/src/Avalonia.Themes.Fluent/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/ComboBox.xaml @@ -123,7 +123,7 @@ MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" MaxHeight="{TemplateBinding MaxDropDownHeight}" PlacementTarget="{TemplateBinding}" - StaysOpen="False"> + IsLightDismissEnabled="True"> diff --git a/src/Avalonia.Themes.Fluent/MenuItem.xaml b/src/Avalonia.Themes.Fluent/MenuItem.xaml index fbb994e90c..f861c456e9 100644 --- a/src/Avalonia.Themes.Fluent/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/MenuItem.xaml @@ -108,7 +108,6 @@ + IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"> diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 5519c11582..1d5b39cc14 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -349,7 +349,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void StaysOpen_False_Should_Not_Handle_Closing_Click() + public void LightDismiss_Should_Not_Handle_Closing_Click() { using (CreateServices()) { @@ -357,7 +357,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var target = new Popup() { PlacementTarget = window , - StaysOpen = false, + IsLightDismissEnabled = true, }; target.Open(); @@ -378,7 +378,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var target = new Popup() { PlacementTarget = window, - StaysOpen = false, + IsLightDismissEnabled = true, }; target.Open(); From c45436dff124117a01b591262e9b80441a5b1a91 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jul 2020 09:49:59 +0200 Subject: [PATCH 04/79] Added Popup.OverlayDismissEventPassThrough. To control whether dismiss events are passed through from the overlay layer to the underlying window content. --- src/Avalonia.Controls/AutoCompleteBox.cs | 7 +-- .../Calendar/CalendarDatePicker.cs | 7 +-- src/Avalonia.Controls/ComboBox.cs | 22 +------ src/Avalonia.Controls/ContextMenu.cs | 1 + src/Avalonia.Controls/Primitives/Popup.cs | 59 ++++++++++++++++--- .../Primitives/PopupClosedEventArgs.cs | 33 ----------- src/Avalonia.Input/InputExtensions.cs | 29 ++++++++- .../Primitives/PopupTests.cs | 49 ++++++--------- .../MockWindowingPlatform.cs | 4 ++ 9 files changed, 103 insertions(+), 108 deletions(-) delete mode 100644 src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 31101dc0f1..c164f282e8 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1647,7 +1647,7 @@ namespace Avalonia.Controls /// /// The source object. /// The event data. - private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e) + private void DropDownPopup_Closed(object sender, EventArgs e) { // Force the drop down dependency property to be false. if (IsDropDownOpen) @@ -1655,11 +1655,6 @@ namespace Avalonia.Controls IsDropDownOpen = false; } - if (e.CloseEvent is PointerEventArgs pointerEvent) - { - pointerEvent.Handled = true; - } - // Fire the DropDownClosed event if (_popupHasOpened) { diff --git a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs index b987f065be..046b55d49a 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs @@ -889,17 +889,12 @@ namespace Avalonia.Controls _ignoreButtonClick = false; } } - private void PopUp_Closed(object sender, PopupClosedEventArgs e) + private void PopUp_Closed(object sender, EventArgs e) { IsDropDownOpen = false; if(!_isPopupClosing) { - if (e.CloseEvent is PointerEventArgs pointerEvent) - { - pointerEvent.Handled = true; - } - _isPopupClosing = true; Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false); } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 75a0e41e7b..27313b0b4c 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -290,24 +290,6 @@ namespace Avalonia.Controls _popup = e.NameScope.Get("PART_Popup"); _popup.Opened += PopupOpened; - _popup.Closed += PopupClosed; - } - - /// - /// Called when the ComboBox popup is closed, with the - /// that caused the popup to close. - /// - /// The event args. - /// - /// This method can be overridden to control whether the event that caused the popup to close - /// is swallowed or passed through. - /// - protected virtual void PopupClosedOverride(PopupClosedEventArgs e) - { - if (e.CloseEvent is PointerEventArgs pointerEvent) - { - pointerEvent.Handled = true; - } } internal void ItemFocused(ComboBoxItem dropDownItem) @@ -318,13 +300,11 @@ namespace Avalonia.Controls } } - private void PopupClosed(object sender, PopupClosedEventArgs e) + private void PopupClosed(object sender, EventArgs e) { _subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen = null; - PopupClosedOverride(e); - if (CanFocus(this)) { Focus(); diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 7720011d5e..b4e4dd5071 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -266,6 +266,7 @@ namespace Avalonia.Controls PlacementRect = PlacementRect, PlacementTarget = PlacementTarget ?? control, IsLightDismissEnabled = true, + OverlayDismissEventPassThrough = true, }; _popup.Opened += PopupOpened; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 7272a565d9..c97d90baed 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -1,12 +1,10 @@ using System; -using System.Diagnostics; using System.Linq; using System.Reactive.Disposables; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Platform; @@ -86,6 +84,9 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(ObeyScreenEdges), true); #pragma warning restore 618 + public static readonly StyledProperty OverlayDismissEventPassThroughProperty = + AvaloniaProperty.Register(nameof(OverlayDismissEventPassThrough)); + /// /// Defines the property. /// @@ -138,7 +139,7 @@ namespace Avalonia.Controls.Primitives /// /// Raised when the popup closes. /// - public event EventHandler? Closed; + public event EventHandler? Closed; /// /// Raised when the popup opens. @@ -179,6 +180,9 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets a value that determines how the can be dismissed. /// + /// + /// Light dismiss is when the user taps on any area other than the popup. + /// public bool IsLightDismissEnabled { get => GetValue(IsLightDismissEnabledProperty); @@ -266,6 +270,22 @@ namespace Avalonia.Controls.Primitives set => SetValue(ObeyScreenEdgesProperty, value); } + /// + /// Gets or sets a value indicating whether the event that closes the popup is passed + /// through to the parent window. + /// + /// + /// When is set to true, clicks outside the the popup + /// cause the popup to close. When is set to + /// false, these clicks will be handled by the popup and not be registered by the parent + /// window. When set to true, the events will be passed through to the parent window. + /// + public bool OverlayDismissEventPassThrough + { + get => GetValue(OverlayDismissEventPassThroughProperty); + set => SetValue(OverlayDismissEventPassThroughProperty, value); + } + /// /// Gets or sets the Horizontal offset of the popup in relation to the . /// @@ -384,7 +404,7 @@ namespace Avalonia.Controls.Primitives if (parentPopupRoot?.Parent is Popup popup) { - DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed, + DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed, (x, handler) => x.Closed += handler, (x, handler) => x.Closed -= handler)); } @@ -436,7 +456,7 @@ namespace Avalonia.Controls.Primitives /// /// Closes the popup. /// - public void Close() => CloseCore(null); + public void Close() => CloseCore(); /// /// Measures the control. @@ -506,7 +526,7 @@ namespace Avalonia.Controls.Primitives } } - private void CloseCore(EventArgs? closeEvent) + private void CloseCore() { if (_openState is null) { @@ -526,7 +546,7 @@ namespace Avalonia.Controls.Primitives IsOpen = false; } - Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent)); + Closed?.Invoke(this, EventArgs.Empty); } private void ListenForNonClientClick(RawInputEventArgs e) @@ -535,7 +555,7 @@ namespace Avalonia.Controls.Primitives if (IsLightDismissEnabled && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) { - CloseCore(e); + CloseCore(); } } @@ -543,7 +563,28 @@ namespace Avalonia.Controls.Primitives { if (IsLightDismissEnabled && e.Source is IVisual v && !IsChildOrThis(v)) { - CloseCore(e); + CloseCore(); + + if (OverlayDismissEventPassThrough) + { + PassThroughEvent(e); + } + } + } + + private void PassThroughEvent(PointerPressedEventArgs e) + { + if (e.Source is LightDismissOverlayLayer layer && + layer.GetVisualRoot() is IInputElement root) + { + var p = e.GetCurrentPoint(root); + var hit = root.InputHitTest(p.Position, x => x != layer); + + if (hit != null) + { + hit.RaiseEvent(e); + e.Handled = true; + } } } diff --git a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs deleted file mode 100644 index db554b3c82..0000000000 --- a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Avalonia.Interactivity; - -#nullable enable - -namespace Avalonia.Controls.Primitives -{ - /// - /// Holds data for the event. - /// - public class PopupClosedEventArgs : EventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// - public PopupClosedEventArgs(EventArgs? closeEvent) - { - CloseEvent = closeEvent; - } - - /// - /// Gets the event that closed the popup, if any. - /// - /// - /// If is true, then this property will hold details of the - /// interaction that caused the popup to close if the close was caused by e.g. a pointer press - /// outside the popup. It can be used to mark the event as handled if the event should not - /// be propagated. - /// - public EventArgs? CloseEvent { get; } - } -} diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index 4babe711f2..cbe36583e6 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Input { /// @@ -22,7 +24,7 @@ namespace Avalonia.Input /// public static IEnumerable GetInputElementsAt(this IInputElement element, Point p) { - Contract.Requires(element != null); + element = element ?? throw new ArgumentNullException(nameof(element)); return element.GetVisualsAt(p, s_hitTestDelegate).Cast(); } @@ -33,13 +35,34 @@ namespace Avalonia.Input /// The element to test. /// The point on . /// The topmost at the specified position. - public static IInputElement InputHitTest(this IInputElement element, Point p) + public static IInputElement? InputHitTest(this IInputElement element, Point p) { - Contract.Requires(element != null); + element = element ?? throw new ArgumentNullException(nameof(element)); return element.GetVisualAt(p, s_hitTestDelegate) as IInputElement; } + /// + /// Returns the topmost active input element at a point on an . + /// + /// The element to test. + /// The point on . + /// + /// A filter predicate. If the predicate returns false then the visual and all its + /// children will be excluded from the results. + /// + /// The topmost at the specified position. + public static IInputElement? InputHitTest( + this IInputElement element, + Point p, + Func filter) + { + element = element ?? throw new ArgumentNullException(nameof(element)); + filter = filter ?? throw new ArgumentNullException(nameof(filter)); + + return element.GetVisualAt(p, x => s_hitTestDelegate(x) && filter(x)) as IInputElement; + } + private static bool IsHitTestVisible(IVisual visual) { var element = visual as IInputElement; diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 1d5b39cc14..422ade2f90 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -349,53 +349,42 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void LightDismiss_Should_Not_Handle_Closing_Click() + public void OverlayDismissEventPassThrough_Should_Pass_Event_To_Window_Contents() { using (CreateServices()) { var window = PreparedWindow(); + var rendererMock = Mock.Get(window.Renderer); var target = new Popup() { PlacementTarget = window , IsLightDismissEnabled = true, + OverlayDismissEventPassThrough = true, }; - target.Open(); - - var e = CreatePointerPressedEventArgs(window); - window.RaiseEvent(e); + var raised = 0; + var border = new Border(); + window.Content = border; - Assert.False(e.Handled); - } - } + rendererMock.Setup(x => + x.HitTestFirst(new Point(10, 15), window, It.IsAny>())) + .Returns(border); - [Fact] - public void Should_Pass_Closing_Click_To_Closed_Event() - { - using (CreateServices()) - { - var window = PreparedWindow(); - var target = new Popup() + border.PointerPressed += (s, e) => { - PlacementTarget = window, - IsLightDismissEnabled = true, + Assert.Same(border, e.Source); + ++raised; }; target.Open(); + Assert.True(target.IsOpen); - var press = CreatePointerPressedEventArgs(window); - var raised = 0; - - target.Closed += (s, e) => - { - Assert.Same(press, e.CloseEvent); - ++raised; - }; - - var lightDismissLayer = window.FindDescendantOfType().LightDismissOverlayLayer; - lightDismissLayer.RaiseEvent(press); + var e = CreatePointerPressedEventArgs(window, new Point(10, 15)); + var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window); + overlay.RaiseEvent(e); Assert.Equal(1, raised); + Assert.False(target.IsOpen); } } @@ -411,14 +400,14 @@ namespace Avalonia.Controls.UnitTests.Primitives }))); } - private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source) + private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source, Point p) { var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); return new PointerPressedEventArgs( source, pointer, source, - default, + p, 0, new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), KeyModifiers.None); diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 48a333dc54..9b77fbb009 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Moq; using Avalonia.Platform; +using Avalonia.Rendering; namespace Avalonia.UnitTests { @@ -39,6 +40,9 @@ namespace Avalonia.UnitTests return CreatePopupMock(windowImpl.Object).Object; }); + windowImpl.Setup(x => x.CreateRenderer(It.IsAny())) + .Returns(Mock.Of()); + windowImpl.Setup(x => x.Dispose()).Callback(() => { windowImpl.Object.Closed?.Invoke(); From 714b0740db18387d055886360c37b9bf40fcf6dd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jul 2020 13:14:22 +0200 Subject: [PATCH 05/79] Make leak tests pass again. --- .../Primitives/PopupTests.cs | 13 ++++++++++--- tests/Avalonia.UnitTests/MockWindowingPlatform.cs | 3 --- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 422ade2f90..d9176ca55d 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -14,6 +14,7 @@ using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; using Avalonia.Input; +using Avalonia.Rendering; namespace Avalonia.Controls.UnitTests.Primitives { @@ -353,8 +354,14 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var window = PreparedWindow(); - var rendererMock = Mock.Get(window.Renderer); + var renderer = new Mock(); + var platform = AvaloniaLocator.Current.GetService(); + var windowImpl = Mock.Get(platform.CreateWindow()); + windowImpl.Setup(x => x.CreateRenderer(It.IsAny())).Returns(renderer.Object); + + var window = new Window(windowImpl.Object); + window.ApplyTemplate(); + var target = new Popup() { PlacementTarget = window , @@ -366,7 +373,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var border = new Border(); window.Content = border; - rendererMock.Setup(x => + renderer.Setup(x => x.HitTestFirst(new Point(10, 15), window, It.IsAny>())) .Returns(border); diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 9b77fbb009..72f8ab9fd0 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -40,9 +40,6 @@ namespace Avalonia.UnitTests return CreatePopupMock(windowImpl.Object).Object; }); - windowImpl.Setup(x => x.CreateRenderer(It.IsAny())) - .Returns(Mock.Of()); - windowImpl.Setup(x => x.Dispose()).Callback(() => { windowImpl.Object.Closed?.Invoke(); From dd563805cde3e864c08a89b0c04a8dc54cf53238 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jul 2020 13:24:41 +0200 Subject: [PATCH 06/79] Nullable enable menu-related classes. --- src/Avalonia.Controls/IMenuElement.cs | 4 +++- src/Avalonia.Controls/IMenuItem.cs | 6 ++++-- src/Avalonia.Controls/Menu.cs | 2 ++ src/Avalonia.Controls/MenuBase.cs | 8 ++++---- src/Avalonia.Controls/MenuItem.cs | 20 ++++++++++--------- .../Platform/DefaultMenuInteractionHandler.cs | 7 ++++--- 6 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/IMenuElement.cs b/src/Avalonia.Controls/IMenuElement.cs index ee9d0fd6b6..426f265084 100644 --- a/src/Avalonia.Controls/IMenuElement.cs +++ b/src/Avalonia.Controls/IMenuElement.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using Avalonia.Input; +#nullable enable + namespace Avalonia.Controls { /// @@ -11,7 +13,7 @@ namespace Avalonia.Controls /// /// Gets or sets the currently selected submenu item. /// - IMenuItem SelectedItem { get; set; } + IMenuItem? SelectedItem { get; set; } /// /// Gets the submenu items. diff --git a/src/Avalonia.Controls/IMenuItem.cs b/src/Avalonia.Controls/IMenuItem.cs index 132d565cb7..94d761f725 100644 --- a/src/Avalonia.Controls/IMenuItem.cs +++ b/src/Avalonia.Controls/IMenuItem.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Controls +#nullable enable + +namespace Avalonia.Controls { /// /// Represents a . @@ -29,7 +31,7 @@ /// /// Gets the parent . /// - new IMenuElement Parent { get; } + new IMenuElement? Parent { get; } /// /// Raises a click event on the menu item. diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 7205af0e75..3733137714 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -4,6 +4,8 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; +#nullable enable + namespace Avalonia.Controls { /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index 4554cb2bcf..0434928280 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -8,6 +8,8 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; +#nullable enable + namespace Avalonia.Controls { /// @@ -51,9 +53,7 @@ namespace Avalonia.Controls /// The menu interaction handler. public MenuBase(IMenuInteractionHandler interactionHandler) { - Contract.Requires(interactionHandler != null); - - InteractionHandler = interactionHandler; + InteractionHandler = interactionHandler ?? throw new ArgumentNullException(nameof(interactionHandler)); } /// @@ -77,7 +77,7 @@ namespace Avalonia.Controls IMenuInteractionHandler IMenu.InteractionHandler => InteractionHandler; /// - IMenuItem IMenuElement.SelectedItem + IMenuItem? IMenuElement.SelectedItem { get { diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 912abc6de3..aab099911b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -12,6 +12,8 @@ using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls { /// @@ -22,7 +24,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = + public static readonly DirectProperty CommandProperty = Button.CommandProperty.AddOwner( menuItem => menuItem.Command, (menuItem, command) => menuItem.Command = command, @@ -94,10 +96,10 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); - private ICommand _command; + private ICommand? _command; private bool _commandCanExecute = true; - private Popup _popup; - private IDisposable _gridHack; + private Popup? _popup; + private IDisposable? _gridHack; /// /// Initializes static members of the class. @@ -166,7 +168,7 @@ namespace Avalonia.Controls /// /// Gets or sets the command associated with the menu item. /// - public ICommand Command + public ICommand? Command { get { return _command; } set { SetAndRaise(CommandProperty, ref _command, value); } @@ -246,7 +248,7 @@ namespace Avalonia.Controls bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false; /// - IMenuElement IMenuItem.Parent => Parent as IMenuElement; + IMenuElement? IMenuItem.Parent => Parent as IMenuElement; protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; @@ -254,7 +256,7 @@ namespace Avalonia.Controls bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); /// - IMenuItem IMenuElement.SelectedItem + IMenuItem? IMenuElement.SelectedItem { get { @@ -551,7 +553,7 @@ namespace Avalonia.Controls /// The property change event. private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e) { - if ((bool)e.NewValue) + if ((bool)e.NewValue!) { Focus(); } @@ -563,7 +565,7 @@ namespace Avalonia.Controls /// The property change event. private void SubMenuOpenChanged(AvaloniaPropertyChangedEventArgs e) { - var value = (bool)e.NewValue; + var value = (bool)e.NewValue!; if (value) { diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 2a7ea12d79..be92b89b73 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -3,7 +3,6 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.VisualTree; @@ -235,7 +234,9 @@ namespace Avalonia.Controls.Platform // If the the parent is an IMenu which successfully moved its selection, // and the current menu is open then close the current menu and open the // new menu. - if (item.IsSubMenuOpen && item.Parent is IMenu) + if (item.IsSubMenuOpen && + item.Parent is IMenu && + item.Parent.SelectedItem is object) { item.Close(); Open(item.Parent.SelectedItem, true); @@ -385,7 +386,7 @@ namespace Avalonia.Controls.Platform { if (e.Source == Menu) { - Menu.MoveSelection(NavigationDirection.First, true); + Menu?.MoveSelection(NavigationDirection.First, true); } } From 226c57b7d792b9ab1de96afd1ebec71d4a9a086f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jul 2020 13:25:30 +0200 Subject: [PATCH 07/79] Ensure that Open is called on main menu. --- .../Platform/DefaultMenuInteractionHandler.cs | 5 +++++ .../DefaultMenuInteractionHandlerTests.cs | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index be92b89b73..6d6398bcda 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -364,6 +364,11 @@ namespace Avalonia.Controls.Platform } else { + if (item.IsTopLevel && item.Parent is IMainMenu mainMenu) + { + mainMenu.Open(); + } + Open(item, false); } diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index 97b6bf9d24..64f35049ce 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -124,6 +124,24 @@ namespace Avalonia.Controls.UnitTests.Platform Assert.True(e.Handled); } + [Fact] + public void Click_On_TopLevel_Calls_MainMenu_Open() + { + var target = new DefaultMenuInteractionHandler(false); + var menu = new Mock(); + menu.As(); + + var item = Mock.Of(x => + x.IsTopLevel == true && + x.HasSubMenu == true && + x.Parent == menu.Object); + + var e = CreatePressed(item); + + target.PointerPressed(item, e); + menu.Verify(x => x.Open()); + } + [Fact] public void Click_On_Open_TopLevel_Menu_Closes_Menu() { From f0eafa70d0d7e8b066cf01cccfe5c6c85774ec20 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jul 2020 20:27:42 +0200 Subject: [PATCH 08/79] Added LightDismissOverlayLayer.InputPassThroughElement. Which implements something similar to UWP's `FlyoutBase.OverlayInputPassThroughElement`. With this feature, `Menu`/`MenuItem` can use `IsLightDismissEnabled="True"` fixing #3965 for top-level menus. --- src/Avalonia.Controls/Menu.cs | 17 ++++++++++++++ .../Primitives/LightDismissOverlayLayer.cs | 22 ++++++++++++++++++- src/Avalonia.Themes.Default/MenuItem.xaml | 2 ++ src/Avalonia.Themes.Fluent/MenuItem.xaml | 5 +++-- .../Rendering/ICustomSimpleHitTest.cs | 11 ++++++++++ .../Rendering/SceneGraph/Scene.cs | 6 +++++ 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 3733137714..f74daa791d 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -1,4 +1,5 @@ using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; @@ -16,6 +17,8 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); + private LightDismissOverlayLayer? _overlay; + /// /// Initializes a new instance of the class. /// @@ -45,6 +48,13 @@ namespace Avalonia.Controls return; } + if (_overlay?.InputPassThroughElement == this) + { + _overlay.InputPassThroughElement = null; + } + + _overlay = null; + foreach (var i in ((IMenu)this).SubItems) { i.Close(); @@ -68,6 +78,13 @@ namespace Avalonia.Controls return; } + _overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(this); + + if (_overlay is object) + { + _overlay.InputPassThroughElement = this; + } + IsOpen = true; RaiseEvent(new RoutedEventArgs diff --git a/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs index ede9a9b635..752eedb68a 100644 --- a/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs @@ -1,6 +1,8 @@ using System; using System.Linq; using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.VisualTree; @@ -11,8 +13,10 @@ namespace Avalonia.Controls.Primitives /// /// A layer that is used to dismiss a when the user clicks outside. /// - public class LightDismissOverlayLayer : Border + public class LightDismissOverlayLayer : Border, ICustomHitTest { + public IInputElement? InputPassThroughElement { get; set; } + /// /// Returns the light dismiss overlay for a specified visual. /// @@ -37,5 +41,21 @@ namespace Avalonia.Controls.Primitives return manager?.LightDismissOverlayLayer; } + + public bool HitTest(Point point) + { + if (InputPassThroughElement is object) + { + var p = point.Transform(this.TransformToVisual(VisualRoot)!.Value); + var hit = VisualRoot.GetVisualAt(p, x => x != this); + + if (hit is object) + { + return !InputPassThroughElement.IsVisualAncestorOf(hit); + } + } + + return true; + } } } diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 0eb87166b2..b1f536a704 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -59,6 +59,7 @@ Grid.Column="4"/> + IsLightDismissEnabled="True" + IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"> + /// Allows customization of hit-testing for all renderers. + /// + /// + /// Note that this interface can only used to make a portion of a control non-hittable, it + /// cannot expand the hittable area of a control. + /// + public interface ICustomHitTest : ICustomSimpleHitTest + { + } + public static class CustomSimpleHitTestExtensions { public static bool HitTestCustom(this IVisual visual, Point point) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index 0f6001516d..4f5c97cdff 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -304,6 +304,12 @@ namespace Avalonia.Rendering.SceneGraph clipped = !node.GeometryClip.FillContains(controlPoint.Value); } + if (!clipped && node.Visual is ICustomHitTest custom) + { + var controlPoint = _sceneRoot.Visual.TranslatePoint(_point, node.Visual); + clipped = !custom.HitTest(controlPoint.Value); + } + return !clipped; } From b43c8f4ceffeb1a86bcc6ecb13250715f6ff4f62 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jul 2020 17:59:30 +0200 Subject: [PATCH 09/79] Added `Popup.OverlayInputPassThroughElement`. --- src/Avalonia.Controls/Primitives/Popup.cs | 28 +++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index c97d90baed..71efd322ec 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -87,6 +87,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty OverlayDismissEventPassThroughProperty = AvaloniaProperty.Register(nameof(OverlayDismissEventPassThrough)); + public static readonly DirectProperty OverlayInputPassThroughElementProperty = + AvaloniaProperty.RegisterDirect( + nameof(OverlayInputPassThroughElement), + o => o.OverlayInputPassThroughElement, + (o, v) => o.OverlayInputPassThroughElement = v); + /// /// Defines the property. /// @@ -125,6 +131,7 @@ namespace Avalonia.Controls.Primitives private bool _isOpen; private bool _ignoreIsOpenChanged; private PopupOpenState? _openState; + private IInputElement _overlayInputPassThroughElement; /// /// Initializes static members of the class. @@ -286,6 +293,16 @@ namespace Avalonia.Controls.Primitives set => SetValue(OverlayDismissEventPassThroughProperty, value); } + /// + /// Gets or sets an element that should receive pointer input events even when underneath + /// the popup's overlay. + /// + public IInputElement OverlayInputPassThroughElement + { + get => _overlayInputPassThroughElement; + set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + } + /// /// Gets or sets the Horizontal offset of the popup in relation to the . /// @@ -430,7 +447,14 @@ namespace Avalonia.Controls.Primitives if (dismissLayer != null) { dismissLayer.IsVisible = true; - DeferCleanup(Disposable.Create(() => dismissLayer.IsVisible = false)); + dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement; + + DeferCleanup(Disposable.Create(() => + { + dismissLayer.IsVisible = false; + dismissLayer.InputPassThroughElement = null; + })); + DeferCleanup(SubscribeToEventHandler>( dismissLayer, PointerPressedDismissOverlay, @@ -694,7 +718,7 @@ namespace Avalonia.Controls.Primitives private void WindowLostFocus() { - if(IsLightDismissEnabled) + if (IsLightDismissEnabled) Close(); } From c37144b59633f8f52f917a77f5bceb7c0e678647 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 16 Jul 2020 13:03:26 +0300 Subject: [PATCH 10/79] add failing unit tests for #4306 --- .../ListBoxTests_Multiple.cs | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs new file mode 100644 index 0000000000..7c7cdd08db --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs @@ -0,0 +1,126 @@ +using System.Linq; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ListBoxTests_Multiple + { + [Fact] + public void Focusing_Item_With_Shift_And_Arrow_Key_Should_Add_To_Selection() + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + SelectionMode = SelectionMode.Multiple + }; + + ApplyTemplate(target); + + target.SelectedItem = "Foo"; + + target.Presenter.Panel.Children[1].RaiseEvent(new GotFocusEventArgs + { + RoutedEvent = InputElement.GotFocusEvent, + NavigationMethod = NavigationMethod.Directional, + KeyModifiers = KeyModifiers.Shift + }); + + Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + } + + [Fact] + public void Focusing_Item_With_Ctrl_And_Arrow_Key_Should_Add_To_Selection() + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + SelectionMode = SelectionMode.Multiple + }; + + ApplyTemplate(target); + + target.SelectedItem = "Foo"; + + target.Presenter.Panel.Children[1].RaiseEvent(new GotFocusEventArgs + { + RoutedEvent = InputElement.GotFocusEvent, + NavigationMethod = NavigationMethod.Directional, + KeyModifiers = KeyModifiers.Control + }); + + Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + } + + [Fact] + public void Focusing_Selected_Item_With_Ctrl_And_Arrow_Key_Should_Remove_From_Selection() + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + SelectionMode = SelectionMode.Multiple + }; + + ApplyTemplate(target); + + target.SelectedItems.Add("Foo"); + target.SelectedItems.Add("Bar"); + + target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs + { + RoutedEvent = InputElement.GotFocusEvent, + NavigationMethod = NavigationMethod.Directional, + KeyModifiers = KeyModifiers.Control + }); + + Assert.Equal(new[] { "Bar" }, target.SelectedItems); + } + + private Control CreateListBoxTemplate(ITemplatedControl parent, INameScope scope) + { + return new ScrollViewer + { + Template = new FuncControlTemplate(CreateScrollViewerTemplate), + Content = new ItemsPresenter + { + Name = "PART_ItemsPresenter", + [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), + }.RegisterInNameScope(scope) + }; + } + + private Control CreateScrollViewerTemplate(ITemplatedControl parent, INameScope scope) + { + return new ScrollContentPresenter + { + Name = "PART_ContentPresenter", + [~ContentPresenter.ContentProperty] = + parent.GetObservable(ContentControl.ContentProperty).ToBinding(), + }.RegisterInNameScope(scope); + } + + private void ApplyTemplate(ListBox target) + { + // Apply the template to the ListBox itself. + target.ApplyTemplate(); + + // Then to its inner ScrollViewer. + var scrollViewer = (ScrollViewer)target.GetVisualChildren().Single(); + scrollViewer.ApplyTemplate(); + + // Then make the ScrollViewer create its child. + ((ContentPresenter)scrollViewer.Presenter).UpdateChild(); + + // Now the ItemsPresenter should be reigstered, so apply its template. + target.Presenter.ApplyTemplate(); + } + } +} From c08d88afcff791837d28b0a1dd73105da0d1cc6e Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 16 Jul 2020 12:47:26 +0300 Subject: [PATCH 11/79] pass key modifiers to focus --- src/Avalonia.Controls/ItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 6e0ad66699..1aa7945901 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -295,7 +295,7 @@ namespace Avalonia.Controls if (next != null) { - focus.Focus(next, NavigationMethod.Directional); + focus.Focus(next, NavigationMethod.Directional, e.KeyModifiers); e.Handled = true; } From e5c373acd06489b0e715282158488712259723eb Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 16 Jul 2020 12:48:12 +0300 Subject: [PATCH 12/79] pass ctrl key state to selection logic in listbox --- src/Avalonia.Controls/ListBox.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 3c21cd2c38..a085bfb6bc 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -136,7 +136,8 @@ namespace Avalonia.Controls e.Handled = UpdateSelectionFromEventSource( e.Source, true, - (e.KeyModifiers & KeyModifiers.Shift) != 0); + (e.KeyModifiers & KeyModifiers.Shift) != 0, + (e.KeyModifiers & KeyModifiers.Control) != 0); } } From 5403fcf0cadc60b3791a095f8c5744d537a6638a Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 25 Jul 2020 14:55:04 +0800 Subject: [PATCH 13/79] Fix vertical progressbar --- src/Avalonia.Themes.Default/ProgressBar.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Default/ProgressBar.xaml b/src/Avalonia.Themes.Default/ProgressBar.xaml index 43a0752bc8..098fa26e24 100644 --- a/src/Avalonia.Themes.Default/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/ProgressBar.xaml @@ -77,11 +77,11 @@ Easing="LinearEasing"> + Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" /> + Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" /> From d2f50ea1e2c7ca773d99af7189a0dd60778de1d1 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 25 Jul 2020 15:02:57 +0800 Subject: [PATCH 14/79] fix offset --- src/Avalonia.Controls/ProgressBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index e904957429..a92f24a050 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -215,7 +215,7 @@ namespace Avalonia.Controls TemplateProperties.Container2AnimationEndPosition = barIndicatorWidth2 * 1.66; // Position at 166% // Remove these properties when we switch to fluent as default and removed the old one. - IndeterminateStartingOffset = -(dim / 5d); + IndeterminateStartingOffset = -dim; IndeterminateEndingOffset = dim; var padding = Padding; From cfba204bc628b1aff877323517674b808abcd6f0 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 25 Jul 2020 19:03:20 +0800 Subject: [PATCH 15/79] fix --- src/Avalonia.Themes.Default/ProgressBar.xaml | 46 ++++++++------------ 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Themes.Default/ProgressBar.xaml b/src/Avalonia.Themes.Default/ProgressBar.xaml index 098fa26e24..d9c407245b 100644 --- a/src/Avalonia.Themes.Default/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/ProgressBar.xaml @@ -13,19 +13,14 @@ - - + + + + + - - + + @@ -54,36 +49,31 @@ - - From f0529e1c739cc8cb6073af8ba0fe7cf9dd608096 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Sun, 26 Jul 2020 13:18:43 +0200 Subject: [PATCH 16/79] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 19a9a8420d..c1bf5bbfde 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg) +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg) From b04a1f78646099fa6f2a3dd4026ff2e7697d61cb Mon Sep 17 00:00:00 2001 From: Anton Mitsengendler Date: Sun, 26 Jul 2020 22:12:39 +0300 Subject: [PATCH 17/79] Changed resource name for a default placeholder foreground Added template binding for the placeholder foreground property to make it customizable from user code --- src/Avalonia.Themes.Default/ComboBox.xaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index 8ee818bad2..0fd6b144a4 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -25,7 +25,7 @@ - + Date: Mon, 27 Jul 2020 16:03:16 +0300 Subject: [PATCH 18/79] Enabled custom renderer factory for X11 and macOS --- src/Avalonia.Native/WindowImplBase.cs | 10 +++++++++- src/Avalonia.X11/X11Window.cs | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 42eecc36ea..4b13666edd 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -300,7 +300,15 @@ namespace Avalonia.Native public IRenderer CreateRenderer(IRenderRoot root) { if (_deferredRendering) - return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); + { + var loop = AvaloniaLocator.Current.GetService(); + var customRendererFactory = AvaloniaLocator.Current.GetService(); + + if (customRendererFactory != null) + return customRendererFactory.Create(root, loop); + return new DeferredRenderer(root, loop); + } + return new ImmediateRenderer(root); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index c24abcd230..0c0b942bcd 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -331,6 +331,11 @@ namespace Avalonia.X11 public IRenderer CreateRenderer(IRenderRoot root) { var loop = AvaloniaLocator.Current.GetService(); + var customRendererFactory = AvaloniaLocator.Current.GetService(); + + if (customRendererFactory != null) + return customRendererFactory.Create(root, loop); + return _platform.Options.UseDeferredRendering ? new DeferredRenderer(root, loop) : (IRenderer)new X11ImmediateRendererProxy(root, loop); From 8ac2e525ada5f667d77774dd5880efcbd7fc1bea Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 27 Jul 2020 16:03:40 +0300 Subject: [PATCH 19/79] Expose SKSurface from ISkiaDrawingContextImpl --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 8 +++++--- src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs | 2 +- src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderSession.cs | 2 +- src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs | 2 +- src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs | 2 +- src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs | 1 + src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 2 +- 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index d818e683c3..b93d0f8868 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -44,7 +44,7 @@ namespace Avalonia.Skia /// /// Canvas to draw to. /// - public SKCanvas Canvas; + public SKSurface Surface; /// /// Dpi of drawings. @@ -81,8 +81,8 @@ namespace Avalonia.Skia _grContext = createInfo.GrContext; if (_grContext != null) Monitor.Enter(_grContext); - - Canvas = createInfo.Canvas; + Surface = createInfo.Surface; + Canvas = createInfo.Surface.Canvas; if (Canvas == null) { @@ -102,8 +102,10 @@ namespace Avalonia.Skia /// Skia canvas. /// public SKCanvas Canvas { get; } + public SKSurface Surface { get; } SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas; + SKSurface ISkiaDrawingContextImpl.SkSurface => Surface; GRContext ISkiaDrawingContextImpl.GrContext => _grContext; /// diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 8b04676b09..8d35d27a81 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -52,7 +52,7 @@ namespace Avalonia.Skia var createInfo = new DrawingContextImpl.CreateInfo { - Canvas = canvas, + Surface = _framebufferSurface, Dpi = framebuffer.Dpi, VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderSession.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderSession.cs index c54d1bd859..a4e2bfed52 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderSession.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderSession.cs @@ -16,7 +16,7 @@ namespace Avalonia.Skia /// /// Canvas that will be used to render. /// - SKCanvas Canvas { get; } + SKSurface SkSurface { get; } /// /// Scaling factor. diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index e0b7019672..2bb739f372 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -49,7 +49,7 @@ namespace Avalonia.Skia } public GRContext GrContext { get; } - public SKCanvas Canvas => _surface.Canvas; + public SKSurface SkSurface => _surface; public double ScaleFactor => _glSession.Scaling; } diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 94e513b2fd..ef5da5eb08 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -27,7 +27,7 @@ namespace Avalonia.Skia var nfo = new DrawingContextImpl.CreateInfo { GrContext = session.GrContext, - Canvas = session.Canvas, + Surface = session.SkSurface, Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor, VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true diff --git a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs index a9b91384d7..38fa5a5253 100644 --- a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs @@ -7,5 +7,6 @@ namespace Avalonia.Skia { SKCanvas SkCanvas { get; } GRContext GrContext { get; } + SKSurface SkSurface { get; } } } diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 588f7bee6c..27b29c6e1e 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -70,7 +70,7 @@ namespace Avalonia.Skia var createInfo = new DrawingContextImpl.CreateInfo { - Canvas = _canvas, + Surface = _surface, Dpi = Dpi, VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = _disableLcdRendering, From 2bec4c14c5b77bf2b94f5943d311d18b78d2ce1b Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 27 Jul 2020 15:14:10 +0200 Subject: [PATCH 20/79] Fixes SKFontStyleSlant to FontStyle conversion --- src/Avalonia.Visuals/Media/FontStyle.cs | 8 ++++---- src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs | 3 ++- src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs | 11 +++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Visuals/Media/FontStyle.cs b/src/Avalonia.Visuals/Media/FontStyle.cs index cbc92b1a9f..b9d04bf9ff 100644 --- a/src/Avalonia.Visuals/Media/FontStyle.cs +++ b/src/Avalonia.Visuals/Media/FontStyle.cs @@ -11,13 +11,13 @@ namespace Avalonia.Media Normal, /// - /// An oblique font. + /// An italic font. /// - Oblique, + Italic, /// - /// An italic font. + /// An oblique font. /// - Italic, + Oblique } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index d36baf331d..7ca44e7282 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -56,7 +56,8 @@ namespace Avalonia.Skia continue; } - var key = new FontKey(fontFamily.Name, (FontStyle)typeface.FontSlant, (FontWeight)typeface.FontWeight); + var key = new FontKey(fontFamily.Name, typeface.FontSlant.ToAvalonia(), + (FontWeight)typeface.FontWeight); typeFaceCollection.AddTypeface(key, typeface); } diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index ec7e0a67ed..1e772ef067 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -138,6 +138,17 @@ namespace Avalonia.Skia } } + public static FontStyle ToAvalonia(this SKFontStyleSlant slant) + { + return slant switch + { + SKFontStyleSlant.Upright => FontStyle.Normal, + SKFontStyleSlant.Italic => FontStyle.Italic, + SKFontStyleSlant.Oblique => FontStyle.Oblique, + _ => throw new ArgumentOutOfRangeException(nameof (slant), slant, null) + }; + } + public static SKPath Clone(this SKPath src) { return src != null ? new SKPath(src) : null; From 54553b44d39d7e32d7980da4b7d9276bdbd69b94 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 27 Jul 2020 16:48:41 +0200 Subject: [PATCH 21/79] Make npm silent --- nukebuild/Build.cs | 4 +++- .../Remote/HtmlTransport/webapp/webpack.config.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index fe877dc49c..fbfbf47e1b 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -128,7 +128,9 @@ partial class Build : NukeBuild { var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp"; - NpmTasks.NpmInstall(c => c.SetWorkingDirectory(webappDir)); + NpmTasks.NpmInstall(c => c + .SetWorkingDirectory(webappDir) + .SetArgumentConfigurator(a => a.Add("--silent"))); NpmTasks.NpmRun(c => c .SetWorkingDirectory(webappDir) .SetCommand("dist")); diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js index 3057f269a5..3750351165 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js @@ -93,7 +93,7 @@ const config = { plugins: [ new Printer(), - new CleanWebpackPlugin([path.resolve(__dirname, 'build')]), + new CleanWebpackPlugin([path.resolve(__dirname, 'build')], { verbose: false }), new MiniCssExtractPlugin({ filename: "[name].[chunkhash]h" + ".css", From 8e1443384f5b8323178a5102e3fcdb1891008f5f Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 27 Jul 2020 17:25:33 +0200 Subject: [PATCH 22/79] Add app compat baseline --- src/Avalonia.Visuals/ApiCompatBaseline.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/Avalonia.Visuals/ApiCompatBaseline.txt diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt new file mode 100644 index 0000000000..00618448a2 --- /dev/null +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -0,0 +1,4 @@ +Compat issues with assembly Avalonia.Visuals: +EnumValuesMustMatch : Enum value 'Avalonia.Media.FontStyle Avalonia.Media.FontStyle.Italic' is (System.Int32)1 in the implementation but (System.Int32)2 in the contract. +EnumValuesMustMatch : Enum value 'Avalonia.Media.FontStyle Avalonia.Media.FontStyle.Oblique' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. +Total Issues: 2 From 385dd5227155342ed5c1fb142bcbfcce21312584 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Jul 2020 13:33:15 -0300 Subject: [PATCH 23/79] remove apicompat from backend impl checks. --- .../Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj | 3 +-- src/Skia/Avalonia.Skia/Avalonia.Skia.csproj | 3 +-- src/Windows/Avalonia.Win32/Avalonia.Win32.csproj | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj index 22599200bc..48095a4c25 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj +++ b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj @@ -7,6 +7,5 @@ - - + diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index ef955eb8be..ac029f1062 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -16,6 +16,5 @@ - - + diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index bcbdde322c..7fc24d4d3d 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -8,6 +8,5 @@ - - + From 307ab82292bf4eed46e390e7d77def7acc28cbcb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Jul 2020 13:37:01 -0300 Subject: [PATCH 24/79] remove opengl apicompat checks. --- src/Avalonia.OpenGL/Avalonia.OpenGL.csproj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj index 2e23f24deb..d761e60c07 100644 --- a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj +++ b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj @@ -9,7 +9,5 @@ - - - + From d759435c05a089086484d664752b5a35297e903d Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Mon, 27 Jul 2020 21:52:51 +0300 Subject: [PATCH 25/79] Reverts changing colors for default theme by '36c5ad4621d372cf932f1259c77a23c4d1cb6962' --- src/Avalonia.Themes.Default/TitleBar.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Default/TitleBar.xaml b/src/Avalonia.Themes.Default/TitleBar.xaml index 4dba5b4ba4..7f8ed24076 100644 --- a/src/Avalonia.Themes.Default/TitleBar.xaml +++ b/src/Avalonia.Themes.Default/TitleBar.xaml @@ -5,7 +5,7 @@ diff --git a/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml index f4145a51f5..88c6b661f1 100644 --- a/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml @@ -1,5 +1,16 @@ - + diff --git a/src/Avalonia.Themes.Fluent/TextBox.xaml b/src/Avalonia.Themes.Fluent/TextBox.xaml index 0327e776e3..be5e21d03d 100644 --- a/src/Avalonia.Themes.Fluent/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/TextBox.xaml @@ -1,4 +1,13 @@ + + + + + + 0,0,0,4 @@ -42,7 +51,7 @@ @@ -136,7 +145,7 @@ diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 3f1ec289d1..b0fbcc76d2 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -67,9 +67,9 @@ namespace ControlCatalog public override void Initialize() { - AvaloniaXamlLoader.Load(this); - Styles.Insert(0, FluentDark); + + AvaloniaXamlLoader.Load(this); } public override void OnFrameworkInitializationCompleted() diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index af95e3c356..efc90357ed 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -1,9 +1,7 @@ + x:Class="ControlCatalog.MainView"> diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 304782dbf9..392ccb57c3 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -43,7 +43,7 @@ - + diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index d775eb9635..c39f414b44 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -29,7 +29,8 @@ namespace ControlCatalog.Pages var screens = w.Screens.All; var scaling = ((IRenderRoot)w).RenderScaling; - Pen p = new Pen(Brushes.Black); + var drawBrush = Brushes.Green; + Pen p = new Pen(drawBrush); if (screens != null) foreach (Screen screen in screens) { @@ -53,19 +54,19 @@ namespace ControlCatalog.Pages }; text.Text = $"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}"; - context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height), text); + context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height), text); text.Text = $"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}"; - context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text); + context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text); text.Text = $"Scaling: {screen.PixelDensity * 100}%"; - context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 40), text); + context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 40), text); text.Text = $"Primary: {screen.Primary}"; - context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 60), text); + context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 60), text); text.Text = $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}"; - context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text); + context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text); } context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10, w.Bounds.Width / 10, w.Bounds.Height / 10)); diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index 4b8edcf98c..4a1c196917 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -12,7 +12,7 @@ @@ -49,7 +49,7 @@ From 037e63d9ab7145893b684b28d2ec62ed614a8a92 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Jul 2020 10:48:29 -0300 Subject: [PATCH 48/79] add unit test for specific scenario where datacontext changes tip before remvoed from visual tree. --- .../ToolTipTests.cs | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index 67df6343af..e52e7a487b 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Markup.Xaml; using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; @@ -66,34 +67,33 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Should_Close_When_Tip_Is_Changed() + public void Should_Close_When_Tip_Is_Opened_And_Detached_From_Visual_Tree() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var window = new Window(); - - var panel = new Panel(); - - var target = new Decorator() - { - [ToolTip.TipProperty] = "Tip", - [ToolTip.ShowDelayProperty] = 0 - }; + var xaml = @" + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); - panel.Children.Add(target); - - window.Content = panel; - + window.DataContext = new ToolTipViewModel(); window.ApplyTemplate(); window.Presenter.ApplyTemplate(); + var target = window.Find("PART_target"); + var panel = window.Find("PART_panel"); + Assert.True((target as IVisual).IsAttachedToVisualTree); _mouseHelper.Enter(target); Assert.True(ToolTip.GetIsOpen(target)); - - ToolTip.SetTip(target, ""); + + panel.Children.Remove(target); Assert.False(ToolTip.GetIsOpen(target)); } @@ -242,4 +242,9 @@ namespace Avalonia.Controls.UnitTests } } } + + internal class ToolTipViewModel + { + public string Tip => "Tip"; + } } From eb132105e43adef9ce6d2feb4675dbab30b1904d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Jul 2020 10:52:47 -0300 Subject: [PATCH 49/79] restore tooltipservice. failing unit test. --- src/Avalonia.Controls/ToolTipService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index 587bbb6aa6..569697304f 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -29,7 +29,6 @@ namespace Avalonia.Controls control.PointerEnter -= ControlPointerEnter; control.PointerLeave -= ControlPointerLeave; control.DetachedFromVisualTree -= ControlDetaching; - Close(control); } if (e.NewValue != null) From 49e41b959438fa4ccab96fcc4b18cef696b859c9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Jul 2020 13:00:05 -0300 Subject: [PATCH 50/79] prevent tooltipgetting stuck. --- src/Avalonia.Controls/ToolTip.cs | 1 + src/Avalonia.Controls/ToolTipService.cs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index b458b15c64..cf0652247f 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -66,6 +66,7 @@ namespace Avalonia.Controls static ToolTip() { TipProperty.Changed.Subscribe(ToolTipService.Instance.TipChanged); + IsOpenProperty.Changed.Subscribe(ToolTipService.Instance.TipOpenChanged); IsOpenProperty.Changed.Subscribe(IsOpenChanged); } diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index 569697304f..e2a0f9e50c 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -28,20 +28,33 @@ namespace Avalonia.Controls { control.PointerEnter -= ControlPointerEnter; control.PointerLeave -= ControlPointerLeave; - control.DetachedFromVisualTree -= ControlDetaching; } if (e.NewValue != null) { control.PointerEnter += ControlPointerEnter; control.PointerLeave += ControlPointerLeave; + } + } + + internal void TipOpenChanged(AvaloniaPropertyChangedEventArgs e) + { + var control = (Control)e.Sender; + + if (e.OldValue is false && e.NewValue is true) + { control.DetachedFromVisualTree += ControlDetaching; } + else if(e.OldValue is true && e.NewValue is false) + { + control.DetachedFromVisualTree -= ControlDetaching; + } } private void ControlDetaching(object sender, VisualTreeAttachmentEventArgs e) { var control = (Control)sender; + control.DetachedFromVisualTree -= ControlDetaching; Close(control); } From 151ea1b1819cccd6b3f184b72e7d8c8283192e37 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 30 Jul 2020 23:59:29 +0200 Subject: [PATCH 51/79] Limit amount of reentrant dispatcher calls. --- .../Threading/AvaloniaScheduler.cs | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs index 0bfc713ba0..397826df53 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs @@ -9,6 +9,16 @@ namespace Avalonia.Threading /// public class AvaloniaScheduler : LocalScheduler { + /// + /// Users can schedule actions on the dispatcher thread while being on the correct thread already. + /// We are optimizing this case by invoking user callback immediately which can lead to stack overflows in certain cases. + /// To prevent this we are limiting amount of reentrant calls to before we will + /// schedule on a dispatcher anyway. + /// + private const int MaxReentrantSchedules = 32; + + private int _reentrancyGuard; + /// /// The instance of the . /// @@ -24,31 +34,58 @@ namespace Avalonia.Threading /// public override IDisposable Schedule(TState state, TimeSpan dueTime, Func action) { - var composite = new CompositeDisposable(2); + IDisposable PostOnDispatcher() + { + var composite = new CompositeDisposable(2); + + var cancellation = new CancellationDisposable(); + + Dispatcher.UIThread.Post(() => + { + if (!cancellation.Token.IsCancellationRequested) + { + composite.Add(action(this, state)); + } + }, DispatcherPriority.DataBind); + + composite.Add(cancellation); + + return composite; + } + if (dueTime == TimeSpan.Zero) { if (!Dispatcher.UIThread.CheckAccess()) { - var cancellation = new CancellationDisposable(); - Dispatcher.UIThread.Post(() => - { - if (!cancellation.Token.IsCancellationRequested) - { - composite.Add(action(this, state)); - } - }, DispatcherPriority.DataBind); - composite.Add(cancellation); + return PostOnDispatcher(); } else { - return action(this, state); + if (_reentrancyGuard >= MaxReentrantSchedules) + { + return PostOnDispatcher(); + } + + try + { + _reentrancyGuard++; + + return action(this, state); + } + finally + { + _reentrancyGuard--; + } } } else { + var composite = new CompositeDisposable(2); + composite.Add(DispatcherTimer.RunOnce(() => composite.Add(action(this, state)), dueTime)); + + return composite; } - return composite; } } } From b941fd675f7c4f4bd7d382f85dd438f1b260b17d Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Thu, 30 Jul 2020 21:19:33 -0400 Subject: [PATCH 52/79] Default theme: Replace DropDown with ComboBox in ManagedFileChooser --- src/Avalonia.Dialogs/ManagedFileChooser.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/ManagedFileChooser.xaml index ea45e83812..227cc1afc0 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml +++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml @@ -66,7 +66,7 @@ - Date: Thu, 30 Jul 2020 21:42:30 -0400 Subject: [PATCH 53/79] Default theme: Add ToggleSwitch resources --- .../Accents/BaseDark.xaml | 2 + .../Accents/BaseLight.xaml | 2 + src/Avalonia.Themes.Default/TabItem.xaml | 2 +- src/Avalonia.Themes.Default/ToggleSwitch.xaml | 45 ++++++++++++++----- src/Avalonia.Themes.Fluent/ToggleSwitch.xaml | 2 - 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index ffe3e92202..4274f7ee4d 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -58,6 +58,8 @@ + + 1,1,1,1 0.5 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index c0e5f47eed..b1c0946d0f 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -61,6 +61,8 @@ + + 1 0.5 diff --git a/src/Avalonia.Themes.Default/TabItem.xaml b/src/Avalonia.Themes.Default/TabItem.xaml index 92482a564c..6e344ce58e 100644 --- a/src/Avalonia.Themes.Default/TabItem.xaml +++ b/src/Avalonia.Themes.Default/TabItem.xaml @@ -2,7 +2,7 @@ - - @@ -262,10 +289,6 @@ - - diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml index 219f6f6742..56249f5986 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -6,8 +6,6 @@ 6 6 154 - 20 - 0 From bf346ff16eb9f52c5529c8f58bb20bca895d9812 Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Thu, 30 Jul 2020 23:10:34 -0400 Subject: [PATCH 54/79] Replace StaticResource with SolidColorBrush+DynamicResource to fix tests --- src/Avalonia.Themes.Default/ToggleSwitch.xaml | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml index 48e92d2283..5c0a3b99f3 100644 --- a/src/Avalonia.Themes.Default/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml @@ -8,38 +8,38 @@ 154 0 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 177c6530931449ff500bb103c65d68c8af6d92b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Onak?= Date: Fri, 31 Jul 2020 22:40:11 +0200 Subject: [PATCH 55/79] Revert GridSplitter size for default theme --- src/Avalonia.Themes.Default/GridSplitter.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/GridSplitter.xaml index dc5cd002dc..6d9cb4f31f 100644 --- a/src/Avalonia.Themes.Default/GridSplitter.xaml +++ b/src/Avalonia.Themes.Default/GridSplitter.xaml @@ -2,8 +2,8 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 4e63d1d223..e5b654b490 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -55,4 +55,7 @@ + + + diff --git a/src/Avalonia.Themes.Default/SplitView.xaml b/src/Avalonia.Themes.Default/SplitView.xaml new file mode 100644 index 0000000000..4a9a7be164 --- /dev/null +++ b/src/Avalonia.Themes.Default/SplitView.xaml @@ -0,0 +1,219 @@ + + + + 320 + 48 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/TimePicker.xaml b/src/Avalonia.Themes.Default/TimePicker.xaml new file mode 100644 index 0000000000..6c4d0c97bd --- /dev/null +++ b/src/Avalonia.Themes.Default/TimePicker.xaml @@ -0,0 +1,283 @@ + + + + + 40 + 1 + 1 + 0,0,0,4 + 40 + 41 + 242 + 456 + 0,3,0,6 + 0,3,0,6 + + + + + + + + + + + + + + + + + + + + + + + + + + From 96d1d7358192511886a8e33ea54df17485ce7309 Mon Sep 17 00:00:00 2001 From: amwx Date: Tue, 4 Aug 2020 23:14:28 -0500 Subject: [PATCH 69/79] SplitView Default style --- src/Avalonia.Themes.Default/SplitView.xaml | 219 +++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 src/Avalonia.Themes.Default/SplitView.xaml diff --git a/src/Avalonia.Themes.Default/SplitView.xaml b/src/Avalonia.Themes.Default/SplitView.xaml new file mode 100644 index 0000000000..4a9a7be164 --- /dev/null +++ b/src/Avalonia.Themes.Default/SplitView.xaml @@ -0,0 +1,219 @@ + + + + 320 + 48 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 80863f44a9d38731f15c8bb4551d54c153c2b5ca Mon Sep 17 00:00:00 2001 From: amwx Date: Tue, 4 Aug 2020 23:15:01 -0500 Subject: [PATCH 70/79] Date/Time Picker default style --- src/Avalonia.Themes.Default/DatePicker.xaml | 334 ++++++++++++++++++++ src/Avalonia.Themes.Default/TimePicker.xaml | 283 +++++++++++++++++ 2 files changed, 617 insertions(+) create mode 100644 src/Avalonia.Themes.Default/DatePicker.xaml create mode 100644 src/Avalonia.Themes.Default/TimePicker.xaml diff --git a/src/Avalonia.Themes.Default/DatePicker.xaml b/src/Avalonia.Themes.Default/DatePicker.xaml new file mode 100644 index 0000000000..da878c88e2 --- /dev/null +++ b/src/Avalonia.Themes.Default/DatePicker.xaml @@ -0,0 +1,334 @@ + + + + + 0,0,0,4 + 40 + 40 + 41 + 296 + 456 + 0,3,0,6 + 9,3,0,6 + 0,3,0,6 + 9,3,0,6 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/TimePicker.xaml b/src/Avalonia.Themes.Default/TimePicker.xaml new file mode 100644 index 0000000000..6c4d0c97bd --- /dev/null +++ b/src/Avalonia.Themes.Default/TimePicker.xaml @@ -0,0 +1,283 @@ + + + + + 40 + 1 + 1 + 0,0,0,4 + 40 + 41 + 242 + 456 + 0,3,0,6 + 0,3,0,6 + + + + + + + + + + + + + + + + + + + + + + + + + + From 7ef5078610930498485f7d578d816418ad838734 Mon Sep 17 00:00:00 2001 From: amwx Date: Tue, 4 Aug 2020 23:15:12 -0500 Subject: [PATCH 71/79] Default theme support --- src/Avalonia.Themes.Default/Accents/BaseDark.xaml | 3 +++ src/Avalonia.Themes.Default/Accents/BaseLight.xaml | 3 +++ src/Avalonia.Themes.Default/DefaultTheme.xaml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index 4274f7ee4d..44dfb9ea48 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -58,6 +58,9 @@ + + + 1,1,1,1 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index b1c0946d0f..9ed3207235 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -61,6 +61,9 @@ + + + 1 diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 4e63d1d223..e5b654b490 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -55,4 +55,7 @@ + + + From 4f7a701ab79044ba29a23e65a48446271ff81dfc Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 5 Aug 2020 06:35:37 +0200 Subject: [PATCH 72/79] Reuse the shaped text runs as much as possible --- .../Media/TextFormatting/TextFormatterImpl.cs | 79 +++++++++++++------ .../Media/TextFormatting/TextLayout.cs | 5 +- .../Media/TextFormatting/TextLineImpl.cs | 4 +- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 10 +-- 4 files changed, 62 insertions(+), 36 deletions(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 9318fcc68e..b116249fd4 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -31,7 +31,8 @@ namespace Avalonia.Media.TextFormatting case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: { - textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties); + textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties, + nextLineBreak); break; } default: @@ -118,7 +119,7 @@ namespace Avalonia.Media.TextFormatting /// The text run's. /// The length to split at. /// The split text runs. - internal static SplitTextRunsResult SplitTextRuns(IReadOnlyList textRuns, int length) + internal static SplitTextRunsResult SplitTextRuns(List textRuns, int length) { var currentLength = 0; @@ -134,13 +135,13 @@ namespace Avalonia.Media.TextFormatting var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; - var first = new ShapedTextCharacters[firstCount]; + var first = new List(firstCount); if (firstCount > 1) { for (var j = 0; j < i; j++) { - first[j] = textRuns[j]; + first.Add(textRuns[j]); } } @@ -148,7 +149,7 @@ namespace Avalonia.Media.TextFormatting if (currentLength + currentRun.GlyphRun.Characters.Length == length) { - var second = new ShapedTextCharacters[secondCount]; + var second = new List(secondCount); var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; @@ -156,11 +157,11 @@ namespace Avalonia.Media.TextFormatting { for (var j = 0; j < secondCount; j++) { - second[j] = textRuns[i + j + offset]; + second.Add(textRuns[i + j + offset]); } } - first[i] = currentRun; + first.Add(currentRun); return new SplitTextRunsResult(first, second); } @@ -168,22 +169,22 @@ namespace Avalonia.Media.TextFormatting { secondCount++; - var second = new ShapedTextCharacters[secondCount]; + var second = new List(secondCount); + + var split = currentRun.Split(length - currentLength); + + first.Add(split.First); + + second.Add(split.Second); if (secondCount > 0) { for (var j = 1; j < secondCount; j++) { - second[j] = textRuns[i + j]; + second.Add(textRuns[i + j]); } } - var split = currentRun.Split(length - currentLength); - - first[i] = split.First; - - second[0] = split.Second; - return new SplitTextRunsResult(first, second); } } @@ -201,7 +202,7 @@ namespace Avalonia.Media.TextFormatting /// /// The formatted text runs. /// - private static IReadOnlyList FetchTextRuns(ITextSource textSource, + private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, TextLineBreak previousLineBreak, out TextLineBreak nextLineBreak) { nextLineBreak = default; @@ -212,8 +213,10 @@ namespace Avalonia.Media.TextFormatting if (previousLineBreak != null) { - foreach (var shapedCharacters in previousLineBreak.RemainingCharacters) + for (var index = 0; index < previousLineBreak.RemainingCharacters.Count; index++) { + var shapedCharacters = previousLineBreak.RemainingCharacters[index]; + if (shapedCharacters == null) { continue; @@ -225,6 +228,14 @@ namespace Avalonia.Media.TextFormatting { var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); + if (++index < previousLineBreak.RemainingCharacters.Count) + { + for (; index < previousLineBreak.RemainingCharacters.Count; index++) + { + splitResult.Second.Add(previousLineBreak.RemainingCharacters[index]); + } + } + nextLineBreak = new TextLineBreak(splitResult.Second); return splitResult.First; @@ -323,9 +334,10 @@ namespace Avalonia.Media.TextFormatting /// The text range that is covered by the text runs. /// The paragraph width. /// The text paragraph properties. + /// The current line break if the line was explicitly broken. /// The wrapped text line. - private static TextLine PerformTextWrapping(IReadOnlyList textRuns, TextRange textRange, - double paragraphWidth, TextParagraphProperties paragraphProperties) + private static TextLine PerformTextWrapping(List textRuns, TextRange textRange, + double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak currentLineBreak) { var availableWidth = paragraphWidth; var currentWidth = 0.0; @@ -388,8 +400,22 @@ namespace Avalonia.Media.TextFormatting var textLineMetrics = TextLineMetrics.Create(splitResult.First, new TextRange(textRange.Start, currentLength), paragraphWidth, paragraphProperties); - var lineBreak = splitResult.Second != null && splitResult.Second.Count > 0 ? - new TextLineBreak(splitResult.Second) : + var remainingCharacters = splitResult.Second; + + if (currentLineBreak?.RemainingCharacters != null) + { + if (remainingCharacters != null) + { + remainingCharacters.AddRange(currentLineBreak.RemainingCharacters); + } + else + { + remainingCharacters = new List(currentLineBreak.RemainingCharacters); + } + } + + var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ? + new TextLineBreak(remainingCharacters) : null; return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak); @@ -403,7 +429,10 @@ namespace Avalonia.Media.TextFormatting } return new TextLineImpl(textRuns, - TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties)); + TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties), + currentLineBreak?.RemainingCharacters != null ? + new TextLineBreak(currentLineBreak.RemainingCharacters) : + null); } /// @@ -434,7 +463,7 @@ namespace Avalonia.Media.TextFormatting internal readonly struct SplitTextRunsResult { - public SplitTextRunsResult(IReadOnlyList first, IReadOnlyList second) + public SplitTextRunsResult(List first, List second) { First = first; @@ -447,7 +476,7 @@ namespace Avalonia.Media.TextFormatting /// /// The first text runs. /// - public IReadOnlyList First { get; } + public List First { get; } /// /// Gets the second text runs. @@ -455,7 +484,7 @@ namespace Avalonia.Media.TextFormatting /// /// The second text runs. /// - public IReadOnlyList Second { get; } + public List Second { get; } } private struct TextRunEnumerator diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 14602a2560..fa7d6cb4bf 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -183,7 +183,10 @@ namespace Avalonia.Media.TextFormatting var glyphRun = TextShaper.Current.ShapeText(new ReadOnlySlice(s_empty, startingIndex, 1), properties.Typeface, properties.FontRenderingEmSize, properties.CultureInfo); - var textRuns = new[] { new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties) }; + var textRuns = new List + { + new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties) + }; return new TextLineImpl(textRuns, TextLineMetrics.Create(textRuns, new TextRange(startingIndex, 1), MaxWidth, _paragraphProperties)); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index 435752160e..8b44e32c48 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -6,9 +6,9 @@ namespace Avalonia.Media.TextFormatting { internal class TextLineImpl : TextLine { - private readonly IReadOnlyList _textRuns; + private readonly List _textRuns; - public TextLineImpl(IReadOnlyList textRuns, TextLineMetrics lineMetrics, + public TextLineImpl(List textRuns, TextLineMetrics lineMetrics, TextLineBreak lineBreak = null, bool hasCollapsed = false) { _textRuns = textRuns; diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index b0384a1fdf..61075171fe 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -123,10 +123,7 @@ namespace Avalonia.Skia return; } - if (offsetBuffer == null) - { - offsetBuffer = new Vector[glyphPositions.Length]; - } + offsetBuffer ??= new Vector[glyphPositions.Length]; var offsetX = position.XOffset * textScale; @@ -138,10 +135,7 @@ namespace Avalonia.Skia private static void SetAdvance(ReadOnlySpan glyphPositions, int index, double textScale, ref double[] advanceBuffer) { - if (advanceBuffer == null) - { - advanceBuffer = new double[glyphPositions.Length]; - } + advanceBuffer ??= new double[glyphPositions.Length]; // Depends on direction of layout // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale; From 922c46bbc4d125a005164c942cc1af70729afd6a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 5 Aug 2020 14:38:30 +0200 Subject: [PATCH 73/79] Dispose the grid hack in the correct place. The grid hack was initiated when attached to the visual tree but the dispose had been placed in `OnDetachedFromLogicalTree` by mistake. Place it in `OnDetachedFromVisualTree`. --- src/Avalonia.Controls/MenuItem.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 912abc6de3..2a22896de1 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -323,9 +323,6 @@ namespace Avalonia.Controls { Command.CanExecuteChanged -= CanExecuteChanged; } - - _gridHack?.Dispose(); - _gridHack = null; } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) @@ -351,6 +348,14 @@ namespace Avalonia.Controls } } + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _gridHack?.Dispose(); + _gridHack = null; + } + /// /// Called when the is clicked. /// From a91d6322112c679e96dc8968280bf62832139b9e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 5 Aug 2020 16:33:09 +0200 Subject: [PATCH 74/79] Move ListBox page view model to its own file. Also remove `SelectedItem` and `SelectedItems` in favor of `SelectionModel`. --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 3 +- .../ControlCatalog/Pages/ListBoxPage.xaml.cs | 68 +------------------ .../ViewModels/ListBoxPageViewModel.cs | 65 ++++++++++++++++++ 3 files changed, 68 insertions(+), 68 deletions(-) create mode 100644 samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index f4d81418ac..edf3d41bf5 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -11,8 +11,7 @@ Spacing="16"> (Enumerable.Range(1, 10000).Select(i => GenerateItem())); - SelectedItems = new ObservableCollection(); - - AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); - - RemoveItemCommand = ReactiveCommand.Create(() => - { - while (SelectedItems.Count > 0) - { - Items.Remove(SelectedItems[0]); - } - }); - - SelectRandomItemCommand = ReactiveCommand.Create(() => - { - var random = new Random(); - - SelectedItem = Items[random.Next(Items.Count - 1)]; - }); - } - - public ObservableCollection Items { get; } - - private string _selectedItem; - - public string SelectedItem - { - get { return _selectedItem; } - set { this.RaiseAndSetIfChanged(ref _selectedItem, value); } - } - - - public ObservableCollection SelectedItems { get; } - - public ReactiveCommand AddItemCommand { get; } - - public ReactiveCommand RemoveItemCommand { get; } - - public ReactiveCommand SelectRandomItemCommand { get; } - - public SelectionMode SelectionMode - { - get => _selectionMode; - set - { - SelectedItems.Clear(); - this.RaiseAndSetIfChanged(ref _selectionMode, value); - } - } - - private string GenerateItem() => $"Item {_counter++.ToString()}"; - } } } diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs new file mode 100644 index 0000000000..6bdb5c0103 --- /dev/null +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using Avalonia.Controls; +using ReactiveUI; + +namespace ControlCatalog.ViewModels +{ + public class ListBoxPageViewModel : ReactiveObject + { + private int _counter; + private SelectionMode _selectionMode; + + public ListBoxPageViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem())); + Selection = new SelectionModel(); + Selection.Select(1); + + AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); + + RemoveItemCommand = ReactiveCommand.Create(() => + { + while (Selection.SelectedItems.Count > 0) + { + Items.Remove((string)Selection.SelectedItems.First()); + } + }); + + SelectRandomItemCommand = ReactiveCommand.Create(() => + { + var random = new Random(); + + using (Selection.Update()) + { + Selection.ClearSelection(); + Selection.Select(random.Next(Items.Count - 1)); + } + }); + } + + public ObservableCollection Items { get; } + + public SelectionModel Selection { get; } + + public ReactiveCommand AddItemCommand { get; } + + public ReactiveCommand RemoveItemCommand { get; } + + public ReactiveCommand SelectRandomItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + Selection.ClearSelection(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private string GenerateItem() => $"Item {_counter++.ToString()}"; + } +} From d2af884aedf68f4534e2afa20e4c282cc4e7eba2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 5 Aug 2020 17:13:02 +0200 Subject: [PATCH 75/79] Added a failing test for #4272. --- .../Utils/SelectedItemsSyncTests.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs index 3ab5950974..4aa7e24aa7 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs @@ -208,6 +208,20 @@ namespace Avalonia.Controls.UnitTests.Utils target.SetItems(new[] { "foo", "bar", "baz" })); } + [Fact] + public void Selected_Items_Can_Be_Set_Before_SelectionModel_Source() + { + var model = new SelectionModel(); + var target = new SelectedItemsSync(model); + var items = new AvaloniaList { "foo", "bar", "baz" }; + var selectedItems = new AvaloniaList { "bar" }; + + target.SetItems(selectedItems); + model.Source = items; + + Assert.Equal(new IndexPath(1), model.SelectedIndex); + } + private static SelectedItemsSync CreateTarget( IEnumerable items = null) { From 0f9ac73b4fc43dde5384e7eb4825a87e0266446f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 5 Aug 2020 17:13:18 +0200 Subject: [PATCH 76/79] Handle uninitialized Source in SelectedItemsSync. If the `SelectionModel` passed to `SelectedItemsSync` has not yet had a `Source` assigned, set a flag to write the selected items to the `SelectionModel` when `Source` is set. Fixes #4272 --- .../Utils/SelectedItemsSync.cs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Utils/SelectedItemsSync.cs b/src/Avalonia.Controls/Utils/SelectedItemsSync.cs index c127771990..91cef9fe64 100644 --- a/src/Avalonia.Controls/Utils/SelectedItemsSync.cs +++ b/src/Avalonia.Controls/Utils/SelectedItemsSync.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using Avalonia.Collections; @@ -16,6 +17,7 @@ namespace Avalonia.Controls.Utils private IList? _items; private bool _updatingItems; private bool _updatingModel; + private bool _initializeOnSourceAssignment; public SelectedItemsSync(ISelectionModel model) { @@ -63,10 +65,18 @@ namespace Avalonia.Controls.Utils _updatingModel = true; _items = items; - using (Model.Update()) + if (Model.Source is object) { - Model.ClearSelection(); - Add(items); + using (Model.Update()) + { + Model.ClearSelection(); + Add(items); + } + } + else if (!_initializeOnSourceAssignment) + { + Model.PropertyChanged += SelectionModelPropertyChanged; + _initializeOnSourceAssignment = true; } if (_items is INotifyCollectionChanged incc2) @@ -86,9 +96,11 @@ namespace Avalonia.Controls.Utils if (_items != null) { + Model.PropertyChanged -= SelectionModelPropertyChanged; Model.SelectionChanged -= SelectionModelSelectionChanged; Model = model; Model.SelectionChanged += SelectionModelSelectionChanged; + _initializeOnSourceAssignment = false; try { @@ -175,6 +187,25 @@ namespace Avalonia.Controls.Utils } } + private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (_initializeOnSourceAssignment && + _items != null && + e.PropertyName == nameof(SelectionModel.Source)) + { + try + { + _updatingModel = true; + Add(_items); + _initializeOnSourceAssignment = false; + } + finally + { + _updatingModel = false; + } + } + } + private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) { if (_updatingModel) From db67fa2cf6c4918a99006528567e2db1c2e0ea26 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 5 Aug 2020 23:57:45 +0200 Subject: [PATCH 77/79] Tweak the MenuItem Grid hack. To try and fix menu layout in the fluent theme. --- src/Avalonia.Controls/MenuItem.cs | 59 ++++++++++++++----------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 2a22896de1..bfa2e81745 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Linq; using System.Windows.Input; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; @@ -97,7 +98,6 @@ namespace Avalonia.Controls private ICommand _command; private bool _commandCanExecute = true; private Popup _popup; - private IDisposable _gridHack; /// /// Initializes static members of the class. @@ -119,6 +119,32 @@ namespace Avalonia.Controls public MenuItem() { + // HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached + // property to store SharedSizeGroup state, except property inheritance is done + // down the logical tree. In this case, the control which is setting + // Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing + // the way Grid stores shared size state, the developers of WPF just created a + // binding of the internal state of the visual parent to the menu item. We don't + // have much choice but to do the same for now unless we want to refactor Grid, + // which I honestly am not brave enough to do right now. Here's the same hack in + // the WPF codebase: + // + // https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141 + // + // In addition to the hack from WPF, we also make sure to return null when we have + // no parent. If we don't do this, inheritance falls back to the logical tree, + // causing the shared size scope in the parent MenuItem to be used, breaking + // menu layout. + + var parentSharedSizeScope = this.GetObservable(VisualParentProperty) + .SelectMany(x => + { + var parent = x as Control; + return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ?? + Observable.Return(null); + }); + + this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope); } /// @@ -325,37 +351,6 @@ namespace Avalonia.Controls } } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - if (this.GetVisualParent() is IControl parent) - { - // HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached - // property to store SharedSizeGroup state, except property inheritance is done - // down the logical tree. In this case, the control which is setting - // Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing - // the way Grid stores shared size state, the developers of WPF just created a - // binding of the internal state of the visual parent to the menu item. We don't - // have much choice but to do the same for now unless we want to refactor Grid, - // which I honestly am not brave enough to do right now. Here's the same hack in - // the WPF codebase: - // - // https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141 - _gridHack = Bind( - DefinitionBase.PrivateSharedSizeScopeProperty, - parent.GetBindingObservable(DefinitionBase.PrivateSharedSizeScopeProperty)); - } - } - - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - - _gridHack?.Dispose(); - _gridHack = null; - } - /// /// Called when the is clicked. /// From c5c12393f6c9c495fea9215aac4a635475bce7fa Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Fri, 7 Aug 2020 20:07:46 +0300 Subject: [PATCH 78/79] Fix typo in FontSizeNormal --- src/Avalonia.Themes.Default/Window.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Default/Window.xaml b/src/Avalonia.Themes.Default/Window.xaml index 7d74a7e6a0..3b378dbcbe 100644 --- a/src/Avalonia.Themes.Default/Window.xaml +++ b/src/Avalonia.Themes.Default/Window.xaml @@ -2,7 +2,7 @@ - + From 3e2e49bda45d49f2feb684f52609b9455f43aaad Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 10 Aug 2020 18:22:12 +0200 Subject: [PATCH 79/79] Bump version to 0.10.999 for CI builds. Now that 0.10 is (nearly) out, bump version on master. --- build/SharedVersion.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/SharedVersion.props b/build/SharedVersion.props index bd183faab3..d3cebef418 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> Avalonia - 0.9.999 + 0.10.999 Copyright 2020 © The AvaloniaUI Project https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/