Browse Source

Merge branch 'master' into lookless-file-dialogs

Max Katz 4 years ago
parent
commit
bccb341efb

+ 1 - 0
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@@ -31,6 +31,7 @@
                                 <CheckBox BorderThickness="0" IsHitTestVisible="False" IsChecked="True"/>
                             </MenuItem.Icon>
                         </MenuItem>
+                        <MenuItem Header="Menu Item that won't close on click" StaysOpenOnClick="True" />
                     </ContextMenu>
                 </Border.ContextMenu>
                 <TextBlock Text="Defined in XAML"/>

+ 14 - 0
src/Avalonia.Base/Data/Optional.cs

@@ -153,4 +153,18 @@ namespace Avalonia.Data
         /// </summary>
         public static Optional<T> Empty => default;
     }
+
+    public static class OptionalExtensions
+    {
+        /// <summary>
+        /// Casts the type of an <see cref="Optional{T}"/> using only the C# cast operator.
+        /// </summary>
+        /// <typeparam name="T">The target type.</typeparam>
+        /// <param name="value">The binding value.</param>
+        /// <returns>The cast value.</returns>
+        public static Optional<T> Cast<T>(this Optional<object> value)
+        {
+            return value.HasValue ? new Optional<T>((T)value.Value) : Optional<T>.Empty;
+        }
+    }
 }

+ 2 - 2
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@@ -127,8 +127,8 @@ namespace Avalonia.PropertyStore
             sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
                 owner,
                 (AvaloniaProperty<T>)property,
-                oldValue.GetValueOrDefault<T>(),
-                newValue.GetValueOrDefault<T>(),
+                oldValue.Cast<T>(),
+                newValue.Cast<T>(),
                 Priority));
         }
 

+ 2 - 2
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@@ -65,8 +65,8 @@ namespace Avalonia.PropertyStore
             sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
                 owner,
                 (AvaloniaProperty<T>)property,
-                oldValue.GetValueOrDefault<T>(),
-                newValue.GetValueOrDefault<T>(),
+                oldValue.Cast<T>(),
+                newValue.Cast<T>(),
                 Priority));
         }
     }

+ 2 - 2
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@@ -36,8 +36,8 @@ namespace Avalonia.PropertyStore
             sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
                 owner,
                 (AvaloniaProperty<T>)property,
-                oldValue.GetValueOrDefault<T>(),
-                newValue.GetValueOrDefault<T>(),
+                oldValue.Cast<T>(),
+                newValue.Cast<T>(),
                 BindingPriority.LocalValue));
         }
     }

+ 2 - 2
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@@ -197,8 +197,8 @@ namespace Avalonia.PropertyStore
             sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
                 owner,
                 (AvaloniaProperty<T>)property,
-                oldValue.GetValueOrDefault<T>(),
-                newValue.GetValueOrDefault<T>(),
+                oldValue.Cast<T>(),
+                newValue.Cast<T>(),
                 Priority));
         }
 

+ 37 - 0
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<object> _comparer;
+
+        public IComparer SourceComparer => _innerComparer;
+        public override IComparer<object> Comparer => _comparer;
+        public override ListSortDirection Direction => _direction;
+        public DataGridComparerSortDesctiption(IComparer comparer, ListSortDirection direction)
+        {
+            _innerComparer = comparer;
+            _direction = direction;
+            _comparer = Comparer<object>.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<DataGridSortDescription>

+ 1 - 1
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);
         }

+ 16 - 0
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@@ -1009,6 +1009,14 @@ namespace Avalonia.Controls
             get;
             set;
         }
+        /// <summary>
+        /// Holds a Comparer to use for sorting, if not using the default.
+        /// </summary>
+        public System.Collections.IComparer CustomSortComparer
+        {
+            get;
+            set;
+        }
 
         /// <summary>
         /// 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<DataGridComparerSortDesctiption>()
+                                  .FirstOrDefault(s => s.SourceComparer == CustomSortComparer);
+                }
+
                 string propertyName = GetSortPropertyName();
 
                 return OwningGrid.DataConnection.SortDescriptions.FirstOrDefault(s => s.HasPropertyPath && s.PropertyPath == propertyName);

+ 6 - 0
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();

+ 4 - 1
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

+ 1 - 1
src/Avalonia.Controls/AutoCompleteBox.cs

@@ -31,7 +31,6 @@ namespace Avalonia.Controls
     /// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populated" />
     /// event.
     /// </summary>
