using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Automation.Peers; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls { /// /// Displays a hierarchical tree of data. /// public class TreeView : ItemsControl, ICustomKeyboardNavigation { /// /// Defines the property. /// public static readonly StyledProperty AutoScrollToSelectedItemProperty = SelectingItemsControl.AutoScrollToSelectedItemProperty.AddOwner(); /// /// Defines the property. /// public static readonly DirectProperty SelectedItemProperty = SelectingItemsControl.SelectedItemProperty.AddOwner( o => o.SelectedItem, (o, v) => o.SelectedItem = v); /// /// Defines the property. /// public static readonly DirectProperty SelectedItemsProperty = AvaloniaProperty.RegisterDirect( nameof(SelectedItems), o => o.SelectedItems, (o, v) => o.SelectedItems = v); /// /// Defines the property. /// public static readonly StyledProperty SelectionModeProperty = ListBox.SelectionModeProperty.AddOwner(); private static readonly IList Empty = Array.Empty(); private object? _selectedItem; private IList? _selectedItems; private bool _syncingSelectedItems; /// /// Initializes static members of the class. /// static TreeView() { SelectingItemsControl.IsSelectedChangedEvent.AddClassHandler((x, e) => x.ContainerSelectionChanged(e)); } /// /// Occurs when the control's selection changes. /// public event EventHandler? SelectionChanged { add => AddHandler(SelectingItemsControl.SelectionChangedEvent, value); remove => RemoveHandler(SelectingItemsControl.SelectionChangedEvent, value); } /// /// Gets or sets a value indicating whether to automatically scroll to newly selected items. /// /// /// This property is of limited use with as it will only scroll /// to realized items. To scroll to a non-expanded item, you need to ensure that its /// ancestors are expanded. /// public bool AutoScrollToSelectedItem { get => GetValue(AutoScrollToSelectedItemProperty); set => SetValue(AutoScrollToSelectedItemProperty, value); } /// /// Gets or sets the selection mode. /// public SelectionMode SelectionMode { get => GetValue(SelectionModeProperty); set => SetValue(SelectionModeProperty, value); } /// /// Gets or sets the selected item. /// /// /// Note that setting this property only currently works if the item is expanded to be visible. /// To select non-expanded nodes use `Selection.SelectedIndex`. /// public object? SelectedItem { get => _selectedItem; set { var selectedItems = SelectedItems; SetAndRaise(SelectedItemProperty, ref _selectedItem, value); if (value != null) { if (selectedItems.Count != 1 || selectedItems[0] != value) { SelectSingleItem(value); } } else if (SelectedItems.Count > 0) { SelectedItems.Clear(); } } } /// /// Gets or sets the selected items. /// [AllowNull] public IList SelectedItems { get { if (_selectedItems == null) { _selectedItems = new AvaloniaList(); SubscribeToSelectedItems(); } return _selectedItems; } set { if (value?.IsFixedSize == true || value?.IsReadOnly == true) { throw new NotSupportedException( "Cannot use a fixed size or read-only collection as SelectedItems."); } UnsubscribeFromSelectedItems(); _selectedItems = value ?? new AvaloniaList(); SubscribeToSelectedItems(); } } /// /// Expands the specified all descendent s. /// /// The item to expand. public void ExpandSubTree(TreeViewItem item) { item.IsExpanded = true; if (item.Presenter?.Panel is null) this.GetLayoutManager()?.ExecuteLayoutPass(); if (item.Presenter?.Panel is { } panel) { foreach (var child in panel.Children) { if (child is TreeViewItem treeViewItem) { ExpandSubTree(treeViewItem); } } } } /// /// Collapse the specified all descendent s. /// /// The item to collapse. public void CollapseSubTree(TreeViewItem item) { item.IsExpanded = false; if (item.Presenter?.Panel != null) { foreach (var child in item.Presenter.Panel.Children) { if (child is TreeViewItem treeViewItem) { CollapseSubTree(treeViewItem); } } } } /// /// Selects all items in the . /// /// /// Note that this method only selects nodes currently visible due to their parent nodes /// being expanded: it does not expand nodes. /// public void SelectAll() { var allItems = new List(); void AddItems(ItemsControl itemsControl) { foreach (var item in itemsControl.ItemsView) allItems.Add(item!); foreach (var child in itemsControl.GetRealizedContainers()) { if (child is ItemsControl childItemsControl) AddItems(childItemsControl); } } AddItems(this); SynchronizeItems(SelectedItems, allItems); } /// /// Deselects all items in the . /// public void UnselectAll() { SelectedItems.Clear(); } public IEnumerable GetRealizedTreeContainers() { static IEnumerable GetRealizedContainers(ItemsControl itemsControl) { foreach (var container in itemsControl.GetRealizedContainers()) { yield return container; if (container is ItemsControl itemsControlContainer) foreach (var child in GetRealizedContainers(itemsControlContainer)) yield return child; } } return GetRealizedContainers(this); } public Control? TreeContainerFromItem(object item) { static Control? TreeContainerFromItem(ItemsControl itemsControl, object item) { if (itemsControl.ContainerFromItem(item) is { } container) return container; foreach (var child in itemsControl.GetRealizedContainers()) { if (child is ItemsControl childItemsControl && TreeContainerFromItem(childItemsControl, item) is { } childContainer) return childContainer; } return null; } return TreeContainerFromItem(this, item); } public object? TreeItemFromContainer(Control container) { static object? TreeItemFromContainer(ItemsControl itemsControl, Control container) { if (itemsControl.ItemFromContainer(container) is { } item) return item; foreach (var child in itemsControl.GetRealizedContainers()) { if (child is ItemsControl childItemsControl && TreeItemFromContainer(childItemsControl, container) is { } childContainer) return childContainer; } return null; } return TreeItemFromContainer(this, container); } /// protected override AutomationPeer OnCreateAutomationPeer() { return new TreeViewAutomationPeer(this); } private protected override void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.OnItemsViewCollectionChanged(sender, e); switch (e.Action) { case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Replace: foreach (var i in e.OldItems!) SelectedItems.Remove(i); break; case NotifyCollectionChangedAction.Reset: SelectedItems.Clear(); break; } } /// /// Subscribes to the CollectionChanged event, if any. /// private void SubscribeToSelectedItems() { if (_selectedItems is INotifyCollectionChanged incc) { incc.CollectionChanged += SelectedItemsCollectionChanged; } SelectedItemsCollectionChanged( _selectedItems, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } private void SelectSingleItem(object item) { var oldValue = _selectedItem; _syncingSelectedItems = true; SelectedItems.Clear(); _selectedItem = item; SelectedItems.Add(item); _syncingSelectedItems = false; RaisePropertyChanged(SelectedItemProperty, oldValue, _selectedItem); } /// /// Called when the CollectionChanged event is raised. /// /// The event sender. /// The event args. private void SelectedItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { IList? added = null; IList? removed = null; switch (e.Action) { case NotifyCollectionChangedAction.Add: SelectedItemsAdded(e.NewItems!.Cast().ToArray()); var selectedItem = SelectedItem; if (AutoScrollToSelectedItem && selectedItem is not null && e.NewItems![0] == selectedItem) { var container = TreeContainerFromItem(selectedItem); 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 (var container in GetRealizedTreeContainers()) { 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) { if (TreeContainerFromItem(item) is Control container) 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]); } } /// /// Unsubscribes from the CollectionChanged event, if any. /// private void UnsubscribeFromSelectedItems() { if (_selectedItems is INotifyCollectionChanged incc) { incc.CollectionChanged -= SelectedItemsCollectionChanged; } } (bool handled, IInputElement? next) ICustomKeyboardNavigation.GetNext(IInputElement element, NavigationDirection direction) { if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) { if (!this.IsVisualAncestorOf((Visual)element)) { var result = _selectedItem != null ? TreeContainerFromItem(_selectedItem) : ContainerFromIndex(0); return (result != null, result); // SelectedItem may not be in the treeview. } return (true, null); } return (false, null); } protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { return new TreeViewItem(); } protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { return NeedsContainer(item, out recycleKey); } protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index) { base.ContainerForItemPreparedOverride(container, item, index); // Once the container has been full prepared and added to the tree, any bindings from // styles or item container themes are guaranteed to be applied. if (container.IsSet(SelectingItemsControl.IsSelectedProperty)) { // The IsSelected property is set on the container: there is a style or item // container theme which has bound the IsSelected property. Update our selection // based on the selection state of the container. var containerIsSelected = SelectingItemsControl.GetIsSelected(container); UpdateSelectionFromContainer(container, select: containerIsSelected, toggleModifier: true); } // The IsSelected property is not set on the container: update the container // selection based on the current selection as understood by this control. MarkContainerSelected(container, SelectedItems.Contains(item)); // If the newly realized container is the selected container, scroll to it after layout. if (AutoScrollToSelectedItem && SelectedItem == item) { Dispatcher.UIThread.Post(container.BringIntoView, DispatcherPriority.Loaded); } } /// protected override void OnGotFocus(FocusChangedEventArgs e) { if (e.NavigationMethod == NavigationMethod.Directional) { e.Handled = UpdateSelectionFromEventSource( e.Source!, true, e.KeyModifiers.HasAllFlags(KeyModifiers.Shift)); } } protected override void OnKeyDown(KeyEventArgs e) { var direction = e.Key.ToNavigationDirection(); if (direction?.IsDirectional() == true && !e.Handled) { if (SelectedItem != null) { var next = GetContainerInDirection( GetContainerFromEventSource(e.Source!), direction.Value, true); if (next != null) { e.Handled = next.Focus(NavigationMethod.Directional); } } else { SelectedItem = ItemsView[0]; } } if (!e.Handled) { var keymap = Application.Current!.PlatformSettings!.HotkeyConfiguration; bool Match(List? gestures) => gestures?.Any(g => g.Matches(e)) ?? false; if (this.SelectionMode == SelectionMode.Multiple && Match(keymap?.SelectAll)) { SelectAll(); e.Handled = true; } } } private TreeViewItem? GetContainerInDirection( TreeViewItem? from, NavigationDirection direction, bool intoChildren) { var parentItemsControl = from?.Parent switch { TreeView tv => (ItemsControl)tv, TreeViewItem i => i, _ => null }; if (parentItemsControl == null) { return null; } var index = from is not null ? parentItemsControl.IndexFromContainer(from) : -1; var parent = from?.Parent as ItemsControl; TreeViewItem? result = null; switch (direction) { case NavigationDirection.Up: if (index > 0) { var previous = (TreeViewItem)parentItemsControl.ContainerFromIndex(index - 1)!; result = previous.IsExpanded && previous.ItemCount > 0 ? (TreeViewItem)previous.ContainerFromIndex(previous.ItemCount - 1)! : previous; } else { result = from?.Parent as TreeViewItem; } break; case NavigationDirection.Down: case NavigationDirection.Right: if (from?.IsExpanded == true && intoChildren && from.ItemCount > 0) { result = (TreeViewItem)from.ContainerFromIndex(0)!; } else if (index < parent?.ItemCount - 1) { result = (TreeViewItem)parentItemsControl.ContainerFromIndex(index + 1)!; } else if (parent is TreeViewItem parentItem) { return GetContainerInDirection(parentItem, direction, false); } break; } return result; } /// /// protected virtual bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs) => ItemSelectionEventTriggers.ShouldTriggerSelection(selectable, eventArgs); /// protected virtual bool ShouldTriggerSelection(Visual selectable, KeyEventArgs eventArgs) => ItemSelectionEventTriggers.ShouldTriggerSelection(selectable, eventArgs); /// /// public virtual bool UpdateSelectionFromEvent(Control container, RoutedEventArgs eventArgs) { if (eventArgs.Handled) { return false; } switch (eventArgs) { case PointerEventArgs pointerEvent when ShouldTriggerSelection(container, pointerEvent): case KeyEventArgs keyEvent when ShouldTriggerSelection(container, keyEvent): UpdateSelectionFromContainer(container, true, ItemSelectionEventTriggers.HasRangeSelectionModifier(container, eventArgs), ItemSelectionEventTriggers.HasToggleSelectionModifier(container, eventArgs), eventArgs is PointerEventArgs { Properties.IsRightButtonPressed: true }); eventArgs.Handled = true; return true; default: return false; } } /// /// Updates the selection for an item based on user interaction. /// /// The container. /// Whether the item should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). /// Whether the event is a right-click. protected void UpdateSelectionFromContainer( Control container, bool select = true, bool rangeModifier = false, bool toggleModifier = false, bool rightButton = false) { var item = TreeItemFromContainer(container); if (item == null) { return; } Control? selectedContainer = null; if (SelectedItem != null) { selectedContainer = TreeContainerFromItem(SelectedItem); } var mode = SelectionMode; var toggle = toggleModifier || mode.HasAllFlags(SelectionMode.Toggle); var multi = mode.HasAllFlags(SelectionMode.Multiple); var range = multi && rangeModifier && selectedContainer != null; if (!select) { SelectedItems.Remove(item); } else if (rightButton) { if (!SelectedItems.Contains(item)) { SelectSingleItem(item); } } else if (!toggle && !range) { SelectSingleItem(item); } else if (multi && range) { SynchronizeItems( SelectedItems, GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem)); } else { var i = SelectedItems.IndexOf(item); if (i != -1) { SelectedItems.Remove(item); } else { if (multi) { SelectedItems.Add(item); } else { SelectedItem = item; } } } } /// /// Find which node is first in hierarchy. /// /// Search root. /// Nodes to find. /// Node to find. /// Found first node. private static TreeViewItem? FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB) { return FindInContainers(treeView, nodeA, nodeB); } private static TreeViewItem? FindInContainers(ItemsControl itemsControl, TreeViewItem nodeA, TreeViewItem nodeB) { foreach (var container in itemsControl.GetRealizedContainers()) { TreeViewItem? node = FindFirstNode(container 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, nodeA, nodeB); } /// /// Returns all items that belong to containers between and . /// The range is inclusive. /// /// From container. /// To container. private List GetItemsInRange(TreeViewItem? from, TreeViewItem? to) { var items = new List(); 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 is not null && node != to) { var item = TreeItemFromContainer(node); if (item != null) { items.Add(item); } node = GetContainerInDirection(node, NavigationDirection.Down, true); } var toItem = TreeItemFromContainer(to); if (toItem != null) { items.Add(toItem); } if (wasReversed) { items.Reverse(); } return items; } /// /// Updates the selection based on an event that may have originated in a container that /// belongs to the control. /// /// The control that raised the event. /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). /// Whether the event is a right-click. /// /// True if the event originated from a container that belongs to the control; otherwise /// false. /// protected bool UpdateSelectionFromEventSource( object eventSource, bool select = true, bool rangeModifier = false, bool toggleModifier = false, bool rightButton = false) { var container = GetContainerFromEventSource(eventSource); if (container != null) { UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier, rightButton); return true; } return false; } /// /// Tries to get the container that was the source of an event. /// /// The control that raised the event. /// The container or null if the event did not originate in a container. protected TreeViewItem? GetContainerFromEventSource(object eventSource) { var item = ((Visual)eventSource).GetSelfAndVisualAncestors() .OfType() .FirstOrDefault(); return item?.TreeViewOwner == this ? item : null; } /// /// Called when a container raises the /// . /// /// The event. private void ContainerSelectionChanged(RoutedEventArgs e) { if (e.Source is TreeViewItem container && container.TreeViewOwner == this && TreeItemFromContainer(container) is object item) { var containerIsSelected = SelectingItemsControl.GetIsSelected(container); var ourIsSelected = SelectedItems.Contains(item); if (containerIsSelected != ourIsSelected) { if (containerIsSelected) SelectedItems.Add(item); else SelectedItems.Remove(item); } } if (e.Source != this) { e.Handled = true; } } /// /// Sets a container's 'selected' class or . /// /// The container. /// Whether the control is selected private void MarkContainerSelected(Control container, bool selected) { container.SetCurrentValue(SelectingItemsControl.IsSelectedProperty, selected); } /// /// Makes a list of objects equal another (though doesn't preserve order). /// /// The items collection. /// The desired items. private static void SynchronizeItems(IList items, List desired) { var itemsCount = items.Count; if (desired is not null) { var desiredCount = desired.Count; if (itemsCount == 0 && desiredCount > 0) { // Add all desired foreach (var item in desired) { items.Add(item); } } else if (itemsCount > 0 && desiredCount == 0) { // Remove all items.Clear(); } // Intersect else { var list = new object[items.Count]; items.CopyTo(list, 0); var toRemove = list.Except(desired).ToArray(); var toAdd = desired.Except(list).ToArray(); foreach (var i in toRemove) { items.Remove(i); } foreach (var i in toAdd) { items.Add(i); } } } } } }