From 8ae3a9f3580e65a6b00e119c758f5a10f83bb59a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 15 Dec 2020 23:11:54 -0500 Subject: [PATCH 1/9] Fix DataGridColumn.DisplayIndex initial value handling --- samples/ControlCatalog/Pages/DataGridPage.xaml | 4 ++-- src/Avalonia.Controls.DataGrid/DataGrid.cs | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index cacc2204bd..6817d0698e 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -38,8 +38,8 @@ - - + + diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 5bb2763566..e582153f41 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2285,6 +2285,17 @@ namespace Avalonia.Controls } } + /// + /// Comparator class so we can sort list by the display index + /// + public class DisplayIndexComparer : IComparer + { + int IComparer.Compare(DataGridColumn x, DataGridColumn y) + { + return (x.DisplayIndexWithFiller < y.DisplayIndexWithFiller) ? -1 : 1; + } + } + /// /// Builds the visual tree for the column header when a new template is applied. /// @@ -2309,8 +2320,11 @@ namespace Avalonia.Controls ColumnsInternal.FillerColumn.IsRepresented = false; } _columnHeadersPresenter.OwningGrid = this; - // Columns were added before before our Template was applied, add the ColumnHeaders now - foreach (DataGridColumn column in ColumnsItemsInternal) + + // Columns were added before our Template was applied, add the ColumnHeaders now + List sortedInternal = new List(ColumnsItemsInternal); + sortedInternal.Sort(new DisplayIndexComparer()); + foreach (DataGridColumn column in sortedInternal) { InsertDisplayedColumnHeader(column); } From 5109b00644ea834ab65bc6f9681e6a7717458634 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 20 Dec 2020 18:04:01 -0500 Subject: [PATCH 2/9] Reuse PropertyType in the ShouldNotSet method --- src/Avalonia.Base/Data/Core/SettableNode.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index d0a918dc88..363d3da0ef 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -15,7 +15,8 @@ namespace Avalonia.Data.Core private bool ShouldNotSet(object value) { - if (PropertyType == null) + var propertyType = PropertyType; + if (propertyType == null) { return false; } @@ -37,7 +38,7 @@ namespace Avalonia.Data.Core return false; } - if (PropertyType.IsValueType) + if (propertyType.IsValueType) { return lastValue.Equals(value); } From b240fc7d0c75bbfc2e060c78b67e4af4278e638f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 20 Dec 2020 18:05:59 -0500 Subject: [PATCH 3/9] Add failing and working tests for null DataContext --- .../AvaloniaObjectTests_Binding.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 7fc0d52be6..7c4c5c96a5 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -4,6 +4,8 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading; using System.Threading.Tasks; + +using Avalonia.Controls; using Avalonia.Data; using Avalonia.Logging; using Avalonia.Platform; @@ -797,6 +799,24 @@ namespace Avalonia.Base.UnitTests Assert.False(source.SetterCalled); } + [Fact] + public void TwoWay_Binding_Should_Not_Fail_With_Null_DataContext() + { + var target = new TextBlock(); + target.DataContext = null; + + target.Bind(TextBlock.TextProperty, new Binding("Missing", BindingMode.TwoWay)); + } + + [Fact] + public void TwoWay_Binding_Should_Not_Fail_With_Null_DataContext_Indexer() + { + var target = new TextBlock(); + target.DataContext = null; + + target.Bind(TextBlock.TextProperty, new Binding("[0]", BindingMode.TwoWay)); + } + [Fact] public void Disposing_Completed_Binding_Does_Not_Throw() { From d020d9872cbd385b8bf124b224f611bf58d15a43 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 20 Dec 2020 18:07:06 -0500 Subject: [PATCH 4/9] Fix failing test when DataContext is null --- .../Markup/Parsers/Nodes/StringIndexerNode.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs index 76fbc9a982..f3abd6a5c5 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs @@ -129,7 +129,10 @@ namespace Avalonia.Markup.Parsers.Nodes { get { - Target.TryGetTarget(out object target); + if (!Target.TryGetTarget(out object target)) + { + return null; + } return GetIndexer(target.GetType().GetTypeInfo())?.PropertyType; } From 5403c743441d476f2577c3d3c1b4c91e1e5a0477 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 3 Jan 2021 03:31:15 -0500 Subject: [PATCH 5/9] Recalculate ToolTip position on Offset/Placement props changed --- src/Avalonia.Controls/ToolTip.cs | 40 +++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 71bd0726d4..ab507d07a2 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Reactive.Linq; using Avalonia.Controls.Metadata; @@ -21,8 +22,8 @@ namespace Avalonia.Controls /// /// Defines the ToolTip.Tip attached property. /// - public static readonly AttachedProperty TipProperty = - AvaloniaProperty.RegisterAttached("Tip"); + public static readonly AttachedProperty TipProperty = + AvaloniaProperty.RegisterAttached("Tip"); /// /// Defines the ToolTip.IsOpen attached property. @@ -57,10 +58,10 @@ namespace Avalonia.Controls /// /// Stores the current instance in the control. /// - internal static readonly AttachedProperty ToolTipProperty = - AvaloniaProperty.RegisterAttached("ToolTip"); + internal static readonly AttachedProperty ToolTipProperty = + AvaloniaProperty.RegisterAttached("ToolTip"); - private IPopupHost _popup; + private IPopupHost? _popup; /// /// Initializes static members of the class. @@ -70,6 +71,10 @@ namespace Avalonia.Controls TipProperty.Changed.Subscribe(ToolTipService.Instance.TipChanged); IsOpenProperty.Changed.Subscribe(ToolTipService.Instance.TipOpenChanged); IsOpenProperty.Changed.Subscribe(IsOpenChanged); + + HorizontalOffsetProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged); + VerticalOffsetProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged); + PlacementProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged); } /// @@ -79,7 +84,7 @@ namespace Avalonia.Controls /// /// The content to be displayed in the control's tooltip. /// - public static object GetTip(Control element) + public static object? GetTip(Control element) { return element.GetValue(TipProperty); } @@ -89,7 +94,7 @@ namespace Avalonia.Controls /// /// The control to get the property from. /// The content to be displayed in the control's tooltip. - public static void SetTip(Control element, object value) + public static void SetTip(Control element, object? value) { element.SetValue(TipProperty, value); } @@ -207,8 +212,8 @@ namespace Avalonia.Controls private static void IsOpenChanged(AvaloniaPropertyChangedEventArgs e) { var control = (Control)e.Sender; - var newValue = (bool)e.NewValue; - ToolTip toolTip; + var newValue = (bool)e.NewValue!; + ToolTip? toolTip; if (newValue) { @@ -235,6 +240,23 @@ namespace Avalonia.Controls toolTip?.UpdatePseudoClasses(newValue); } + private static void RecalculatePositionOnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + var control = (Control)args.Sender; + var tooltip = control.GetValue(ToolTipProperty); + if (tooltip == null) + { + return; + } + + tooltip.RecalculatePosition(control); + } + + internal void RecalculatePosition(Control control) + { + _popup?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); + } + private void Open(Control control) { Close(); From 3140c1c15e14cdf4eed536e307b5f0af164153ba Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 3 Jan 2021 03:31:31 -0500 Subject: [PATCH 6/9] Recalculate ToolTip position on target bounds changed --- src/Avalonia.Controls/ToolTipService.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index 341ab2fe81..cd0fad1997 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -28,12 +28,14 @@ namespace Avalonia.Controls { control.PointerEnter -= ControlPointerEnter; control.PointerLeave -= ControlPointerLeave; + control.PropertyChanged -= Control_PropertyChanged; } if (e.NewValue != null) { control.PointerEnter += ControlPointerEnter; control.PointerLeave += ControlPointerLeave; + control.PropertyChanged += Control_PropertyChanged; } if (ToolTip.GetIsOpen(control) && e.NewValue != e.OldValue && !(e.NewValue is ToolTip)) @@ -97,6 +99,17 @@ namespace Avalonia.Controls Close(control); } + private void Control_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + var control = (Control)sender; + + if (e.Property == Visual.TransformedBoundsProperty) + { + var toolTip = control.GetValue(ToolTip.ToolTipProperty); + toolTip?.RecalculatePosition(control); + } + } + private void StartShowTimer(int showDelay, Control control) { _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(showDelay) }; From 2cac9980e1ed21c4413e795394538bb2f039e0a6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 3 Jan 2021 03:31:51 -0500 Subject: [PATCH 7/9] Update tooltip samples --- samples/ControlCatalog/Pages/SliderPage.xaml | 14 ++++++++++ samples/ControlCatalog/Pages/ToolTipPage.xaml | 27 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index b3f32ed421..eeb198976b 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -22,6 +22,20 @@ IsSnapToTickEnabled="True" Ticks="0,20,25,40,75,100" Width="300" /> + + + + + ToolTip A control which pops up a hint when a control is hovered - @@ -38,6 +38,31 @@ ToolTip bottom placement + + + + + Moving offset + From ca947cc912288c464ec5e30e7c21d7b94328dac8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 3 Jan 2021 04:04:19 -0500 Subject: [PATCH 8/9] Use Bounds instead of TransformedBoundsProperty to recalculate ToolTip position --- src/Avalonia.Controls/ToolTipService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index cd0fad1997..d40a116dd5 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -103,7 +103,7 @@ namespace Avalonia.Controls { var control = (Control)sender; - if (e.Property == Visual.TransformedBoundsProperty) + if (e.Property == Visual.BoundsProperty) { var toolTip = control.GetValue(ToolTip.ToolTipProperty); toolTip?.RecalculatePosition(control); From ee717223f002832853d93e5d7d2b734e9431ca4d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 3 Jan 2021 16:18:31 -0500 Subject: [PATCH 9/9] Use EffectiveViewportChanged instead of subscribing on PropertyChanged --- src/Avalonia.Controls/ToolTipService.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index d40a116dd5..9e0bf60f42 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -28,14 +28,12 @@ namespace Avalonia.Controls { control.PointerEnter -= ControlPointerEnter; control.PointerLeave -= ControlPointerLeave; - control.PropertyChanged -= Control_PropertyChanged; } if (e.NewValue != null) { control.PointerEnter += ControlPointerEnter; control.PointerLeave += ControlPointerLeave; - control.PropertyChanged += Control_PropertyChanged; } if (ToolTip.GetIsOpen(control) && e.NewValue != e.OldValue && !(e.NewValue is ToolTip)) @@ -53,10 +51,12 @@ namespace Avalonia.Controls if (e.OldValue is false && e.NewValue is true) { control.DetachedFromVisualTree += ControlDetaching; + control.EffectiveViewportChanged += ControlEffectiveViewportChanged; } else if(e.OldValue is true && e.NewValue is false) { control.DetachedFromVisualTree -= ControlDetaching; + control.EffectiveViewportChanged -= ControlEffectiveViewportChanged; } } @@ -64,6 +64,7 @@ namespace Avalonia.Controls { var control = (Control)sender; control.DetachedFromVisualTree -= ControlDetaching; + control.EffectiveViewportChanged -= ControlEffectiveViewportChanged; Close(control); } @@ -99,15 +100,11 @@ namespace Avalonia.Controls Close(control); } - private void Control_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void ControlEffectiveViewportChanged(object sender, Layout.EffectiveViewportChangedEventArgs e) { var control = (Control)sender; - - if (e.Property == Visual.BoundsProperty) - { - var toolTip = control.GetValue(ToolTip.ToolTipProperty); - toolTip?.RecalculatePosition(control); - } + var toolTip = control.GetValue(ToolTip.ToolTipProperty); + toolTip?.RecalculatePosition(control); } private void StartShowTimer(int showDelay, Control control)