diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 20ca291910..c7a75f5a70 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -1,10 +1,8 @@ - - - - + + - + + + + diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs index 1d0c228a0e..5e555c8c91 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs @@ -5,5 +5,25 @@ namespace ControlCatalog.Pages { public class ButtonPage : UserControl { + private int repeatButtonClickCount = 0; + + public ButtonPage() + { + InitializeComponent(); + + this.FindControl("RepeatButton").Click += OnRepeatButtonClick; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public void OnRepeatButtonClick(object sender, object args) + { + repeatButtonClickCount++; + var textBlock = this.FindControl("RepeatButtonTextBlock"); + textBlock.Text = $"Repeat Button: {repeatButtonClickCount}"; + } } } diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index 58f7b881fe..c6f5521e60 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -9,12 +9,14 @@ diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index 6019d5f91f..789b45e62c 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -20,6 +20,7 @@ + Single diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs index d396ef2b3d..5bc23e2fe5 100644 --- a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs @@ -23,12 +23,14 @@ namespace ControlCatalog.ViewModels AddItemCommand = ReactiveCommand.Create(AddItem); RemoveItemCommand = ReactiveCommand.Create(RemoveItem); + SelectRandomItemCommand = ReactiveCommand.Create(SelectRandomItem); } public ObservableCollection Items { get; } public SelectionModel Selection { get; } public ReactiveCommand AddItemCommand { get; } public ReactiveCommand RemoveItemCommand { get; } + public ReactiveCommand SelectRandomItemCommand { get; } public SelectionMode SelectionMode { @@ -74,6 +76,15 @@ namespace ControlCatalog.ViewModels } } + private void SelectRandomItem() + { + var random = new Random(); + var depth = random.Next(4); + var indexes = Enumerable.Range(0, 4).Select(x => random.Next(10)); + var path = new IndexPath(indexes); + Selection.SelectedIndex = path; + } + private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) { var selected = string.Join(",", e.SelectedIndices); diff --git a/src/Avalonia.Controls/IndexPath.cs b/src/Avalonia.Controls/IndexPath.cs index 6c5aaf7ad1..73b75bc23d 100644 --- a/src/Avalonia.Controls/IndexPath.cs +++ b/src/Avalonia.Controls/IndexPath.cs @@ -123,6 +123,26 @@ namespace Avalonia.Controls } } + public bool IsAncestorOf(in IndexPath other) + { + if (other.GetSize() <= GetSize()) + { + return false; + } + + var size = GetSize(); + + for (int i = 0; i < size; i++) + { + if (GetAt(i) != other.GetAt(i)) + { + return false; + } + } + + return true; + } + public override string ToString() { if (_path != null) diff --git a/src/Avalonia.Controls/IndexRange.cs b/src/Avalonia.Controls/IndexRange.cs index 1dc161c699..e45d013af4 100644 --- a/src/Avalonia.Controls/IndexRange.cs +++ b/src/Avalonia.Controls/IndexRange.cs @@ -132,6 +132,53 @@ namespace Avalonia.Controls return result; } + public static int Intersect( + IList ranges, + IndexRange range, + IList? removed = null) + { + var result = 0; + + for (var i = 0; i < ranges.Count && range != s_invalid; ++i) + { + var existing = ranges[i]; + + if (existing.End < range.Begin || existing.Begin > range.End) + { + removed?.Add(existing); + ranges.RemoveAt(i--); + result += existing.Count; + } + else + { + if (existing.Begin < range.Begin) + { + var except = new IndexRange(existing.Begin, range.Begin - 1); + removed?.Add(except); + ranges[i] = existing = new IndexRange(range.Begin, existing.End); + result += except.Count; + } + + if (existing.End > range.End) + { + var except = new IndexRange(range.End + 1, existing.End); + removed?.Add(except); + ranges[i] = new IndexRange(existing.Begin, range.End); + result += except.Count; + } + } + } + + MergeRanges(ranges); + + if (removed is object) + { + MergeRanges(removed); + } + + return result; + } + public static int Remove( IList ranges, IndexRange range, diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index e104a8a664..1db47a13e7 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -41,13 +41,16 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty IsDirectionReversedProperty = AvaloniaProperty.Register(nameof(IsDirectionReversed)); + public static readonly StyledProperty IgnoreThumbDragProperty = + AvaloniaProperty.Register(nameof(IsThumbDragHandled)); + private double _minimum; private double _maximum = 100.0; private double _value; static Track() { - ThumbProperty.Changed.AddClassHandler((x,e) => x.ThumbChanged(e)); + ThumbProperty.Changed.AddClassHandler((x, e) => x.ThumbChanged(e)); IncreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); DecreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); @@ -113,6 +116,12 @@ namespace Avalonia.Controls.Primitives set { SetValue(IsDirectionReversedProperty, value); } } + public bool IsThumbDragHandled + { + get { return GetValue(IgnoreThumbDragProperty); } + set { SetValue(IgnoreThumbDragProperty, value); } + } + private double ThumbCenterOffset { get; set; } private double Density { get; set; } @@ -422,6 +431,9 @@ namespace Avalonia.Controls.Primitives private void ThumbDragged(object sender, VectorEventArgs e) { + if (IsThumbDragHandled) + return; + Value = MathUtilities.Clamp( Value + ValueFromDistance(e.Vector.X, e.Vector.Y), Minimum, diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index 93699583e6..ff1c0260bb 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -46,17 +46,25 @@ namespace Avalonia.Controls if (_rootNode.Source != null) { - if (_rootNode.Source != null) + // Temporarily prevent auto-select when switching source. + var restoreAutoSelect = _autoSelect; + _autoSelect = false; + + try { using (var operation = new Operation(this)) { ClearSelection(resetAnchor: true); } } + finally + { + _autoSelect = restoreAutoSelect; + } } _rootNode.Source = value; - ApplyAutoSelect(); + ApplyAutoSelect(true); RaisePropertyChanged("Source"); @@ -114,7 +122,7 @@ namespace Avalonia.Controls if (_autoSelect != value) { _autoSelect = value; - ApplyAutoSelect(); + ApplyAutoSelect(true); } } } @@ -133,7 +141,7 @@ namespace Avalonia.Controls while (current?.AnchorIndex >= 0) { path.Add(current.AnchorIndex); - current = current.GetAt(current.AnchorIndex, false); + current = current.GetAt(current.AnchorIndex, false, default); } anchor = new IndexPath(path); @@ -188,7 +196,6 @@ namespace Avalonia.Controls using var operation = new Operation(this); ClearSelection(resetAnchor: true); SelectWithPathImpl(value, select: true); - ApplyAutoSelect(); } } } @@ -384,21 +391,18 @@ namespace Avalonia.Controls { using var operation = new Operation(this); SelectImpl(index, select: false); - ApplyAutoSelect(); } public void Deselect(int groupIndex, int itemIndex) { using var operation = new Operation(this); SelectWithGroupImpl(groupIndex, itemIndex, select: false); - ApplyAutoSelect(); } public void DeselectAt(IndexPath index) { using var operation = new Operation(this); SelectWithPathImpl(index, select: false); - ApplyAutoSelect(); } public bool IsSelected(int index) => _rootNode.IsSelected(index); @@ -416,7 +420,7 @@ namespace Avalonia.Controls for (int i = 0; i < path.GetSize() - 1; i++) { var childIndex = path.GetAt(i); - node = node.GetAt(childIndex, realizeChild: false); + node = node.GetAt(childIndex, false, default); if (node == null) { @@ -451,7 +455,7 @@ namespace Avalonia.Controls } var isSelected = (bool?)false; - var childNode = _rootNode.GetAt(groupIndex, realizeChild: false); + var childNode = _rootNode.GetAt(groupIndex, false, default); if (childNode != null) { @@ -470,7 +474,7 @@ namespace Avalonia.Controls for (int i = 0; i < path.GetSize() - 1; i++) { var childIndex = path.GetAt(i); - node = node.GetAt(childIndex, realizeChild: false); + node = node.GetAt(childIndex, false, default); if (node == null) { @@ -565,7 +569,6 @@ namespace Avalonia.Controls { using var operation = new Operation(this); ClearSelection(resetAnchor: true); - ApplyAutoSelect(); } public IDisposable Update() => new Operation(this); @@ -592,10 +595,13 @@ namespace Avalonia.Controls } OnSelectionChanged(e); - ApplyAutoSelect(); + ApplyAutoSelect(true); } - internal IObservable? ResolvePath(object data, IndexPath dataIndexPath) + internal IObservable? ResolvePath( + object data, + IndexPath dataIndexPath, + IndexPath finalIndexPath) { IObservable? resolved = null; @@ -604,18 +610,22 @@ namespace Avalonia.Controls { if (_childrenRequestedEventArgs == null) { - _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(data, dataIndexPath, false); + _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs( + data, + dataIndexPath, + finalIndexPath, + false); } else { - _childrenRequestedEventArgs.Initialize(data, dataIndexPath, false); + _childrenRequestedEventArgs.Initialize(data, dataIndexPath, finalIndexPath, false); } ChildrenRequested(this, _childrenRequestedEventArgs); resolved = _childrenRequestedEventArgs.Children; // Clear out the values in the args so that it cannot be used after the event handler call. - _childrenRequestedEventArgs.Initialize(null, default, true); + _childrenRequestedEventArgs.Initialize(null, default, default, true); } return resolved; @@ -632,6 +642,8 @@ namespace Avalonia.Controls { AnchorIndex = default; } + + OnSelectionChanged(); } private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null) @@ -667,6 +679,8 @@ namespace Avalonia.Controls { AnchorIndex = new IndexPath(index); } + + OnSelectionChanged(); } private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select) @@ -676,13 +690,15 @@ namespace Avalonia.Controls ClearSelection(resetAnchor: true); } - var childNode = _rootNode.GetAt(groupIndex, realizeChild: true); + var childNode = _rootNode.GetAt(groupIndex, true, new IndexPath(groupIndex, itemIndex)); var selected = childNode!.Select(itemIndex, select); if (selected) { AnchorIndex = new IndexPath(groupIndex, itemIndex); } + + OnSelectionChanged(); } private void SelectWithPathImpl(IndexPath index, bool select) @@ -711,6 +727,8 @@ namespace Avalonia.Controls { AnchorIndex = index; } + + OnSelectionChanged(); } private void SelectRangeFromAnchorImpl(int index, bool select) @@ -724,6 +742,7 @@ namespace Avalonia.Controls } _rootNode.SelectRange(new IndexRange(anchorIndex, index), select); + OnSelectionChanged(); } private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select) @@ -752,11 +771,13 @@ namespace Avalonia.Controls for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++) { - var groupNode = _rootNode.GetAt(groupIdx, realizeChild: true)!; + var groupNode = _rootNode.GetAt(groupIdx, true, new IndexPath(endGroupIndex, endItemIndex))!; int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0; int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1; groupNode.SelectRange(new IndexRange(startIndex, endIndex), select); } + + OnSelectionChanged(); } private void SelectRangeImpl(IndexPath start, IndexPath end, bool select) @@ -784,6 +805,8 @@ namespace Avalonia.Controls info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select); } }); + + OnSelectionChanged(); } private void BeginOperation() @@ -806,6 +829,8 @@ namespace Avalonia.Controls if (--_operationCount == 0) { + ApplyAutoSelect(false); + var changes = new List(); _rootNode.EndOperation(changes); @@ -827,7 +852,7 @@ namespace Avalonia.Controls } } - private void ApplyAutoSelect() + private void ApplyAutoSelect(bool createOperation) { if (AutoSelect) { @@ -835,8 +860,15 @@ namespace Avalonia.Controls if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0) { - using var operation = new Operation(this); - SelectImpl(0, true); + if (createOperation) + { + using var operation = new Operation(this); + SelectImpl(0, true); + } + else + { + SelectImpl(0, true); + } } } } diff --git a/src/Avalonia.Controls/SelectionModelChangeSet.cs b/src/Avalonia.Controls/SelectionModelChangeSet.cs index 6e77dc5755..d1df38656a 100644 --- a/src/Avalonia.Controls/SelectionModelChangeSet.cs +++ b/src/Avalonia.Controls/SelectionModelChangeSet.cs @@ -135,7 +135,7 @@ namespace Avalonia.Controls if (index >= currentIndex && index < currentIndex + currentCount) { int targetIndex = GetIndexAt(getRanges(info), index - currentIndex); - item = info.Items?.GetAt(targetIndex); + item = info.Items?.Count > targetIndex ? info.Items?.GetAt(targetIndex) : null; break; } diff --git a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs index 974da0cf71..b1f3e0b2c4 100644 --- a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs +++ b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs @@ -16,15 +16,17 @@ namespace Avalonia.Controls { private object? _source; private IndexPath _sourceIndexPath; + private IndexPath _finalIndexPath; private bool _throwOnAccess; internal SelectionModelChildrenRequestedEventArgs( object source, IndexPath sourceIndexPath, + IndexPath finalIndexPath, bool throwOnAccess) { source = source ?? throw new ArgumentNullException(nameof(source)); - Initialize(source, sourceIndexPath, throwOnAccess); + Initialize(source, sourceIndexPath, finalIndexPath, throwOnAccess); } /// @@ -65,9 +67,26 @@ namespace Avalonia.Controls } } + /// + /// Gets the index of the final object which is being attempted to be retrieved. + /// + public IndexPath FinalIndex + { + get + { + if (_throwOnAccess) + { + throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs)); + } + + return _finalIndexPath; + } + } + internal void Initialize( object? source, IndexPath sourceIndexPath, + IndexPath finalIndexPath, bool throwOnAccess) { if (!throwOnAccess && source == null) @@ -77,6 +96,7 @@ namespace Avalonia.Controls _source = source; _sourceIndexPath = sourceIndexPath; + _finalIndexPath = finalIndexPath; _throwOnAccess = throwOnAccess; } } diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs index 0b00db88c3..d99606673e 100644 --- a/src/Avalonia.Controls/SelectionNode.cs +++ b/src/Avalonia.Controls/SelectionNode.cs @@ -101,6 +101,7 @@ namespace Avalonia.Controls ItemsSourceView = newDataSource; + TrimInvalidSelections(); PopulateSelectedItemsFromSelectedIndices(); HookupCollectionChangedHandler(); OnSelectionChanged(); @@ -108,6 +109,26 @@ namespace Avalonia.Controls } } + private void TrimInvalidSelections() + { + if (_selected == null || ItemsSourceView == null) + { + return; + } + + var validRange = ItemsSourceView.Count > 0 ? new IndexRange(0, ItemsSourceView.Count - 1) : new IndexRange(-1, -1); + var removed = new List(); + var removedCount = IndexRange.Intersect(_selected, validRange, removed); + + if (removedCount > 0) + { + using var operation = _manager.Update(); + SelectedCount -= removedCount; + OnSelectionChanged(); + _operation!.Deselected(removed); + } + } + public ItemsSourceView? ItemsSourceView { get; private set; } public int DataCount => ItemsSourceView?.Count ?? 0; public int ChildrenNodeCount => _childrenNodes.Count; @@ -141,7 +162,7 @@ namespace Avalonia.Controls // create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid // an explosion of node objects. However, I'm still creating the m_childrenNodes // collection unfortunately. - public SelectionNode? GetAt(int index, bool realizeChild) + public SelectionNode? GetAt(int index, bool realizeChild, IndexPath finalIndexPath) { SelectionNode? child = null; @@ -171,7 +192,7 @@ namespace Avalonia.Controls if (childData != null) { var childDataIndexPath = IndexPath.CloneWithChildIndex(index); - resolver = _manager.ResolvePath(childData, childDataIndexPath); + resolver = _manager.ResolvePath(childData, childDataIndexPath, finalIndexPath); } if (resolver != null) @@ -843,7 +864,7 @@ namespace Avalonia.Controls int notSelectedCount = 0; for (int i = 0; i < ChildrenNodeCount; i++) { - var child = GetAt(i, realizeChild: false); + var child = GetAt(i, false, default); if (child != null) { diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index e92c8faf20..ec23bfa396 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,12 +1,40 @@ using System; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; -using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; +using Avalonia.Utilities; namespace Avalonia.Controls { + + /// + /// Enum which describes how to position the ticks in a . + /// + public enum TickPlacement + { + /// + /// No tick marks will appear. + /// + None, + + /// + /// Tick marks will appear above the track for a horizontal , or to the left of the track for a vertical . + /// + TopLeft, + + /// + /// Tick marks will appear below the track for a horizontal , or to the right of the track for a vertical . + /// + BottomRight, + + /// + /// Tick marks appear on both sides of either a horizontal or vertical . + /// + Outside + } + /// /// A control that lets the user select from a range of values by moving a Thumb control along a Track. /// @@ -30,19 +58,31 @@ namespace Avalonia.Controls public static readonly StyledProperty TickFrequencyProperty = AvaloniaProperty.Register(nameof(TickFrequency), 0.0); + /// + /// Defines the property. + /// + public static readonly StyledProperty TickPlacementProperty = + AvaloniaProperty.Register(nameof(TickPlacement), 0d); + // Slider required parts + private bool _isDragging = false; private Track _track; private Button _decreaseButton; private Button _increaseButton; + private IDisposable _decreaseButtonPressDispose; + private IDisposable _decreaseButtonReleaseDispose; + private IDisposable _increaseButtonSubscription; + private IDisposable _increaseButtonReleaseDispose; + private IDisposable _pointerMovedDispose; /// /// Initializes static members of the class. /// static Slider() { + PressedMixin.Attach(); OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal); Thumb.DragStartedEvent.AddClassHandler((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble); - Thumb.DragDeltaEvent.AddClassHandler((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble); } @@ -81,57 +121,88 @@ namespace Avalonia.Controls set { SetValue(TickFrequencyProperty, value); } } + /// + /// Gets or sets a value that indicates where to draw + /// tick marks in relation to the track. + /// + public TickPlacement TickPlacement + { + get { return GetValue(TickPlacementProperty); } + set { SetValue(TickPlacementProperty, value); } + } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - if (_decreaseButton != null) - { - _decreaseButton.Click -= DecreaseClick; - } - - if (_increaseButton != null) - { - _increaseButton.Click -= IncreaseClick; - } + base.OnApplyTemplate(e); + + _decreaseButtonPressDispose?.Dispose(); + _decreaseButtonReleaseDispose?.Dispose(); + _increaseButtonSubscription?.Dispose(); + _increaseButtonReleaseDispose?.Dispose(); + _pointerMovedDispose?.Dispose(); _decreaseButton = e.NameScope.Find - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 266acce971..a20f075e21 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -23,7 +23,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/RepeatButton.xaml deleted file mode 100644 index 702e4e6ebd..0000000000 --- a/src/Avalonia.Themes.Fluent/RepeatButton.xaml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index b21cbf3650..a57ea6cedd 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -1,93 +1,264 @@ - + + + + + + + + + + + + + + + + + 0,0,0,4 + 15 + 15 + 32 + 32 + 10 + 20 + 20 + 20 + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/src/Avalonia.Themes.Fluent/ToolTip.xaml b/src/Avalonia.Themes.Fluent/ToolTip.xaml index 1fc0202dd3..cf6f32f9bc 100644 --- a/src/Avalonia.Themes.Fluent/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/ToolTip.xaml @@ -1,17 +1,78 @@ - \ No newline at end of file + + + + + Hover Here + + + + + + ToolTip + A control which pops up a hint when a control is hovered + + + ToolTip bottom placement + + + + + + + + + + + + diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 9ab5882361..7aa1eed890 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -107,6 +107,14 @@ namespace Avalonia.Data private object ConvertValue(IList values, Type targetType, IMultiValueConverter converter) { + for (var i = 0; i < values.Count; ++i) + { + if (values[i] is BindingNotification notification) + { + values[i] = notification.Value; + } + } + var culture = CultureInfo.CurrentCulture; var converted = converter.Convert(values, targetType, ConverterParameter, culture); diff --git a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs index e0f46d9fa9..e01c752658 100644 --- a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs @@ -127,6 +127,88 @@ namespace Avalonia.Controls.UnitTests Assert.Empty(selected); } + [Fact] + public void Intersect_Should_Remove_Items_From_Beginning() + { + var ranges = new List { new IndexRange(0, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(2, 12), removed); + + Assert.Equal(2, result); + Assert.Equal(new[] { new IndexRange(2, 10) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 1) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Items_From_End() + { + var ranges = new List { new IndexRange(0, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(0, 8), removed); + + Assert.Equal(2, result); + Assert.Equal(new[] { new IndexRange(0, 8) }, ranges); + Assert.Equal(new[] { new IndexRange(9, 10) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_Range_Start() + { + var ranges = new List { new IndexRange(0, 5), new IndexRange(6, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(6, 10), removed); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(6, 10) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 5) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_Range_End() + { + var ranges = new List { new IndexRange(0, 5), new IndexRange(6, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(0, 4), removed); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(0, 4) }, ranges); + Assert.Equal(new[] { new IndexRange(5, 10) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_Range_Start_End() + { + var ranges = new List + { + new IndexRange(0, 2), + new IndexRange(3, 7), + new IndexRange(8, 10) + }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(3, 7), removed); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(3, 7) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 2), new IndexRange(8, 10) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_And_Partial_Range_Start_End() + { + var ranges = new List + { + new IndexRange(0, 2), + new IndexRange(3, 7), + new IndexRange(8, 10) + }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(4, 6), removed); + + Assert.Equal(8, result); + Assert.Equal(new[] { new IndexRange(4, 6) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 3), new IndexRange(7, 10) }, removed); + } + [Fact] public void Remove_Should_Remove_Entire_Range() { diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index ebf9c40012..24e82a69d0 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1512,6 +1512,47 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, raised); } + [Fact] + public void Batch_Update_Selection_Is_Correct_Throughout() + { + var data = new[] { "foo", "bar", "baz", "qux" }; + var target = new SelectionModel { Source = data }; + var raised = 0; + + using (target.Update()) + { + target.Select(1); + + Assert.Equal(new IndexPath(1), target.SelectedIndex); + Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + + target.Deselect(1); + + Assert.Equal(new IndexPath(), target.SelectedIndex); + Assert.Empty(target.SelectedIndices); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + + target.SelectRange(new IndexPath(1), new IndexPath(1)); + + Assert.Equal(new IndexPath(1), target.SelectedIndex); + Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + + target.ClearSelection(); + + Assert.Equal(new IndexPath(), target.SelectedIndex); + Assert.Empty(target.SelectedIndices); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + } + + Assert.Equal(0, raised); + } + [Fact] public void AutoSelect_Selects_When_Enabled() { @@ -1713,6 +1754,30 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, raised); } + [Fact] + public void AutoSelect_Is_Applied_At_End_Of_Batch_Update() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { AutoSelect = true, Source = data }; + + using (target.Update()) + { + target.ClearSelection(); + + Assert.Equal(new IndexPath(), target.SelectedIndex); + Assert.Empty(target.SelectedIndices); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + } + + Assert.Equal(new IndexPath(0), target.SelectedIndex); + Assert.Equal(new[] { new IndexPath(0) }, target.SelectedIndices); + Assert.Equal("foo", target.SelectedItem); + Assert.Equal(new[] { "foo" }, target.SelectedItems); + + Assert.Equal(new IndexPath(0), target.SelectedIndex); + } + [Fact] public void Can_Replace_Parent_Children_Collection() { @@ -1806,6 +1871,87 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, node.PropertyChangedSubscriptions); } + [Fact] + public void Setting_SelectedIndex_To_Minus_1_Clears_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { Source = data }; + target.SelectedIndex = new IndexPath(1); + target.SelectedIndex = new IndexPath(-1); + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Assigning_Source_With_Less_Items_Than_Previous_Clears_Selection() + { + var data = new[] { "foo", "bar", "baz", "boo", "hoo" }; + var smallerData = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.Source = data; + target.SelectedIndex = new IndexPath(4); + target.Source = smallerData; + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel(); + target.SelectedIndex = new IndexPath(4); + target.Source = data; + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection_RetainSelection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.SelectedIndex = new IndexPath(4); + target.Source = data; + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Multiple_Selection_Trims_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.Select(4); + target.Select(2); + target.Source = data; + Assert.Equal(1, target.SelectedIndices.Count); + Assert.Equal(new IndexPath(2), target.SelectedIndices.First()); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Selection_Raises_SelectionChanged() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel(); + var raised = 0; + + target.SelectedIndex = new IndexPath(4); + + target.SelectionChanged += (s, e) => + { + if (raised == 0) + { + Assert.Equal(new[] { Path(4) }, e.DeselectedIndices); + Assert.Equal(new object[] { null }, e.DeselectedItems); + Assert.Empty(e.SelectedIndices); + Assert.Empty(e.SelectedItems); + } + + ++raised; + }; + + target.Source = data; + + Assert.Equal(2, raised); + } + private int GetSubscriberCount(AvaloniaList list) { return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0; diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 24aacd4000..db9211ac3c 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; @@ -325,6 +327,28 @@ namespace Avalonia.Controls.UnitTests Assert.NotEqual(dataContext, tabItem.Content); } + [Fact] + public void Can_Have_Empty_Tab_Control() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new Markup.Xaml.AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var tabControl = window.FindControl("tabs"); + + tabControl.DataContext = new { Tabs = new List() }; + window.ApplyTemplate(); + + Assert.Equal(0, tabControl.Items.Count()); + } + } + private IControlTemplate TabControlTemplate() { return new FuncControlTemplate((parent, scope) => diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index dc5b574db7..34b37e7635 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -102,5 +102,76 @@ namespace Avalonia.Controls.UnitTests Assert.True(ToolTip.GetIsOpen(target)); } } + + [Fact] + public void Open_Class_Should_Not_Initially_Be_Added() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var toolTip = new ToolTip(); + var window = new Window(); + + var decorator = new Decorator() + { + [ToolTip.TipProperty] = toolTip + }; + + window.Content = decorator; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.Empty(toolTip.Classes); + } + } + + [Fact] + public void Setting_IsOpen_Should_Add_Open_Class() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var toolTip = new ToolTip(); + var window = new Window(); + + var decorator = new Decorator() + { + [ToolTip.TipProperty] = toolTip + }; + + window.Content = decorator; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + ToolTip.SetIsOpen(decorator, true); + + Assert.Equal(new[] { ":open" }, toolTip.Classes); + } + } + + [Fact] + public void Clearing_IsOpen_Should_Remove_Open_Class() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var toolTip = new ToolTip(); + var window = new Window(); + + var decorator = new Decorator() + { + [ToolTip.TipProperty] = toolTip + }; + + window.Content = decorator; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + ToolTip.SetIsOpen(decorator, true); + ToolTip.SetIsOpen(decorator, false); + + Assert.Empty(toolTip.Classes); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 373f3e6861..c1bd45bcad 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -118,313 +118,340 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Clicking_Item_Should_Select_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); - Assert.NotNull(container); + Assert.NotNull(container); - _mouse.Click(container); + _mouse.Click(container); - Assert.Equal(item, target.SelectedItem); - Assert.True(container.IsSelected); + Assert.Equal(item, target.SelectedItem); + Assert.True(container.IsSelected); + } } [Fact] public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); - Assert.NotNull(container); + Assert.NotNull(container); - target.SelectedItem = item; + target.SelectedItem = item; - Assert.True(container.IsSelected); + Assert.True(container.IsSelected); - _mouse.Click(container, modifiers: KeyModifiers.Control); + _mouse.Click(container, modifiers: KeyModifiers.Control); - Assert.Null(target.SelectedItem); - Assert.False(container.IsSelected); + Assert.Null(target.SelectedItem); + Assert.False(container.IsSelected); + } } [Fact] public void Clicking_WithControlModifier_Not_Selected_Item_Should_Select_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item1 = tree[0].Children[1].Children[0]; - var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); + var item1 = tree[0].Children[1].Children[0]; + var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); - var item2 = tree[0].Children[1]; - var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); + var item2 = tree[0].Children[1]; + var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); - Assert.NotNull(container1); - Assert.NotNull(container2); + Assert.NotNull(container1); + Assert.NotNull(container2); - target.SelectedItem = item1; + target.SelectedItem = item1; - Assert.True(container1.IsSelected); + Assert.True(container1.IsSelected); - _mouse.Click(container2, modifiers: KeyModifiers.Control); - - Assert.Equal(item2, target.SelectedItem); - Assert.False(container1.IsSelected); - Assert.True(container2.IsSelected); + _mouse.Click(container2, modifiers: KeyModifiers.Control); + + Assert.Equal(item2, target.SelectedItem); + Assert.False(container1.IsSelected); + Assert.True(container2.IsSelected); + } } [Fact] public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_And_Remove_From_SelectedItems() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var item1 = rootNode.Children[0]; - var item2 = rootNode.Children.Last(); + var item1 = rootNode.Children[0]; + var item2 = rootNode.Children.Last(); - var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); - var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); + var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); + var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); - ClickContainer(item1Container, KeyModifiers.Control); - Assert.True(item1Container.IsSelected); + ClickContainer(item1Container, KeyModifiers.Control); + Assert.True(item1Container.IsSelected); - ClickContainer(item2Container, KeyModifiers.Control); - Assert.True(item2Container.IsSelected); + ClickContainer(item2Container, KeyModifiers.Control); + Assert.True(item2Container.IsSelected); - Assert.Equal(new[] {item1, item2}, target.Selection.SelectedItems.OfType()); + Assert.Equal(new[] { item1, item2 }, target.Selection.SelectedItems.OfType()); - ClickContainer(item1Container, KeyModifiers.Control); - Assert.False(item1Container.IsSelected); + ClickContainer(item1Container, KeyModifiers.Control); + Assert.False(item1Container.IsSelected); - Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType()); + Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType()); + } } [Fact] public void Clicking_WithShiftModifier_DownDirection_Should_Select_Range_Of_Items() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children.Last(); + var from = rootNode.Children[0]; + var to = rootNode.Children.Last(); - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); + } } [Fact] public void Clicking_WithShiftModifier_UpDirection_Should_Select_Range_Of_Items() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); + } } [Fact] public void Clicking_First_Item_Of_SelectedItems_Should_Select_Only_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - foreach (var child in rootNode.Children) - { - if (child == from) + foreach (var child in rootNode.Children) { - continue; - } + if (child == from) + { + continue; + } - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child); + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child); - Assert.False(container.IsSelected); + Assert.False(container.IsSelected); + } } } [Fact] public void Setting_SelectedItem_Should_Set_Container_Selected() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); - Assert.NotNull(container); + Assert.NotNull(container); - target.SelectedItem = item; + target.SelectedItem = item; - Assert.True(container.IsSelected); + Assert.True(container.IsSelected); + } } [Fact] public void Setting_SelectedItem_Should_Raise_SelectedItemChanged_Event() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; + var item = tree[0].Children[1].Children[0]; - var called = false; - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.RemovedItems); - Assert.Equal(1, e.AddedItems.Count); - Assert.Same(item, e.AddedItems[0]); - called = true; - }; + var called = false; + target.SelectionChanged += (s, e) => + { + Assert.Empty(e.RemovedItems); + Assert.Equal(1, e.AddedItems.Count); + Assert.Same(item, e.AddedItems[0]); + called = true; + }; - target.SelectedItem = item; - Assert.True(called); + target.SelectedItem = item; + Assert.True(called); + } } [Fact] @@ -564,7 +591,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node() { - using (UnitTestApplication.Start(TestServices.RealFocus)) + using (Application()) { var focus = FocusManager.Instance; var navigation = AvaloniaLocator.Current.GetService(); @@ -647,7 +674,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Pressing_SelectAll_Gesture_With_Downward_Range_Selected_Should_Select_All_Nodes() { - using (UnitTestApplication.Start()) + using (Application()) { var tree = CreateTestTreeData(); var target = new TreeView @@ -694,7 +721,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Pressing_SelectAll_Gesture_With_Upward_Range_Selected_Should_Select_All_Nodes() { - using (UnitTestApplication.Start()) + using (Application()) { var tree = CreateTestTreeData(); var target = new TreeView @@ -768,97 +795,106 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - var rootNode = tree[0]; - var to = rootNode.Children[0]; - var then = rootNode.Children[1]; + var rootNode = tree[0]; + var to = rootNode.Children[0]; + var then = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then); - ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(toContainer, KeyModifiers.Shift); + ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(toContainer, KeyModifiers.Shift); - Assert.Equal(2, target.Selection.SelectedItems.Count); + Assert.Equal(2, target.Selection.SelectedItems.Count); - _mouse.Click(thenContainer, MouseButton.Right); + _mouse.Click(thenContainer, MouseButton.Right); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } } [Fact] public void Shift_Right_Click_Should_Not_Select_Multiple() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var rootNode = tree[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - _mouse.Click(fromContainer); - _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } } [Fact] public void Ctrl_Right_Click_Should_Not_Select_Multiple() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var rootNode = tree[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - _mouse.Click(fromContainer); - _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } } [Fact] @@ -944,7 +980,7 @@ namespace Avalonia.Controls.UnitTests public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() { /// Issue #2980. - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Application()) { var target = new DerivedTreeView { @@ -1183,12 +1219,12 @@ namespace Avalonia.Controls.UnitTests } } - void ClickContainer(IControl container, KeyModifiers modifiers) + private void ClickContainer(IControl container, KeyModifiers modifiers) { _mouse.Click(container, modifiers: modifiers); } - void AssertChildrenSelected(TreeView treeView, Node rootNode) + private void AssertChildrenSelected(TreeView treeView, Node rootNode) { foreach (var child in rootNode.Children) { @@ -1198,6 +1234,16 @@ namespace Avalonia.Controls.UnitTests } } + private IDisposable Application() + { + return UnitTestApplication.Start( + TestServices.MockThreadingInterface.With( + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: new KeyboardNavigationHandler(), + inputManager: new InputManager())); + } + private class Node : NotifyingBase { private IAvaloniaList _children; diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index 5773f5e6b0..64f5eb2a0e 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -113,6 +113,50 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("(null)", target.Text); } + [Fact] + public void Should_Pass_UnsetValue_To_Converter_For_Broken_Binding() + { + var source = new { A = 1, B = 2, C = 3 }; + var target = new TextBlock { DataContext = source }; + + var binding = new MultiBinding + { + Converter = new ConcatConverter(), + Bindings = new[] + { + new Binding { Path = "A" }, + new Binding { Path = "B" }, + new Binding { Path = "Missing" }, + }, + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("1,2,(unset)", target.Text); + } + + [Fact] + public void Should_Pass_FallbackValue_To_Converter_For_Broken_Binding() + { + var source = new { A = 1, B = 2, C = 3 }; + var target = new TextBlock { DataContext = source }; + + var binding = new MultiBinding + { + Converter = new ConcatConverter(), + Bindings = new[] + { + new Binding { Path = "A" }, + new Binding { Path = "B" }, + new Binding { Path = "Missing", FallbackValue = "Fallback" }, + }, + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("1,2,Fallback", target.Text); + } + private class ConcatConverter : IMultiValueConverter { public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index 43c7525939..6730e3134d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -1,6 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Presenters; using Avalonia.Data; +using Avalonia.Data.Converters; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Data @@ -54,5 +61,51 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal("bar", textBlock.Text); } } + + [Fact] + public void MultiBinding_TemplatedParent_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBox = window.FindControl("textBox"); + + window.ApplyTemplate(); + textBox.ApplyTemplate(); + + var target = (TextPresenter)textBox.GetVisualChildren().Single(); + Assert.Equal("Foo,Bar", target.Text); + } + } + } + + public class ConcatConverter : IMultiValueConverter + { + public static ConcatConverter Instance { get; } = new ConcatConverter(); + + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return string.Join(",", values); + } } }