// ----------------------------------------------------------------------- // // Copyright 2014 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- namespace Perspex.Controls.Primitives { using System; using System.Collections; using System.Collections.Specialized; using System.Linq; using Perspex.Controls.Generators; using Perspex.Input; using Perspex.Interactivity; using Perspex.Styling; using Perspex.VisualTree; /// /// An that maintains a selection. /// /// /// TODO: Support multiple selection. /// public class SelectingItemsControl : ItemsControl { /// /// Defines the property. /// public static readonly PerspexProperty AutoSelectProperty = PerspexProperty.Register("AutoSelect"); /// /// Defines the property. /// public static readonly PerspexProperty SelectedIndexProperty = PerspexProperty.Register( nameof(SelectedIndex), defaultValue: -1, validate: ValidateSelectedIndex); /// /// Defines the property. /// public static readonly PerspexProperty SelectedItemProperty = PerspexProperty.Register( nameof(SelectedItem), validate: ValidateSelectedItem); /// /// Event that should be raised by items that implement to /// notify the parent that their selection state /// has changed. /// public static readonly RoutedEvent IsSelectedChangedEvent = RoutedEvent.Register("IsSelectedChanged", RoutingStrategies.Bubble); /// /// Initializes static members of the class. /// static SelectingItemsControl() { IsSelectedChangedEvent.AddClassHandler(x => x.ContainerSelectionChanged); SelectedIndexProperty.Changed.AddClassHandler(x => x.SelectedIndexChanged); SelectedItemProperty.Changed.AddClassHandler(x => x.SelectedItemChanged); } /// /// Initializes a new instance of the class. /// public SelectingItemsControl() { this.ItemContainerGenerator.ContainersInitialized.Subscribe(this.ContainersInitialized); } /// /// Gets or sets a value indicating whether the control should always try to keep an item /// selected where possible. /// public bool AutoSelect { get { return this.GetValue(AutoSelectProperty); } set { this.SetValue(AutoSelectProperty, value); } } /// /// Gets or sets the index of the selected item. /// public int SelectedIndex { get { return this.GetValue(SelectedIndexProperty); } set { this.SetValue(SelectedIndexProperty, value); } } /// /// Gets or sets the selected item. /// public object SelectedItem { get { return this.GetValue(SelectedItemProperty); } set { this.SetValue(SelectedItemProperty, value); } } /// protected override void ItemsChanged(PerspexPropertyChangedEventArgs e) { base.ItemsChanged(e); if (this.SelectedIndex != -1) { this.SelectedIndex = IndexOf((IEnumerable)e.NewValue, this.SelectedItem); } else if (this.AutoSelect && this.Items != null & this.Items.Cast().Any()) { this.SelectedIndex = 0; } } /// protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { base.ItemsCollectionChanged(sender, e); switch (e.Action) { case NotifyCollectionChangedAction.Add: if (this.AutoSelect && this.SelectedIndex == -1) { this.SelectedIndex = 0; } break; case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Replace: var selectedIndex = this.SelectedIndex; if (selectedIndex >= e.OldStartingIndex && selectedIndex < e.OldStartingIndex + e.OldItems.Count) { if (!this.AutoSelect) { this.SelectedIndex = -1; } else { this.LostSelection(); } } break; case NotifyCollectionChangedAction.Reset: this.SelectedIndex = IndexOf(e.NewItems, this.SelectedItem); break; } } /// protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); this.TrySetSelectionFromContainerEvent(e.Source, true); } /// /// Gets the index of an item in a collection. /// /// The collection. /// The item. /// The index of the item or -1 if the item was not found. private static int IndexOf(IEnumerable items, object item) { if (items != null && item != null) { var list = items as IList; if (list != null) { return list.IndexOf(item); } else { int index = 0; foreach (var i in items) { if (object.Equals(i, item)) { return index; } ++index; } } } return -1; } /// /// Sets a container's 'selected' class or . /// /// The container. /// Whether the control is selected private static void MarkContainerSelected(IControl container, bool selected) { var selectable = container as ISelectable; var styleable = container as IStyleable; if (selectable != null) { selectable.IsSelected = selected; } else if (styleable != null) { if (selected) { styleable.Classes.Add("selected"); } else { styleable.Classes.Remove("selected"); } } } /// /// Coerces the property. /// /// The object with the property. /// The proposed value of the property. /// The final value of the property. private static int ValidateSelectedIndex(SelectingItemsControl sender, int index) { var items = sender.Items; return (index >= 0 && index < items?.Cast().Count()) ? index : -1; } /// /// Coerces the property. /// /// The object with the property. /// The proposed value of the property. /// The final value of the property. private static object ValidateSelectedItem(SelectingItemsControl sender, object item) { var items = sender.Items; return items?.Cast().Contains(item) == true ? item : null; } /// /// Called when new containers are initialized by the . /// /// The containers. private void ContainersInitialized(ItemContainers containers) { var selectedIndex = this.SelectedIndex; var selectedContainer = containers.Items.OfType().FirstOrDefault(x => x.IsSelected); if (selectedContainer != null) { this.SelectedIndex = containers.Items.IndexOf((IControl)selectedContainer) + containers.StartingIndex; } else if (selectedIndex >= containers.StartingIndex && selectedIndex < containers.StartingIndex + containers.Items.Count) { var container = containers.Items[selectedIndex - containers.StartingIndex]; MarkContainerSelected(container, true); } } /// /// Called when a container raises the . /// /// The event. private void ContainerSelectionChanged(RoutedEventArgs e) { var selectable = (ISelectable)e.Source; if (selectable != null) { this.TrySetSelectionFromContainerEvent(e.Source, selectable.IsSelected); } } /// /// Called when the property changes. /// /// The event args. private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e) { var index = (int)e.OldValue; if (index != -1) { var container = this.ItemContainerGenerator.ContainerFromIndex(index); MarkContainerSelected(container, false); } index = (int)e.NewValue; if (index == -1) { this.SelectedItem = null; } else { this.SelectedItem = this.Items.Cast().ElementAt((int)e.NewValue); var container = this.ItemContainerGenerator.ContainerFromIndex(index); MarkContainerSelected(container, true); var inputElement = container as IInputElement; if (inputElement != null && this.Presenter != null && this.Presenter.Panel != null) { KeyboardNavigation.SetTabOnceActiveElement(this.Presenter.Panel, inputElement); } } } /// /// Called when the property changes. /// /// The event args. private void SelectedItemChanged(PerspexPropertyChangedEventArgs e) { this.SelectedIndex = IndexOf(this.Items, e.NewValue); } /// /// 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. private IControl GetContainerFromEvent(IInteractive eventSource) { var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() .OfType() .FirstOrDefault(x => x.LogicalParent == this); return item as IControl; } /// /// Called when the currently selected item is lost and the selection must be changed /// depending on the property. /// private void LostSelection() { var items = this.Items?.Cast(); if (items != null && this.AutoSelect) { var index = Math.Min(this.SelectedIndex, items.Count() - 1); if (index > -1) { this.SelectedItem = items.ElementAt(index); return; } } this.SelectedIndex = -1; } /// /// Tries to set the selection to a container that raised an event. /// /// The control that raised the event. /// Whether the container should be selected or unselected. private void TrySetSelectionFromContainerEvent(IInteractive eventSource, bool select) { var item = this.GetContainerFromEvent(eventSource); if (item != null) { var index = this.ItemContainerGenerator.IndexFromContainer(item); if (index != -1) { if (select) { this.SelectedIndex = index; } else { this.LostSelection(); } } } } } }