-    [PseudoClasses(":dropdownopen")]
     public class PopulatedEventArgs : EventArgs
     {
         /// <summary>
@@ -253,6 +252,7 @@ namespace Avalonia.Controls
     /// drop-down that contains possible matches based on the input in the text
     /// box.
     /// </summary>
+    [PseudoClasses(":dropdownopen")]
     public class AutoCompleteBox : TemplatedControl
     {
         /// <summary>

+ 6 - 0
src/Avalonia.Controls/IMenuItem.cs

@@ -23,6 +23,12 @@ namespace Avalonia.Controls
         /// </summary>
         bool IsSubMenuOpen { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value that indicates the submenu that this <see cref="MenuItem"/> is
+        /// within should not close when this item is clicked.
+        /// </summary>
+        bool StaysOpenOnClick { get; set; }
+
         /// <summary>
         /// Gets a value that indicates whether the <see cref="MenuItem"/> is a top-level main menu item.
         /// </summary>

+ 17 - 8
src/Avalonia.Controls/ItemsControl.cs

@@ -70,7 +70,7 @@ namespace Avalonia.Controls
         /// </summary>
         public ItemsControl()
         {
-            PseudoClasses.Add(":empty");
+            UpdatePseudoClasses(0);
             SubscribeToItems(_items);
         }
 
@@ -323,6 +323,16 @@ namespace Avalonia.Controls
             base.OnKeyDown(e);
         }
 
+        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == ItemCountProperty)
+            {
+                UpdatePseudoClasses(change.NewValue.GetValueOrDefault<int>());
+            }
+        }
+
         /// <summary>
         /// Called when the <see cref="Items"/> property changes.
         /// </summary>
@@ -371,10 +381,6 @@ 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);
         }
 
         /// <summary>
@@ -431,9 +437,6 @@ namespace Avalonia.Controls
         /// <param name="items">The items collection.</param>
         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 +472,12 @@ namespace Avalonia.Controls
             }
         }
 
+        private void UpdatePseudoClasses(int itemCount)
+        {
+            PseudoClasses.Set(":empty", itemCount == 0);
+            PseudoClasses.Set(":singleitem", itemCount == 1);
+        }
+
         protected static IInputElement GetNextControl(
             INavigableContainer container,
             NavigationDirection direction,

+ 16 - 0
src/Avalonia.Controls/MenuItem.cs

@@ -69,6 +69,12 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<bool> IsSubMenuOpenProperty =
             AvaloniaProperty.Register<MenuItem, bool>(nameof(IsSubMenuOpen));
 
+        /// <summary>
+        /// Defines the <see cref="StaysOpenOnClick"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> StaysOpenOnClickProperty =
+            AvaloniaProperty.Register<MenuItem, bool>(nameof(StaysOpenOnClick));
+
         /// <summary>
         /// Defines the <see cref="Click"/> event.
         /// </summary>
@@ -265,6 +271,16 @@ namespace Avalonia.Controls
             set { SetValue(IsSubMenuOpenProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets a value that indicates the submenu that this <see cref="MenuItem"/> is
+        /// within should not close when this item is clicked.
+        /// </summary>
+        public bool StaysOpenOnClick
+        {
+            get { return GetValue(StaysOpenOnClickProperty); }
+            set { SetValue(StaysOpenOnClickProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets a value that indicates whether the <see cref="MenuItem"/> has a submenu.
         /// </summary>

+ 5 - 1
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)

+ 4 - 8
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,

+ 4 - 2
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@@ -305,9 +305,11 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="height">The current height.</param>
         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;

+ 49 - 0
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<AvaloniaPropertyChangedEventArgs>();
+
+            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>(Orientation.Horizontal);
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            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<string> BarProperty =
                 AvaloniaProperty.Register<TestClass, string>(nameof(Bar));
 
+            public static readonly StyledProperty<Orientation> BazProperty =
+                AvaloniaProperty.Register<TestClass, Orientation>(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<T> : ObservableBase<BindingValue<T>>

+ 123 - 0
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()
         {
@@ -393,6 +404,118 @@ 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<int>() { 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<int>() { 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<int>() { };
+
+            var target = new ItemsControl()
+            {
+                Template = GetTemplate(),
+                Items = items,
+            };
+
+            items.Add(1);
+
+            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<int>() { };
+
+            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<int>() { 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<int>() { 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()
         {

+ 1 - 1
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(