From a2b1320bafda18ee6e3c3841b46aa7ece226a99b Mon Sep 17 00:00:00 2001 From: Collin Alpert Date: Wed, 21 Apr 2021 18:55:56 +0200 Subject: [PATCH 01/13] Make DataGrid.SelectAll() public --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 2604c7a082..a070a1b9d7 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -5357,7 +5357,7 @@ namespace Avalonia.Controls _focusedRow = null; } - private void SelectAll() + public void SelectAll() { SetRowsSelection(0, SlotCount - 1); } From 753c991aa88f4995c8dd3c3a0e0213f27a763851 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 23 Apr 2021 13:51:38 +0200 Subject: [PATCH 02/13] Add failing test condition --- tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 1eec1d84c1..64c0020f92 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -378,7 +378,7 @@ namespace Avalonia.Controls.UnitTests [Theory] [InlineData(new object[] { false, TextWrapping.NoWrap, ScrollBarVisibility.Hidden })] - [InlineData(new object[] { false, TextWrapping.Wrap, ScrollBarVisibility.Hidden })] + [InlineData(new object[] { false, TextWrapping.Wrap, ScrollBarVisibility.Disabled })] [InlineData(new object[] { true, TextWrapping.NoWrap, ScrollBarVisibility.Auto })] [InlineData(new object[] { true, TextWrapping.Wrap, ScrollBarVisibility.Disabled })] public void Has_Correct_Horizontal_ScrollBar_Visibility( From 3f014385939c531b9e4c72b4a6d6a7a28aaea749 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 23 Apr 2021 13:55:13 +0200 Subject: [PATCH 03/13] Fix TextWrapping for AcceptsReturn = false --- src/Avalonia.Controls/TextBox.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 158f3ac886..69da667011 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -175,16 +175,12 @@ namespace Avalonia.Controls this.GetObservable(TextWrappingProperty), (acceptsReturn, wrapping) => { - if (acceptsReturn) + if (wrapping != TextWrapping.NoWrap) { - return wrapping != TextWrapping.Wrap ? - ScrollBarVisibility.Auto : - ScrollBarVisibility.Disabled; - } - else - { - return ScrollBarVisibility.Hidden; + return ScrollBarVisibility.Disabled; } + + return acceptsReturn ? ScrollBarVisibility.Auto : ScrollBarVisibility.Hidden; }); this.Bind( ScrollViewer.HorizontalScrollBarVisibilityProperty, From 202c67caa8e5c60c38dbfe171bc99a674aace7c8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 24 Apr 2021 19:02:13 -0400 Subject: [PATCH 04/13] Fix autocompletebox pseduclasses --- src/Avalonia.Controls/AutoCompleteBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index aab6a41890..c656ba6f6c 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -31,7 +31,6 @@ namespace Avalonia.Controls /// /// event. /// - [PseudoClasses(":dropdownopen")] public class PopulatedEventArgs : EventArgs { /// @@ -253,6 +252,7 @@ namespace Avalonia.Controls /// drop-down that contains possible matches based on the input in the text /// box. /// + [PseudoClasses(":dropdownopen")] public class AutoCompleteBox : TemplatedControl { /// From dbab1666a25afa7943a4be28005af032cd0479cc Mon Sep 17 00:00:00 2001 From: Murdo R Ergeaux Date: Wed, 28 Apr 2021 18:24:18 +0100 Subject: [PATCH 05/13] Use ItemCount to determine PseudoClass in ItemsControl --- src/Avalonia.Controls/ItemsControl.cs | 15 +++-- .../ItemsControlTests.cs | 64 +++++++++++++++++++ 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 4dc8aec6f3..051e1fb0ed 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -346,6 +346,8 @@ namespace Avalonia.Controls Presenter.Items = newValue; } + UpdatePseudoClasses(); + SubscribeToItems(newValue); } @@ -372,9 +374,7 @@ namespace Avalonia.Controls Presenter?.ItemsChanged(e); - var collection = sender as ICollection; - PseudoClasses.Set(":empty", collection == null || collection.Count == 0); - PseudoClasses.Set(":singleitem", collection != null && collection.Count == 1); + UpdatePseudoClasses(); } /// @@ -431,9 +431,6 @@ namespace Avalonia.Controls /// The items collection. private void SubscribeToItems(IEnumerable items) { - PseudoClasses.Set(":empty", items == null || items.Count() == 0); - PseudoClasses.Set(":singleitem", items != null && items.Count() == 1); - if (items is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.AddListener(incc, this); @@ -469,6 +466,12 @@ namespace Avalonia.Controls } } + private void UpdatePseudoClasses() + { + PseudoClasses.Set(":empty", ItemCount == 0); + PseudoClasses.Set(":singleitem", ItemCount == 1); + } + protected static IInputElement GetNextControl( INavigableContainer container, NavigationDirection direction, diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 684486cbae..5413d6d823 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -393,6 +393,70 @@ namespace Avalonia.Controls.UnitTests Assert.Contains(":empty", target.Classes); } + [Fact] + public void Item_Count_Should_Be_Set_When_Items_Added() + { + var target = new ItemsControl() + { + Template = GetTemplate(), + Items = new[] { 1, 2, 3 }, + }; + + Assert.Equal(3, target.ItemCount); + } + + [Fact] + public void Item_Count_Should_Be_Set_When_Items_Changed() + { + var items = new ObservableCollection() { 1, 2, 3 }; + + var target = new ItemsControl() + { + Template = GetTemplate(), + Items = items, + }; + + items.Add(4); + + Assert.Equal(4, target.ItemCount); + + items.Clear(); + + Assert.Equal(0, target.ItemCount); + } + + [Fact] + public void Empty_Class_Should_Be_Set_When_Items_Collection_Cleared() + { + var items = new ObservableCollection() { 1, 2, 3 }; + + var target = new ItemsControl() + { + Template = GetTemplate(), + Items = items, + }; + + items.Clear(); + + Assert.Contains(":empty", target.Classes); + } + + [Fact] + public void Empty_Class_Should_Not_Be_Set_When_Items_Collection_Count_Increases() + { + var items = new ObservableCollection() {}; + + var target = new ItemsControl() + { + Template = GetTemplate(), + Items = items, + }; + + items.Add(1); + + Assert.DoesNotContain(":empty", target.Classes); + } + [Fact] public void Setting_Presenter_Explicitly_Should_Set_Item_Parent() { From 4cd4b0e09ede5d20634f232917244e1c96b7b084 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Wed, 28 Apr 2021 21:33:38 -0400 Subject: [PATCH 06/13] Added IComparer sorting to DataGridColumn Adds an IComparer property to DataGridColumn that can be used to customized the how a column gets sorted --- .../Collections/DataGridSortDescription.cs | 37 +++++++++++++++++++ .../DataGridColumn.cs | 16 ++++++++ .../DataGridColumnHeader.cs | 6 +++ 3 files changed, 59 insertions(+) diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs index 662ff91329..ca6020128c 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs @@ -265,6 +265,43 @@ namespace Avalonia.Collections { return new DataGridPathSortDescription(propertyPath, direction, comparer, null); } + + public static DataGridSortDescription FromComparer(IComparer comparer, ListSortDirection direction = ListSortDirection.Ascending) + { + return new DataGridComparerSortDesctiption(comparer, direction); + } + } + + public class DataGridComparerSortDesctiption : DataGridSortDescription + { + private readonly IComparer _innerComparer; + private readonly ListSortDirection _direction; + private readonly IComparer _comparer; + + public IComparer SourceComparer => _innerComparer; + public override IComparer Comparer => _comparer; + public override ListSortDirection Direction => _direction; + public DataGridComparerSortDesctiption(IComparer comparer, ListSortDirection direction) + { + _innerComparer = comparer; + _direction = direction; + _comparer = Comparer.Create((x, y) => Compare(x, y)); + } + + private int Compare(object x, object y) + { + int result = _innerComparer.Compare(x, y); + + if (Direction == ListSortDirection.Descending) + return -result; + else + return result; + } + public override DataGridSortDescription SwitchSortDirection() + { + var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending; + return new DataGridComparerSortDesctiption(_innerComparer, newDirection); + } } public class DataGridSortDescriptionCollection : AvaloniaList diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 407d6ff058..9141fb2463 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -1009,6 +1009,14 @@ namespace Avalonia.Controls get; set; } + /// + /// Holds a Comparer to use for sorting, if not using the default. + /// + public System.Collections.IComparer CustomSortComparer + { + get; + set; + } /// /// We get the sort description from the data source. We don't worry whether we can modify sort -- perhaps the sort description @@ -1020,6 +1028,14 @@ namespace Avalonia.Controls && OwningGrid.DataConnection != null && OwningGrid.DataConnection.SortDescriptions != null) { + if(CustomSortComparer != null) + { + return + OwningGrid.DataConnection.SortDescriptions + .OfType() + .FirstOrDefault(s => s.SourceComparer == CustomSortComparer); + } + string propertyName = GetSortPropertyName(); return OwningGrid.DataConnection.SortDescriptions.FirstOrDefault(s => s.HasPropertyPath && s.PropertyPath == propertyName); diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 7f8d205949..6f957497cb 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -274,6 +274,12 @@ namespace Avalonia.Controls owningGrid.DataConnection.SortDescriptions.Add(newSort); } } + else if (OwningColumn.CustomSortComparer != null) + { + newSort = DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer); + + owningGrid.DataConnection.SortDescriptions.Add(newSort); + } else { string propertyName = OwningColumn.GetSortPropertyName(); From b1db4ce6d23edb0ffc5ed340332518bce6d39850 Mon Sep 17 00:00:00 2001 From: Murdo R Ergeaux Date: Thu, 29 Apr 2021 08:39:02 +0100 Subject: [PATCH 07/13] Update ItemsControl PseudoClasses on ItemCount property changed --- src/Avalonia.Controls/ItemsControl.cs | 22 ++++--- .../ItemsControlTests.cs | 61 ++++++++++++++++++- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 051e1fb0ed..20d032f597 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -70,7 +70,7 @@ namespace Avalonia.Controls /// public ItemsControl() { - PseudoClasses.Add(":empty"); + UpdatePseudoClasses(0); SubscribeToItems(_items); } @@ -323,6 +323,16 @@ namespace Avalonia.Controls base.OnKeyDown(e); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ItemCountProperty) + { + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + } + } + /// /// Called when the property changes. /// @@ -346,8 +356,6 @@ namespace Avalonia.Controls Presenter.Items = newValue; } - UpdatePseudoClasses(); - SubscribeToItems(newValue); } @@ -373,8 +381,6 @@ namespace Avalonia.Controls } Presenter?.ItemsChanged(e); - - UpdatePseudoClasses(); } /// @@ -466,10 +472,10 @@ namespace Avalonia.Controls } } - private void UpdatePseudoClasses() + private void UpdatePseudoClasses(int itemCount) { - PseudoClasses.Set(":empty", ItemCount == 0); - PseudoClasses.Set(":singleitem", ItemCount == 1); + PseudoClasses.Set(":empty", itemCount == 0); + PseudoClasses.Set(":singleitem", itemCount == 1); } protected static IInputElement GetNextControl( diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 5413d6d823..bfece7871c 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -379,6 +379,17 @@ namespace Avalonia.Controls.UnitTests Assert.DoesNotContain(":empty", target.Classes); } + [Fact] + public void Empty_Class_Should_Be_Set_When_Items_Not_Set() + { + var target = new ItemsControl() + { + Template = GetTemplate(), + }; + + Assert.Contains(":empty", target.Classes); + } + [Fact] public void Empty_Class_Should_Be_Set_When_Empty_Collection_Set() { @@ -444,7 +455,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Empty_Class_Should_Not_Be_Set_When_Items_Collection_Count_Increases() { - var items = new ObservableCollection() {}; + var items = new ObservableCollection() { }; var target = new ItemsControl() { @@ -457,6 +468,54 @@ namespace Avalonia.Controls.UnitTests Assert.DoesNotContain(":empty", target.Classes); } + [Fact] + public void Single_Item_Class_Should_Be_Set_When_Items_Collection_Count_Increases_To_One() + { + var items = new ObservableCollection() { }; + + var target = new ItemsControl() + { + Template = GetTemplate(), + Items = items, + }; + + items.Add(1); + + Assert.Contains(":singleitem", target.Classes); + } + + [Fact] + public void Empty_Class_Should_Not_Be_Set_When_Items_Collection_Cleared() + { + var items = new ObservableCollection() { 1, 2, 3 }; + + var target = new ItemsControl() + { + Template = GetTemplate(), + Items = items, + }; + + items.Clear(); + + Assert.DoesNotContain(":singleitem", target.Classes); + } + + [Fact] + public void Single_Item_Class_Should_Not_Be_Set_When_Items_Collection_Count_Increases_Beyond_One() + { + var items = new ObservableCollection() { 1 }; + + var target = new ItemsControl() + { + Template = GetTemplate(), + Items = items, + }; + + items.Add(2); + + Assert.DoesNotContain(":singleitem", target.Classes); + } + [Fact] public void Setting_Presenter_Explicitly_Should_Set_Item_Parent() { From 5265b0592a86b3e36df1e82d7e494518b5b7f1df Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 29 Apr 2021 17:10:33 +0200 Subject: [PATCH 08/13] Implement MenuItem.StaysOpenOnClick. --- .../ControlCatalog/Pages/ContextMenuPage.xaml | 1 + src/Avalonia.Controls/IMenuItem.cs | 6 ++++++ src/Avalonia.Controls/MenuItem.cs | 16 ++++++++++++++++ .../Platform/DefaultMenuInteractionHandler.cs | 6 +++++- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml index 260162ddb9..25f305151d 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml @@ -31,6 +31,7 @@ + diff --git a/src/Avalonia.Controls/IMenuItem.cs b/src/Avalonia.Controls/IMenuItem.cs index 94d761f725..d92625218a 100644 --- a/src/Avalonia.Controls/IMenuItem.cs +++ b/src/Avalonia.Controls/IMenuItem.cs @@ -23,6 +23,12 @@ namespace Avalonia.Controls /// bool IsSubMenuOpen { get; set; } + /// + /// Gets or sets a value that indicates the submenu that this is + /// within should not close when this item is clicked. + /// + bool StaysOpenOnClick { get; set; } + /// /// Gets a value that indicates whether the is a top-level main menu item. /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 94099a970e..0995ce0e0b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -69,6 +69,12 @@ namespace Avalonia.Controls public static readonly StyledProperty IsSubMenuOpenProperty = AvaloniaProperty.Register(nameof(IsSubMenuOpen)); + /// + /// Defines the property. + /// + public static readonly StyledProperty StaysOpenOnClickProperty = + AvaloniaProperty.Register(nameof(StaysOpenOnClick)); + /// /// Defines the event. /// @@ -265,6 +271,16 @@ namespace Avalonia.Controls set { SetValue(IsSubMenuOpenProperty, value); } } + /// + /// Gets or sets a value that indicates the submenu that this is + /// within should not close when this item is clicked. + /// + public bool StaysOpenOnClick + { + get { return GetValue(StaysOpenOnClickProperty); } + set { SetValue(StaysOpenOnClickProperty, value); } + } + /// /// Gets or sets a value that indicates whether the has a submenu. /// diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index e3e9e84d7e..984faa4d60 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -449,7 +449,11 @@ namespace Avalonia.Controls.Platform protected void Click(IMenuItem item) { item.RaiseClick(); - CloseMenu(item); + + if (!item.StaysOpenOnClick) + { + CloseMenu(item); + } } protected void CloseMenu(IMenuItem item) From 2635a8a7ef42707bcd55dafc980f8d7818b0ef0a Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 29 Apr 2021 17:13:48 +0200 Subject: [PATCH 09/13] Fix copy pasta. --- src/Avalonia.Controls/MenuItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 0995ce0e0b..4c801c2e06 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -70,7 +70,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(IsSubMenuOpen)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty StaysOpenOnClickProperty = AvaloniaProperty.Register(nameof(StaysOpenOnClick)); From d9510f77efeb3b28d1f4be682f707a7dbac0db44 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 30 Apr 2021 10:44:45 +0200 Subject: [PATCH 10/13] Update ApiCompatBaseline.txt --- src/Avalonia.Controls/ApiCompatBaseline.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 3a6810eed9..7199c15d21 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -1,4 +1,7 @@ Compat issues with assembly Avalonia.Controls: +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.IMenuItem.StaysOpenOnClick.set(System.Boolean)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. @@ -7,4 +10,4 @@ EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -Total Issues: 7 +Total Issues: 11 From 04ae8ee43fb6016baef9ca8615025d08d5d97fb1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 30 Apr 2021 12:10:30 +0200 Subject: [PATCH 11/13] Added failing property batch update tests. --- .../AvaloniaObjectTests_BatchUpdate.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs index 53ad87421e..01d5752ead 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs @@ -5,6 +5,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Text; using Avalonia.Data; +using Avalonia.Layout; using Xunit; namespace Avalonia.Base.UnitTests @@ -104,6 +105,25 @@ namespace Avalonia.Base.UnitTests Assert.Equal("baz", target.Foo); } + [Fact] + public void SetValue_Change_Should_Be_Raised_After_Batch_Update_3() + { + var target = new TestClass(); + var raised = new List(); + + target.PropertyChanged += (s, e) => raised.Add(e); + + target.BeginBatchUpdate(); + target.SetValue(TestClass.BazProperty, Orientation.Horizontal, BindingPriority.LocalValue); + target.EndBatchUpdate(); + + Assert.Equal(1, raised.Count); + Assert.Equal(TestClass.BazProperty, raised[0].Property); + Assert.Equal(Orientation.Vertical, raised[0].OldValue); + Assert.Equal(Orientation.Horizontal, raised[0].NewValue); + Assert.Equal(Orientation.Horizontal, target.Baz); + } + [Fact] public void SetValue_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update() { @@ -234,6 +254,26 @@ namespace Avalonia.Base.UnitTests Assert.Equal("baz", raised[0].NewValue); } + [Fact] + public void Binding_Change_Should_Be_Raised_After_Batch_Update_3() + { + var target = new TestClass(); + var observable = new TestObservable(Orientation.Horizontal); + var raised = new List(); + + target.PropertyChanged += (s, e) => raised.Add(e); + + target.BeginBatchUpdate(); + target.Bind(TestClass.BazProperty, observable, BindingPriority.LocalValue); + target.EndBatchUpdate(); + + Assert.Equal(1, raised.Count); + Assert.Equal(TestClass.BazProperty, raised[0].Property); + Assert.Equal(Orientation.Vertical, raised[0].OldValue); + Assert.Equal(Orientation.Horizontal, raised[0].NewValue); + Assert.Equal(Orientation.Horizontal, target.Baz); + } + [Fact] public void Binding_Completion_Should_Be_Raised_After_Batch_Update() { @@ -579,6 +619,9 @@ namespace Avalonia.Base.UnitTests public static readonly StyledProperty BarProperty = AvaloniaProperty.Register(nameof(Bar)); + public static readonly StyledProperty BazProperty = + AvaloniaProperty.Register(nameof(Bar), Orientation.Vertical); + public string Foo { get => GetValue(FooProperty); @@ -590,6 +633,12 @@ namespace Avalonia.Base.UnitTests get => GetValue(BarProperty); set => SetValue(BarProperty, value); } + + public Orientation Baz + { + get => GetValue(BazProperty); + set => SetValue(BazProperty, value); + } } public class TestObservable : ObservableBase> From 8959d55418132248e2c74d11b7d2c37d8e0e2b20 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 30 Apr 2021 12:13:12 +0200 Subject: [PATCH 12/13] Correctly handle default values in batch update notifications. We were previously calling `Optional.GetValueOrDefault()` in order to convert an `Optional` to an `Optional` which meant that empty values were getting converted to default values, meaning that in certain cases change notifications weren't raised. Add an `Optional.Cast()` methods (like we have in `BindingValue`) and use that in order to preserve empty values when ending a batch update operation. --- src/Avalonia.Base/Data/Optional.cs | 14 ++++++++++++++ src/Avalonia.Base/PropertyStore/BindingEntry.cs | 4 ++-- .../PropertyStore/ConstantValueEntry.cs | 4 ++-- src/Avalonia.Base/PropertyStore/LocalValueEntry.cs | 4 ++-- src/Avalonia.Base/PropertyStore/PriorityValue.cs | 4 ++-- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index 8e044d7896..9dec399e35 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -153,4 +153,18 @@ namespace Avalonia.Data /// public static Optional Empty => default; } + + public static class OptionalExtensions + { + /// + /// Casts the type of an using only the C# cast operator. + /// + /// The target type. + /// The binding value. + /// The cast value. + public static Optional Cast(this Optional value) + { + return value.HasValue ? new Optional((T)value.Value) : Optional.Empty; + } + } } diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 3e17a81dd8..1b29338f07 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -127,8 +127,8 @@ namespace Avalonia.PropertyStore sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( owner, (AvaloniaProperty)property, - oldValue.GetValueOrDefault(), - newValue.GetValueOrDefault(), + oldValue.Cast(), + newValue.Cast(), Priority)); } diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index d39fc3bb1e..dc4a1d88c1 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -65,8 +65,8 @@ namespace Avalonia.PropertyStore sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( owner, (AvaloniaProperty)property, - oldValue.GetValueOrDefault(), - newValue.GetValueOrDefault(), + oldValue.Cast(), + newValue.Cast(), Priority)); } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index f49b74f4a8..8fe2ad7794 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -36,8 +36,8 @@ namespace Avalonia.PropertyStore sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( owner, (AvaloniaProperty)property, - oldValue.GetValueOrDefault(), - newValue.GetValueOrDefault(), + oldValue.Cast(), + newValue.Cast(), BindingPriority.LocalValue)); } } diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 80496fc045..556f1a6269 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -197,8 +197,8 @@ namespace Avalonia.PropertyStore sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( owner, (AvaloniaProperty)property, - oldValue.GetValueOrDefault(), - newValue.GetValueOrDefault(), + oldValue.Cast(), + newValue.Cast(), Priority)); } From 9bc9cd46fefe4ce0b800296c608e53e126e014a3 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 30 Apr 2021 15:18:55 +0200 Subject: [PATCH 13/13] Include the TextLine's Start offset to the TextLayout's size calculation. --- src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index ef427b0cd9..a4c9392fa0 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -305,9 +305,11 @@ namespace Avalonia.Media.TextFormatting /// The current height. private static void UpdateBounds(TextLine textLine, ref double width, ref double height) { - if (width < textLine.Width) + var lineWidth = textLine.Width + textLine.Start * 2; + + if (width < lineWidth) { - width = textLine.Width; + width = lineWidth; } height += textLine.Height;