|
|
|
@ -5,11 +5,12 @@ |
|
|
|
using System; |
|
|
|
using System.Collections; |
|
|
|
using System.Collections.Generic; |
|
|
|
using System.Collections.Specialized; |
|
|
|
using System.ComponentModel; |
|
|
|
using System.Linq; |
|
|
|
using Avalonia.Collections; |
|
|
|
using Avalonia.Controls.Generators; |
|
|
|
using Avalonia.Controls.Primitives; |
|
|
|
using Avalonia.Controls.Utils; |
|
|
|
using Avalonia.Data; |
|
|
|
using Avalonia.Input; |
|
|
|
using Avalonia.Input.Platform; |
|
|
|
using Avalonia.Interactivity; |
|
|
|
@ -45,15 +46,29 @@ namespace Avalonia.Controls |
|
|
|
o => o.SelectedItems, |
|
|
|
(o, v) => o.SelectedItems = v); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="Selection"/> property.
|
|
|
|
/// </summary>
|
|
|
|
public static readonly DirectProperty<TreeView, ISelectionModel> SelectionProperty = |
|
|
|
SelectingItemsControl.SelectionProperty.AddOwner<TreeView>( |
|
|
|
o => o.Selection, |
|
|
|
(o, v) => o.Selection = v); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="SelectionMode"/> property.
|
|
|
|
/// </summary>
|
|
|
|
public static readonly StyledProperty<SelectionMode> SelectionModeProperty = |
|
|
|
ListBox.SelectionModeProperty.AddOwner<TreeView>(); |
|
|
|
|
|
|
|
private static readonly IList Empty = Array.Empty<object>(); |
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="SelectionChanged"/> property.
|
|
|
|
/// </summary>
|
|
|
|
public static RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent = |
|
|
|
SelectingItemsControl.SelectionChangedEvent; |
|
|
|
|
|
|
|
private object _selectedItem; |
|
|
|
private IList _selectedItems; |
|
|
|
private ISelectionModel _selection; |
|
|
|
private readonly SelectedItemsSync _selectedItems; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes static members of the <see cref="TreeView"/> class.
|
|
|
|
@ -63,6 +78,13 @@ namespace Avalonia.Controls |
|
|
|
// HACK: Needed or SelectedItem property will not be found in Release build.
|
|
|
|
} |
|
|
|
|
|
|
|
public TreeView() |
|
|
|
{ |
|
|
|
// Setting Selection to null causes a default SelectionModel to be created.
|
|
|
|
Selection = null; |
|
|
|
_selectedItems = new SelectedItemsSync(Selection); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Occurs when the control's selection changes.
|
|
|
|
/// </summary>
|
|
|
|
@ -87,8 +109,6 @@ namespace Avalonia.Controls |
|
|
|
set => SetValue(AutoScrollToSelectedItemProperty, value); |
|
|
|
} |
|
|
|
|
|
|
|
private bool _syncingSelectedItems; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the selection mode.
|
|
|
|
/// </summary>
|
|
|
|
@ -98,61 +118,102 @@ namespace Avalonia.Controls |
|
|
|
set => SetValue(SelectionModeProperty, value); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the selected item.
|
|
|
|
/// </summary>
|
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the selected item.
|
|
|
|
/// </summary>
|
|
|
|
public object SelectedItem |
|
|
|
{ |
|
|
|
get => _selectedItem; |
|
|
|
set |
|
|
|
{ |
|
|
|
var selectedItems = SelectedItems; |
|
|
|
|
|
|
|
SetAndRaise(SelectedItemProperty, ref _selectedItem, value); |
|
|
|
get => Selection.SelectedItem; |
|
|
|
set => Selection.SelectedIndex = IndexFromItem(value); |
|
|
|
} |
|
|
|
|
|
|
|
if (value != null) |
|
|
|
{ |
|
|
|
if (selectedItems.Count != 1 || selectedItems[0] != value) |
|
|
|
{ |
|
|
|
_syncingSelectedItems = true; |
|
|
|
SelectSingleItem(value); |
|
|
|
_syncingSelectedItems = false; |
|
|
|
} |
|
|
|
} |
|
|
|
else if (SelectedItems.Count > 0) |
|
|
|
{ |
|
|
|
SelectedItems.Clear(); |
|
|
|
} |
|
|
|
} |
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the selected items.
|
|
|
|
/// </summary>
|
|
|
|
protected IList SelectedItems |
|
|
|
{ |
|
|
|
get => _selectedItems.GetOrCreateItems(); |
|
|
|
set => _selectedItems.SetItems(value); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the selected items.
|
|
|
|
/// Gets or sets a model holding the current selection.
|
|
|
|
/// </summary>
|
|
|
|
public IList SelectedItems |
|
|
|
public ISelectionModel Selection |
|
|
|
{ |
|
|
|
get |
|
|
|
get => _selection; |
|
|
|
set |
|
|
|
{ |
|
|
|
if (_selectedItems == null) |
|
|
|
value ??= new SelectionModel |
|
|
|
{ |
|
|
|
_selectedItems = new AvaloniaList<object>(); |
|
|
|
SubscribeToSelectedItems(); |
|
|
|
} |
|
|
|
|
|
|
|
return _selectedItems; |
|
|
|
} |
|
|
|
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), |
|
|
|
AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected), |
|
|
|
RetainSelectionOnReset = true, |
|
|
|
}; |
|
|
|
|
|
|
|
set |
|
|
|
{ |
|
|
|
if (value?.IsFixedSize == true || value?.IsReadOnly == true) |
|
|
|
if (_selection != value) |
|
|
|
{ |
|
|
|
throw new NotSupportedException( |
|
|
|
"Cannot use a fixed size or read-only collection as SelectedItems."); |
|
|
|
} |
|
|
|
if (value == null) |
|
|
|
{ |
|
|
|
throw new ArgumentNullException(nameof(value), "Cannot set Selection to null."); |
|
|
|
} |
|
|
|
else if (value.Source != null && value.Source != Items) |
|
|
|
{ |
|
|
|
throw new ArgumentException("Selection has invalid Source."); |
|
|
|
} |
|
|
|
|
|
|
|
List<object> oldSelection = null; |
|
|
|
|
|
|
|
if (_selection != null) |
|
|
|
{ |
|
|
|
oldSelection = Selection.SelectedItems.ToList(); |
|
|
|
_selection.PropertyChanged -= OnSelectionModelPropertyChanged; |
|
|
|
_selection.SelectionChanged -= OnSelectionModelSelectionChanged; |
|
|
|
_selection.ChildrenRequested -= OnSelectionModelChildrenRequested; |
|
|
|
MarkContainersUnselected(); |
|
|
|
} |
|
|
|
|
|
|
|
_selection = value; |
|
|
|
|
|
|
|
if (_selection != null) |
|
|
|
{ |
|
|
|
_selection.Source = Items; |
|
|
|
_selection.PropertyChanged += OnSelectionModelPropertyChanged; |
|
|
|
_selection.SelectionChanged += OnSelectionModelSelectionChanged; |
|
|
|
_selection.ChildrenRequested += OnSelectionModelChildrenRequested; |
|
|
|
|
|
|
|
UnsubscribeFromSelectedItems(); |
|
|
|
_selectedItems = value ?? new AvaloniaList<object>(); |
|
|
|
SubscribeToSelectedItems(); |
|
|
|
if (_selection.SingleSelect) |
|
|
|
{ |
|
|
|
SelectionMode &= ~SelectionMode.Multiple; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
SelectionMode |= SelectionMode.Multiple; |
|
|
|
} |
|
|
|
|
|
|
|
if (_selection.AutoSelect) |
|
|
|
{ |
|
|
|
SelectionMode |= SelectionMode.AlwaysSelected; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
SelectionMode &= ~SelectionMode.AlwaysSelected; |
|
|
|
} |
|
|
|
|
|
|
|
UpdateContainerSelection(); |
|
|
|
|
|
|
|
var selectedItem = SelectedItem; |
|
|
|
|
|
|
|
if (_selectedItem != selectedItem) |
|
|
|
{ |
|
|
|
RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem); |
|
|
|
_selectedItem = selectedItem; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -185,186 +246,12 @@ namespace Avalonia.Controls |
|
|
|
/// Note that this method only selects nodes currently visible due to their parent nodes
|
|
|
|
/// being expanded: it does not expand nodes.
|
|
|
|
/// </remarks>
|
|
|
|
public void SelectAll() |
|
|
|
{ |
|
|
|
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); |
|
|
|
} |
|
|
|
public void SelectAll() => Selection.SelectAll(); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Deselects all items in the <see cref="TreeView"/>.
|
|
|
|
/// </summary>
|
|
|
|
public void UnselectAll() |
|
|
|
{ |
|
|
|
SelectedItems.Clear(); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
|
|
|
|
/// </summary>
|
|
|
|
private void SubscribeToSelectedItems() |
|
|
|
{ |
|
|
|
if (_selectedItems is INotifyCollectionChanged incc) |
|
|
|
{ |
|
|
|
incc.CollectionChanged += SelectedItemsCollectionChanged; |
|
|
|
} |
|
|
|
|
|
|
|
SelectedItemsCollectionChanged( |
|
|
|
_selectedItems, |
|
|
|
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); |
|
|
|
} |
|
|
|
|
|
|
|
private void SelectSingleItem(object item) |
|
|
|
{ |
|
|
|
SelectedItems.Clear(); |
|
|
|
SelectedItems.Add(item); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Called when the <see cref="SelectedItems"/> CollectionChanged event is raised.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="sender">The event sender.</param>
|
|
|
|
/// <param name="e">The event args.</param>
|
|
|
|
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
|
|
|
{ |
|
|
|
IList added = null; |
|
|
|
IList removed = null; |
|
|
|
|
|
|
|
switch (e.Action) |
|
|
|
{ |
|
|
|
case NotifyCollectionChangedAction.Add: |
|
|
|
|
|
|
|
SelectedItemsAdded(e.NewItems.Cast<object>().ToArray()); |
|
|
|
|
|
|
|
if (AutoScrollToSelectedItem) |
|
|
|
{ |
|
|
|
var container = (TreeViewItem)ItemContainerGenerator.Index.ContainerFromItem(e.NewItems[0]); |
|
|
|
|
|
|
|
container?.BringIntoView(); |
|
|
|
} |
|
|
|
|
|
|
|
added = e.NewItems; |
|
|
|
|
|
|
|
break; |
|
|
|
case NotifyCollectionChangedAction.Remove: |
|
|
|
|
|
|
|
if (!_syncingSelectedItems) |
|
|
|
{ |
|
|
|
if (SelectedItems.Count == 0) |
|
|
|
{ |
|
|
|
SelectedItem = null; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
var selectedIndex = SelectedItems.IndexOf(_selectedItem); |
|
|
|
|
|
|
|
if (selectedIndex == -1) |
|
|
|
{ |
|
|
|
var old = _selectedItem; |
|
|
|
_selectedItem = SelectedItems[0]; |
|
|
|
|
|
|
|
RaisePropertyChanged(SelectedItemProperty, old, _selectedItem); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
foreach (var item in e.OldItems) |
|
|
|
{ |
|
|
|
MarkItemSelected(item, false); |
|
|
|
} |
|
|
|
|
|
|
|
removed = e.OldItems; |
|
|
|
|
|
|
|
break; |
|
|
|
case NotifyCollectionChangedAction.Reset: |
|
|
|
|
|
|
|
foreach (IControl container in ItemContainerGenerator.Index.Containers) |
|
|
|
{ |
|
|
|
MarkContainerSelected(container, false); |
|
|
|
} |
|
|
|
|
|
|
|
if (SelectedItems.Count > 0) |
|
|
|
{ |
|
|
|
SelectedItemsAdded(SelectedItems); |
|
|
|
|
|
|
|
added = SelectedItems; |
|
|
|
} |
|
|
|
else if (!_syncingSelectedItems) |
|
|
|
{ |
|
|
|
SelectedItem = null; |
|
|
|
} |
|
|
|
|
|
|
|
break; |
|
|
|
case NotifyCollectionChangedAction.Replace: |
|
|
|
|
|
|
|
foreach (var item in e.OldItems) |
|
|
|
{ |
|
|
|
MarkItemSelected(item, false); |
|
|
|
} |
|
|
|
|
|
|
|
foreach (var item in e.NewItems) |
|
|
|
{ |
|
|
|
MarkItemSelected(item, true); |
|
|
|
} |
|
|
|
|
|
|
|
if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems) |
|
|
|
{ |
|
|
|
var oldItem = SelectedItem; |
|
|
|
var item = SelectedItems[0]; |
|
|
|
_selectedItem = item; |
|
|
|
RaisePropertyChanged(SelectedItemProperty, oldItem, item); |
|
|
|
} |
|
|
|
|
|
|
|
added = e.NewItems; |
|
|
|
removed = e.OldItems; |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
if (added?.Count > 0 || removed?.Count > 0) |
|
|
|
{ |
|
|
|
var changed = new SelectionChangedEventArgs( |
|
|
|
SelectingItemsControl.SelectionChangedEvent, |
|
|
|
removed ?? Empty, |
|
|
|
added ?? Empty); |
|
|
|
RaiseEvent(changed); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void MarkItemSelected(object item, bool selected) |
|
|
|
{ |
|
|
|
var container = ItemContainerGenerator.Index.ContainerFromItem(item); |
|
|
|
|
|
|
|
MarkContainerSelected(container, selected); |
|
|
|
} |
|
|
|
|
|
|
|
private void SelectedItemsAdded(IList items) |
|
|
|
{ |
|
|
|
if (items.Count == 0) |
|
|
|
{ |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
foreach (object item in items) |
|
|
|
{ |
|
|
|
MarkItemSelected(item, true); |
|
|
|
} |
|
|
|
|
|
|
|
if (SelectedItem == null && !_syncingSelectedItems) |
|
|
|
{ |
|
|
|
SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Unsubscribes from the <see cref="SelectedItems"/> CollectionChanged event, if any.
|
|
|
|
/// </summary>
|
|
|
|
private void UnsubscribeFromSelectedItems() |
|
|
|
{ |
|
|
|
if (_selectedItems is INotifyCollectionChanged incc) |
|
|
|
{ |
|
|
|
incc.CollectionChanged -= SelectedItemsCollectionChanged; |
|
|
|
} |
|
|
|
} |
|
|
|
public void UnselectAll() => Selection.ClearSelection(); |
|
|
|
|
|
|
|
(bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, |
|
|
|
NavigationDirection direction) |
|
|
|
@ -448,6 +335,72 @@ namespace Avalonia.Controls |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Called when <see cref="SelectionModel.PropertyChanged"/> is raised.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="sender">The sender.</param>
|
|
|
|
/// <param name="e">The event args.</param>
|
|
|
|
private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) |
|
|
|
{ |
|
|
|
if (e.PropertyName == nameof(SelectionModel.AnchorIndex) && AutoScrollToSelectedItem) |
|
|
|
{ |
|
|
|
var container = ContainerFromIndex(Selection.AnchorIndex); |
|
|
|
|
|
|
|
if (container != null) |
|
|
|
{ |
|
|
|
container.BringIntoView(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Called when <see cref="SelectionModel.SelectionChanged"/> is raised.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="sender">The sender.</param>
|
|
|
|
/// <param name="e">The event args.</param>
|
|
|
|
private void OnSelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) |
|
|
|
{ |
|
|
|
void Mark(IndexPath index, bool selected) |
|
|
|
{ |
|
|
|
var container = ContainerFromIndex(index); |
|
|
|
|
|
|
|
if (container != null) |
|
|
|
{ |
|
|
|
MarkContainerSelected(container, selected); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
foreach (var i in e.SelectedIndices) |
|
|
|
{ |
|
|
|
Mark(i, true); |
|
|
|
} |
|
|
|
|
|
|
|
foreach (var i in e.DeselectedIndices) |
|
|
|
{ |
|
|
|
Mark(i, false); |
|
|
|
} |
|
|
|
|
|
|
|
var newSelectedItem = SelectedItem; |
|
|
|
|
|
|
|
if (newSelectedItem != _selectedItem) |
|
|
|
{ |
|
|
|
RaisePropertyChanged(SelectedItemProperty, _selectedItem, newSelectedItem); |
|
|
|
_selectedItem = newSelectedItem; |
|
|
|
} |
|
|
|
|
|
|
|
var ev = new SelectionChangedEventArgs( |
|
|
|
SelectionChangedEvent, |
|
|
|
e.DeselectedItems.ToList(), |
|
|
|
e.SelectedItems.ToList()); |
|
|
|
RaiseEvent(ev); |
|
|
|
} |
|
|
|
|
|
|
|
private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e) |
|
|
|
{ |
|
|
|
var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as ItemsControl; |
|
|
|
e.Children = container?.Items as IEnumerable; |
|
|
|
} |
|
|
|
|
|
|
|
private TreeViewItem GetContainerInDirection( |
|
|
|
TreeViewItem from, |
|
|
|
NavigationDirection direction, |
|
|
|
@ -501,6 +454,12 @@ namespace Avalonia.Controls |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e) |
|
|
|
{ |
|
|
|
Selection.Source = Items; |
|
|
|
base.ItemsChanged(e); |
|
|
|
} |
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
|
protected override void OnPointerPressed(PointerPressedEventArgs e) |
|
|
|
{ |
|
|
|
@ -522,6 +481,18 @@ namespace Avalonia.Controls |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority) |
|
|
|
{ |
|
|
|
base.OnPropertyChanged(property, oldValue, newValue, priority); |
|
|
|
|
|
|
|
if (property == SelectionModeProperty) |
|
|
|
{ |
|
|
|
var mode = newValue.GetValueOrDefault<SelectionMode>(); |
|
|
|
Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple); |
|
|
|
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Updates the selection for an item based on user interaction.
|
|
|
|
/// </summary>
|
|
|
|
@ -537,9 +508,9 @@ namespace Avalonia.Controls |
|
|
|
bool toggleModifier = false, |
|
|
|
bool rightButton = false) |
|
|
|
{ |
|
|
|
var item = ItemContainerGenerator.Index.ItemFromContainer(container); |
|
|
|
var index = IndexFromContainer((TreeViewItem)container); |
|
|
|
|
|
|
|
if (item == null) |
|
|
|
if (index.GetSize() == 0) |
|
|
|
{ |
|
|
|
return; |
|
|
|
} |
|
|
|
@ -556,41 +527,48 @@ namespace Avalonia.Controls |
|
|
|
var multi = (mode & SelectionMode.Multiple) != 0; |
|
|
|
var range = multi && selectedContainer != null && rangeModifier; |
|
|
|
|
|
|
|
if (rightButton) |
|
|
|
if (!select) |
|
|
|
{ |
|
|
|
Selection.DeselectAt(index); |
|
|
|
} |
|
|
|
else if (rightButton) |
|
|
|
{ |
|
|
|
if (!SelectedItems.Contains(item)) |
|
|
|
if (!Selection.IsSelectedAt(index)) |
|
|
|
{ |
|
|
|
SelectSingleItem(item); |
|
|
|
Selection.SelectedIndex = index; |
|
|
|
} |
|
|
|
} |
|
|
|
else if (!toggle && !range) |
|
|
|
{ |
|
|
|
SelectSingleItem(item); |
|
|
|
Selection.SelectedIndex = index; |
|
|
|
} |
|
|
|
else if (multi && range) |
|
|
|
{ |
|
|
|
SynchronizeItems( |
|
|
|
SelectedItems, |
|
|
|
GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem)); |
|
|
|
using var operation = Selection.Update(); |
|
|
|
var anchor = Selection.AnchorIndex; |
|
|
|
|
|
|
|
if (anchor.GetSize() == 0) |
|
|
|
{ |
|
|
|
anchor = new IndexPath(0); |
|
|
|
} |
|
|
|
|
|
|
|
Selection.ClearSelection(); |
|
|
|
Selection.AnchorIndex = anchor; |
|
|
|
Selection.SelectRangeFromAnchorTo(index); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
var i = SelectedItems.IndexOf(item); |
|
|
|
|
|
|
|
if (i != -1) |
|
|
|
if (Selection.IsSelectedAt(index)) |
|
|
|
{ |
|
|
|
SelectedItems.Remove(item); |
|
|
|
Selection.DeselectAt(index); |
|
|
|
} |
|
|
|
else if (multi) |
|
|
|
{ |
|
|
|
Selection.SelectAt(index); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
if (multi) |
|
|
|
{ |
|
|
|
SelectedItems.Add(item); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
SelectedItem = item; |
|
|
|
} |
|
|
|
Selection.SelectedIndex = index; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -613,117 +591,6 @@ namespace Avalonia.Controls |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Find which node is first in hierarchy.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="treeView">Search root.</param>
|
|
|
|
/// <param name="nodeA">Nodes to find.</param>
|
|
|
|
/// <param name="nodeB">Node to find.</param>
|
|
|
|
/// <returns>Found first node.</returns>
|
|
|
|
private static TreeViewItem FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB) |
|
|
|
{ |
|
|
|
return FindInContainers(treeView.ItemContainerGenerator, nodeA, nodeB); |
|
|
|
} |
|
|
|
|
|
|
|
private static TreeViewItem FindInContainers(ITreeItemContainerGenerator containerGenerator, |
|
|
|
TreeViewItem nodeA, |
|
|
|
TreeViewItem nodeB) |
|
|
|
{ |
|
|
|
IEnumerable<ItemContainerInfo> containers = containerGenerator.Containers; |
|
|
|
|
|
|
|
foreach (ItemContainerInfo container in containers) |
|
|
|
{ |
|
|
|
TreeViewItem node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB); |
|
|
|
|
|
|
|
if (node != null) |
|
|
|
{ |
|
|
|
return node; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
private static TreeViewItem FindFirstNode(TreeViewItem node, TreeViewItem nodeA, TreeViewItem nodeB) |
|
|
|
{ |
|
|
|
if (node == null) |
|
|
|
{ |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
TreeViewItem match = node == nodeA ? nodeA : node == nodeB ? nodeB : null; |
|
|
|
|
|
|
|
if (match != null) |
|
|
|
{ |
|
|
|
return match; |
|
|
|
} |
|
|
|
|
|
|
|
return FindInContainers(node.ItemContainerGenerator, nodeA, nodeB); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Returns all items that belong to containers between <paramref name="from"/> and <paramref name="to"/>.
|
|
|
|
/// The range is inclusive.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="from">From container.</param>
|
|
|
|
/// <param name="to">To container.</param>
|
|
|
|
private List<object> GetItemsInRange(TreeViewItem from, TreeViewItem to) |
|
|
|
{ |
|
|
|
var items = new List<object>(); |
|
|
|
|
|
|
|
if (from == null || to == null) |
|
|
|
{ |
|
|
|
return items; |
|
|
|
} |
|
|
|
|
|
|
|
TreeViewItem firstItem = FindFirstNode(this, from, to); |
|
|
|
|
|
|
|
if (firstItem == null) |
|
|
|
{ |
|
|
|
return items; |
|
|
|
} |
|
|
|
|
|
|
|
bool wasReversed = false; |
|
|
|
|
|
|
|
if (firstItem == to) |
|
|
|
{ |
|
|
|
var temp = from; |
|
|
|
|
|
|
|
from = to; |
|
|
|
to = temp; |
|
|
|
|
|
|
|
wasReversed = true; |
|
|
|
} |
|
|
|
|
|
|
|
TreeViewItem node = from; |
|
|
|
|
|
|
|
while (node != to) |
|
|
|
{ |
|
|
|
var item = ItemContainerGenerator.Index.ItemFromContainer(node); |
|
|
|
|
|
|
|
if (item != null) |
|
|
|
{ |
|
|
|
items.Add(item); |
|
|
|
} |
|
|
|
|
|
|
|
node = GetContainerInDirection(node, NavigationDirection.Down, true); |
|
|
|
} |
|
|
|
|
|
|
|
var toItem = ItemContainerGenerator.Index.ItemFromContainer(to); |
|
|
|
|
|
|
|
if (toItem != null) |
|
|
|
{ |
|
|
|
items.Add(toItem); |
|
|
|
} |
|
|
|
|
|
|
|
if (wasReversed) |
|
|
|
{ |
|
|
|
items.Reverse(); |
|
|
|
} |
|
|
|
|
|
|
|
return items; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Updates the selection based on an event that may have originated in a container that
|
|
|
|
/// belongs to the control.
|
|
|
|
@ -829,26 +696,90 @@ namespace Avalonia.Controls |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Makes a list of objects equal another (though doesn't preserve order).
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="items">The items collection.</param>
|
|
|
|
/// <param name="desired">The desired items.</param>
|
|
|
|
private static void SynchronizeItems(IList items, IEnumerable<object> desired) |
|
|
|
private void MarkContainersUnselected() |
|
|
|
{ |
|
|
|
var list = items.Cast<object>().ToList(); |
|
|
|
var toRemove = list.Except(desired).ToList(); |
|
|
|
var toAdd = desired.Except(list).ToList(); |
|
|
|
foreach (var container in ItemContainerGenerator.Index.Containers) |
|
|
|
{ |
|
|
|
MarkContainerSelected(container, false); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void UpdateContainerSelection() |
|
|
|
{ |
|
|
|
var index = ItemContainerGenerator.Index; |
|
|
|
|
|
|
|
foreach (var container in index.Containers) |
|
|
|
{ |
|
|
|
var i = IndexFromContainer((TreeViewItem)container); |
|
|
|
|
|
|
|
MarkContainerSelected( |
|
|
|
container, |
|
|
|
Selection.IsSelectedAt(i) != false); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static IndexPath IndexFromContainer(TreeViewItem container) |
|
|
|
{ |
|
|
|
var result = new List<int>(); |
|
|
|
|
|
|
|
while (true) |
|
|
|
{ |
|
|
|
if (container.Level == 0) |
|
|
|
{ |
|
|
|
var treeView = container.FindAncestorOfType<TreeView>(); |
|
|
|
|
|
|
|
if (treeView == null) |
|
|
|
{ |
|
|
|
return default; |
|
|
|
} |
|
|
|
|
|
|
|
result.Add(treeView.ItemContainerGenerator.IndexFromContainer(container)); |
|
|
|
result.Reverse(); |
|
|
|
return new IndexPath(result); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
var parent = container.FindAncestorOfType<TreeViewItem>(); |
|
|
|
|
|
|
|
if (parent == null) |
|
|
|
{ |
|
|
|
return default; |
|
|
|
} |
|
|
|
|
|
|
|
result.Add(parent.ItemContainerGenerator.IndexFromContainer(container)); |
|
|
|
container = parent; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private IndexPath IndexFromItem(object item) |
|
|
|
{ |
|
|
|
var container = ItemContainerGenerator.Index.ContainerFromItem(item) as TreeViewItem; |
|
|
|
|
|
|
|
foreach (var i in toRemove) |
|
|
|
if (container != null) |
|
|
|
{ |
|
|
|
items.Remove(i); |
|
|
|
return IndexFromContainer(container); |
|
|
|
} |
|
|
|
|
|
|
|
foreach (var i in toAdd) |
|
|
|
return default; |
|
|
|
} |
|
|
|
|
|
|
|
private TreeViewItem ContainerFromIndex(IndexPath index) |
|
|
|
{ |
|
|
|
TreeViewItem treeViewItem = null; |
|
|
|
|
|
|
|
for (var i = 0; i < index.GetSize(); ++i) |
|
|
|
{ |
|
|
|
items.Add(i); |
|
|
|
var generator = treeViewItem?.ItemContainerGenerator ?? ItemContainerGenerator; |
|
|
|
treeViewItem = generator.ContainerFromIndex(index.GetAt(i)) as TreeViewItem; |
|
|
|
|
|
|
|
if (treeViewItem == null) |
|
|
|
{ |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return treeViewItem; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|