| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850 |
- // Copyright (c) The Avalonia Project. All rights reserved.
- // Licensed under the MIT license. See licence.md file in the project root for full license information.
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Collections.Specialized;
- using System.Linq;
- using Avalonia.Collections;
- using Avalonia.Controls.Generators;
- using Avalonia.Controls.Primitives;
- using Avalonia.Input;
- using Avalonia.Input.Platform;
- using Avalonia.Interactivity;
- using Avalonia.Threading;
- using Avalonia.VisualTree;
- namespace Avalonia.Controls
- {
- /// <summary>
- /// Displays a hierarchical tree of data.
- /// </summary>
- public class TreeView : ItemsControl, ICustomKeyboardNavigation
- {
- /// <summary>
- /// Defines the <see cref="AutoScrollToSelectedItem"/> property.
- /// </summary>
- public static readonly StyledProperty<bool> AutoScrollToSelectedItemProperty =
- SelectingItemsControl.AutoScrollToSelectedItemProperty.AddOwner<TreeView>();
- /// <summary>
- /// Defines the <see cref="SelectedItem"/> property.
- /// </summary>
- public static readonly DirectProperty<TreeView, object> SelectedItemProperty =
- SelectingItemsControl.SelectedItemProperty.AddOwner<TreeView>(
- o => o.SelectedItem,
- (o, v) => o.SelectedItem = v);
- /// <summary>
- /// Defines the <see cref="SelectedItems"/> property.
- /// </summary>
- public static readonly DirectProperty<TreeView, IList> SelectedItemsProperty =
- ListBox.SelectedItemsProperty.AddOwner<TreeView>(
- o => o.SelectedItems,
- (o, v) => o.SelectedItems = 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>();
- private object _selectedItem;
- private IList _selectedItems;
- /// <summary>
- /// Initializes static members of the <see cref="TreeView"/> class.
- /// </summary>
- static TreeView()
- {
- // HACK: Needed or SelectedItem property will not be found in Release build.
- }
- /// <summary>
- /// Occurs when the control's selection changes.
- /// </summary>
- public event EventHandler<SelectionChangedEventArgs> SelectionChanged
- {
- add => AddHandler(SelectingItemsControl.SelectionChangedEvent, value);
- remove => RemoveHandler(SelectingItemsControl.SelectionChangedEvent, value);
- }
- /// <summary>
- /// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
- /// </summary>
- public new ITreeItemContainerGenerator ItemContainerGenerator =>
- (ITreeItemContainerGenerator)base.ItemContainerGenerator;
- /// <summary>
- /// Gets or sets a value indicating whether to automatically scroll to newly selected items.
- /// </summary>
- public bool AutoScrollToSelectedItem
- {
- get => GetValue(AutoScrollToSelectedItemProperty);
- set => SetValue(AutoScrollToSelectedItemProperty, value);
- }
- private bool _syncingSelectedItems;
- /// <summary>
- /// Gets or sets the selection mode.
- /// </summary>
- public SelectionMode SelectionMode
- {
- get => GetValue(SelectionModeProperty);
- set => SetValue(SelectionModeProperty, value);
- }
- /// <summary>
- /// Gets or sets the selected item.
- /// </summary>
- public object SelectedItem
- {
- get => _selectedItem;
- set
- {
- var selectedItems = SelectedItems;
- SetAndRaise(SelectedItemProperty, ref _selectedItem, 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 the selected items.
- /// </summary>
- public IList SelectedItems
- {
- get
- {
- if (_selectedItems == null)
- {
- _selectedItems = new AvaloniaList<object>();
- 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<object>();
- SubscribeToSelectedItems();
- }
- }
- /// <summary>
- /// Expands the specified <see cref="TreeViewItem"/> all descendent <see cref="TreeViewItem"/>s.
- /// </summary>
- /// <param name="item">The item to expand.</param>
- 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);
- }
- }
- }
- }
- /// <summary>
- /// Selects all items in the <see cref="TreeView"/>.
- /// </summary>
- /// <remarks>
- /// 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);
- }
- /// <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,
- added ?? Empty,
- removed ?? 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;
- }
- }
- (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);
- }
- /// <inheritdoc/>
- protected override IItemContainerGenerator CreateItemContainerGenerator()
- {
- var result = new TreeItemContainerGenerator<TreeViewItem>(
- this,
- TreeViewItem.HeaderProperty,
- TreeViewItem.ItemTemplateProperty,
- TreeViewItem.ItemsProperty,
- TreeViewItem.IsExpandedProperty,
- new TreeContainerIndex());
- result.Index.Materialized += ContainerMaterialized;
- return result;
- }
- /// <inheritdoc/>
- protected override void OnGotFocus(GotFocusEventArgs e)
- {
- if (e.NavigationMethod == NavigationMethod.Directional)
- {
- e.Handled = UpdateSelectionFromEventSource(
- e.Source,
- true,
- (e.InputModifiers & InputModifiers.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<PlatformHotkeyConfiguration>();
- bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
- if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
- {
- SelectAll();
- e.Handled = true;
- }
- }
- }
- 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;
- }
- /// <inheritdoc/>
- protected override void OnPointerPressed(PointerPressedEventArgs e)
- {
- base.OnPointerPressed(e);
- if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right)
- {
- e.Handled = UpdateSelectionFromEventSource(
- e.Source,
- true,
- (e.InputModifiers & InputModifiers.Shift) != 0,
- (e.InputModifiers & InputModifiers.Control) != 0,
- e.MouseButton == MouseButton.Right);
- }
- }
- /// <summary>
- /// Updates the selection for an item based on user interaction.
- /// </summary>
- /// <param name="container">The container.</param>
- /// <param name="select">Whether the item should be selected or unselected.</param>
- /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
- /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
- /// <param name="rightButton">Whether the event is a right-click.</param>
- protected void UpdateSelectionFromContainer(
- IControl container,
- bool select = true,
- bool rangeModifier = false,
- bool toggleModifier = false,
- bool rightButton = false)
- {
- var item = ItemContainerGenerator.Index.ItemFromContainer(container);
- if (item == null)
- {
- 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 (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;
- }
- }
- }
- }
- 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;
- }
- }
- /// <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.
- /// </summary>
- /// <param name="eventSource">The control that raised the event.</param>
- /// <param name="select">Whether the container should be selected or unselected.</param>
- /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
- /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
- /// <param name="rightButton">Whether the event is a right-click.</param>
- /// <returns>
- /// True if the event originated from a container that belongs to the control; otherwise
- /// false.
- /// </returns>
- 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;
- }
- /// <summary>
- /// Tries to get the container that was the source of an event.
- /// </summary>
- /// <param name="eventSource">The control that raised the event.</param>
- /// <returns>The container or null if the event did not originate in a container.</returns>
- protected TreeViewItem GetContainerFromEventSource(IInteractive eventSource)
- {
- var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
- .OfType<TreeViewItem>()
- .FirstOrDefault();
- if (item != null)
- {
- if (item.ItemContainerGenerator.Index == ItemContainerGenerator.Index)
- {
- return item;
- }
- }
- return null;
- }
- /// <summary>
- /// Called when a new item container is materialized, to set its selected state.
- /// </summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event args.</param>
- 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;
- }
- }
- }
- /// <summary>
- /// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
- /// </summary>
- /// <param name="container">The container.</param>
- /// <param name="selected">Whether the control is selected</param>
- 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);
- }
- }
- /// <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)
- {
- var list = items.Cast<object>().ToList();
- 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);
- }
- }
- }
- }
|