using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Platform; 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() { // HACK: Needed or SelectedItem property will not be found in Release build. } /// /// Occurs when the control's selection changes. /// public event EventHandler? SelectionChanged { add => AddHandler(SelectingItemsControl.SelectionChangedEvent, value); remove => RemoveHandler(SelectingItemsControl.SelectionChangedEvent, value); } /// /// Gets the for the tree view. /// public new TreeItemContainerGenerator ItemContainerGenerator => (TreeItemContainerGenerator)base.ItemContainerGenerator; /// /// Gets or sets a value indicating whether to automatically scroll to newly selected items. /// 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.GetVisualRoot() as ILayoutRoot)?.LayoutManager.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); } /// /// 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) { _syncingSelectedItems = true; SelectedItems.Clear(); SelectedItems.Add(item); _syncingSelectedItems = false; SetAndRaise(SelectedItemProperty, ref _selectedItem, item); } /// /// 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()); if (AutoScrollToSelectedItem) { var container = 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 (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) { var container = TreeContainerFromItem(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]); } } /// /// 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() => new TreeViewItem(); protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TreeViewItem; protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index) { base.PrepareContainerForItemOverride(container, item, index); if (item == SelectedItem) { MarkContainerSelected(container, true); if (AutoScrollToSelectedItem) Dispatcher.UIThread.Post(container.BringIntoView); } } /// protected override void OnGotFocus(GotFocusEventArgs 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) { FocusManager.Instance?.Focus(next, NavigationMethod.Directional); e.Handled = true; } } else { SelectedItem = ItemsView[0]; } } if (!e.Handled) { var keymap = AvaloniaLocator.Current.GetService(); 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 override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); if (e.Source is Visual source) { var point = e.GetCurrentPoint(source); if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed) { e.Handled = UpdateSelectionFromEventSource( e.Source, true, e.KeyModifiers.HasAllFlags(KeyModifiers.Shift), e.KeyModifiers.HasAllFlags(AvaloniaLocator.Current.GetRequiredService().CommandModifiers), point.Properties.IsRightButtonPressed); } } } /// /// 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 (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; } } } } [Obsolete] private protected override ItemContainerGenerator CreateItemContainerGenerator() { return new TreeItemContainerGenerator(this); } /// /// 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; } /// /// Sets a container's 'selected' class or . /// /// The container. /// Whether the control is selected private void MarkContainerSelected(Control? container, bool selected) { if (container == null) { return; } if (container is ISelectable selectable) { selectable.IsSelected = selected; } else { ((IPseudoClasses)container.Classes).Set(":selected", 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, IEnumerable desired) { var list = items.Cast(); var toRemove = list.Except(desired).ToList(); var toAdd = desired.Except(list).ToList(); foreach (var i in toRemove) { items.Remove(i); } foreach (var i in toAdd) { items.Add(i); } } } }