Browse Source

Merge branch 'master' into lookless-file-dialogs

pull/4615/head
Max Katz 5 years ago
committed by GitHub
parent
commit
bccb341efb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  2. 14
      src/Avalonia.Base/Data/Optional.cs
  3. 4
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  4. 4
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  5. 4
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  6. 4
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  7. 37
      src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
  8. 2
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  9. 16
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  10. 6
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  11. 5
      src/Avalonia.Controls/ApiCompatBaseline.txt
  12. 2
      src/Avalonia.Controls/AutoCompleteBox.cs
  13. 6
      src/Avalonia.Controls/IMenuItem.cs
  14. 25
      src/Avalonia.Controls/ItemsControl.cs
  15. 16
      src/Avalonia.Controls/MenuItem.cs
  16. 6
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  17. 12
      src/Avalonia.Controls/TextBox.cs
  18. 6
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  19. 49
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  20. 123
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  21. 2
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

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

4
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));
}

4
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));
}
}

4
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));
}
}

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

2
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
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
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();

5
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

2
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
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>

25
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
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>

6
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)

12
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,

6
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
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
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()
{

2
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(

Loading…
Cancel
Save