using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; 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; 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 = ListBox.SelectedItemsProperty.AddOwner( o => o.SelectedItems, (o, v) => o.SelectedItems = v); /// /// Defines the property. /// public static readonly DirectProperty SelectionProperty = SelectingItemsControl.SelectionProperty.AddOwner( o => o.Selection, (o, v) => o.Selection = v); /// /// Defines the property. /// public static readonly StyledProperty SelectionModeProperty = ListBox.SelectionModeProperty.AddOwner(); /// /// Defines the property. /// public static RoutedEvent SelectionChangedEvent = SelectingItemsControl.SelectionChangedEvent; private object _selectedItem; private ISelectionModel _selection; private readonly SelectedItemsSync _selectedItems; /// /// Initializes static members of the class. /// static TreeView() { // 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); } /// /// 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 ITreeItemContainerGenerator ItemContainerGenerator => (ITreeItemContainerGenerator)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. /// /// /// Gets or sets the selected item. /// public object SelectedItem { get => Selection.SelectedItem; set => Selection.SelectedIndex = IndexFromItem(value); } /// /// Gets or sets the selected items. /// protected IList SelectedItems { get => _selectedItems.GetOrCreateItems(); set => _selectedItems.SetItems(value); } /// /// Gets or sets a model holding the current selection. /// public ISelectionModel Selection { get => _selection; set { value ??= new SelectionModel { SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected), RetainSelectionOnReset = true, }; if (_selection != value) { 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 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; 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; } } } } } /// /// Expands the specified all descendent s. /// /// The item to expand. public void ExpandSubTree(TreeViewItem item) { item.IsExpanded = true; var panel = item.Presenter.Panel; if (panel != null) { foreach (var child in panel.Children) { if (child is TreeViewItem treeViewItem) { ExpandSubTree(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() => Selection.SelectAll(); /// /// Deselects all items in the . /// public void UnselectAll() => Selection.ClearSelection(); (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, NavigationDirection direction) { if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) { if (!this.IsVisualAncestorOf(element)) { IControl result = _selectedItem != null ? ItemContainerGenerator.Index.ContainerFromItem(_selectedItem) : ItemContainerGenerator.ContainerFromIndex(0); return (true, result); } return (true, null); } return (false, null); } /// protected override IItemContainerGenerator CreateItemContainerGenerator() { var result = new TreeItemContainerGenerator( this, TreeViewItem.HeaderProperty, TreeViewItem.ItemTemplateProperty, TreeViewItem.ItemsProperty, TreeViewItem.IsExpandedProperty); result.Index.Materialized += ContainerMaterialized; return result; } /// protected override void OnGotFocus(GotFocusEventArgs e) { if (e.NavigationMethod == NavigationMethod.Directional) { e.Handled = UpdateSelectionFromEventSource( e.Source, true, (e.KeyModifiers & KeyModifiers.Shift) != 0); } } 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 = ElementAt(Items, 0); } } if (!e.Handled) { var keymap = AvaloniaLocator.Current.GetService(); bool Match(List gestures) => gestures.Any(g => g.Matches(e)); if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) { SelectAll(); e.Handled = true; } } } /// /// Called when is raised. /// /// The sender. /// The event args. private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(SelectionModel.AnchorIndex) && AutoScrollToSelectedItem) { var container = ContainerFromIndex(Selection.AnchorIndex); if (container != null) { container.BringIntoView(); } } } /// /// Called when is raised. /// /// The sender. /// The event args. 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?.GetObservable(ItemsProperty); } private TreeViewItem GetContainerInDirection( TreeViewItem from, NavigationDirection direction, bool intoChildren) { IItemContainerGenerator parentGenerator = GetParentContainerGenerator(from); if (parentGenerator == null) { return null; } var index = parentGenerator.IndexFromContainer(from); var parent = from.Parent as ItemsControl; TreeViewItem result = null; switch (direction) { case NavigationDirection.Up: if (index > 0) { var previous = (TreeViewItem)parentGenerator.ContainerFromIndex(index - 1); result = previous.IsExpanded && previous.ItemCount > 0 ? (TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1) : previous; } else { result = from.Parent as TreeViewItem; } break; case NavigationDirection.Down: if (from.IsExpanded && intoChildren && from.ItemCount > 0) { result = (TreeViewItem)from.ItemContainerGenerator.ContainerFromIndex(0); } else if (index < parent?.ItemCount - 1) { result = (TreeViewItem)parentGenerator.ContainerFromIndex(index + 1); } else if (parent is TreeViewItem parentItem) { return GetContainerInDirection(parentItem, direction, false); } break; } return result; } protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e) { Selection.Source = Items; base.ItemsChanged(e); } /// protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); if (e.Source is IVisual source) { var point = e.GetCurrentPoint(source); if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed) { e.Handled = UpdateSelectionFromEventSource( e.Source, true, (e.KeyModifiers & KeyModifiers.Shift) != 0, (e.KeyModifiers & KeyModifiers.Control) != 0, point.Properties.IsRightButtonPressed); } } } protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) { base.OnPropertyChanged(property, oldValue, newValue, priority); if (property == SelectionModeProperty) { var mode = newValue.GetValueOrDefault(); Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple); Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected); } } /// /// 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( IControl container, bool select = true, bool rangeModifier = false, bool toggleModifier = false, bool rightButton = false) { var index = IndexFromContainer((TreeViewItem)container); if (index.GetSize() == 0) { return; } IControl selectedContainer = null; if (SelectedItem != null) { selectedContainer = ItemContainerGenerator.Index.ContainerFromItem(SelectedItem); } var mode = SelectionMode; var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0; var multi = (mode & SelectionMode.Multiple) != 0; var range = multi && selectedContainer != null && rangeModifier; if (!select) { Selection.DeselectAt(index); } else if (rightButton) { if (!Selection.IsSelectedAt(index)) { Selection.SelectedIndex = index; } } else if (!toggle && !range) { Selection.SelectedIndex = index; } else if (multi && range) { 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 { if (Selection.IsSelectedAt(index)) { Selection.DeselectAt(index); } else if (multi) { Selection.SelectAt(index); } else { Selection.SelectedIndex = index; } } } private static IItemContainerGenerator GetParentContainerGenerator(TreeViewItem item) { if (item == null) { return null; } switch (item.Parent) { case TreeView treeView: return treeView.ItemContainerGenerator; case TreeViewItem treeViewItem: return treeViewItem.ItemContainerGenerator; default: return null; } } /// /// 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( IInteractive 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(IInteractive eventSource) { var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() .OfType() .FirstOrDefault(); if (item != null) { if (item.ItemContainerGenerator.Index == ItemContainerGenerator.Index) { return item; } } return null; } /// /// Called when a new item container is materialized, to set its selected state. /// /// The event sender. /// The event args. private void ContainerMaterialized(object sender, ItemContainerEventArgs e) { var selectedItem = SelectedItem; if (selectedItem == null) { return; } foreach (var container in e.Containers) { if (container.Item == selectedItem) { ((TreeViewItem)container.ContainerControl).IsSelected = true; if (AutoScrollToSelectedItem) { Dispatcher.UIThread.Post(container.ContainerControl.BringIntoView); } break; } } } /// /// Sets a container's 'selected' class or . /// /// The container. /// Whether the control is selected private void MarkContainerSelected(IControl container, bool selected) { if (container == null) { return; } if (container is ISelectable selectable) { selectable.IsSelected = selected; } else { container.Classes.Set(":selected", selected); } } private void MarkContainersUnselected() { 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(); while (true) { if (container.Level == 0) { var treeView = container.FindAncestorOfType(); if (treeView == null) { return default; } result.Add(treeView.ItemContainerGenerator.IndexFromContainer(container)); result.Reverse(); return new IndexPath(result); } else { var parent = container.FindAncestorOfType(); 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; if (container != null) { return IndexFromContainer(container); } return default; } private TreeViewItem ContainerFromIndex(IndexPath index) { TreeViewItem treeViewItem = null; for (var i = 0; i < index.GetSize(); ++i) { var generator = treeViewItem?.ItemContainerGenerator ?? ItemContainerGenerator; treeViewItem = generator.ContainerFromIndex(index.GetAt(i)) as TreeViewItem; if (treeViewItem == null) { return null; } } return treeViewItem; } } }