// ----------------------------------------------------------------------- // // 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 abstract class SelectingItemsControl : ItemsControl { /// /// 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 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(IEnumerable oldValue, IEnumerable newValue) { base.ItemsChanged(oldValue, newValue); this.SelectedIndex = IndexOf(newValue, this.SelectedItem); } /// protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { base.ItemsCollectionChanged(sender, e); switch (e.Action) { case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Replace: var selectedIndex = this.SelectedIndex; if (selectedIndex >= e.OldStartingIndex && selectedIndex < e.OldStartingIndex + e.OldItems.Count) { this.SelectedIndex = -1; } 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); } /// /// 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) { this.TrySetSelectionFromContainerEvent(e.Source); } /// /// 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); } } /// /// Called when the property changes. /// /// The event args. private void SelectedItemChanged(PerspexPropertyChangedEventArgs e) { this.SelectedIndex = IndexOf(this.Items, e.NewValue); } /// /// Tries to set the selection to a container that raised an event. /// /// The control that raised the event. private void TrySetSelectionFromContainerEvent(IInteractive eventSource) { var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() .OfType() .FirstOrDefault(x => x.LogicalParent == this); if (item != null) { var index = this.ItemContainerGenerator.IndexFromContainer((IControl)item); if (index != -1) { this.SelectedIndex = index; } } } } }