// Copyright (c) The Perspex 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 Perspex.Collections; using Perspex.Controls.Generators; using Perspex.Input; using Perspex.Interactivity; using Perspex.Styling; using Perspex.VisualTree; namespace Perspex.Controls.Primitives { /// /// An that maintains a selection. /// public class SelectingItemsControl : ItemsControl { /// /// Defines the property. /// public static readonly PerspexProperty SelectedIndexProperty = PerspexProperty.RegisterDirect( nameof(SelectedIndex), o => o.SelectedIndex, (o, v) => o.SelectedIndex = v); /// /// Defines the property. /// public static readonly PerspexProperty SelectedItemProperty = PerspexProperty.RegisterDirect( nameof(SelectedItem), o => o.SelectedItem, (o, v) => o.SelectedItem = v); /// /// Defines the property. /// protected static readonly PerspexProperty> SelectedIndexesProperty = PerspexProperty.RegisterDirect>( nameof(SelectedIndexes), o => o.SelectedIndexes); /// /// Defines the property. /// protected static readonly PerspexProperty> SelectedItemsProperty = PerspexProperty.RegisterDirect>( nameof(SelectedItems), o => o.SelectedItems); /// /// Defines the property. /// protected static readonly PerspexProperty SelectionModeProperty = PerspexProperty.Register( nameof(SelectionMode)); /// /// 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); private PerspexList _selectedIndexes = new PerspexList(); private PerspexList _selectedItems = new PerspexList(); /// /// 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() { ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized); } /// /// Gets or sets the index of the selected item. /// public int SelectedIndex { get { return _selectedIndexes.Count > 0 ? _selectedIndexes[0]: -1; } set { var old = SelectedIndex; var effective = (value >= 0 && value < Items?.Cast().Count()) ? value : -1; if (old != effective) { _selectedIndexes.Clear(); if (effective != -1) { _selectedIndexes.Add(effective); } RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue); } } } /// /// Gets or sets the selected item. /// public object SelectedItem { get { return _selectedItems.FirstOrDefault(); } set { var old = SelectedItem; var effective = Items?.Cast().Contains(value) == true ? value : null; if (effective != old) { _selectedItems.Clear(); if (effective != null) { _selectedItems.Add(effective); } RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue); } } } /// /// Gets the selected indexes. /// protected IList SelectedIndexes { get { return _selectedIndexes; } } /// /// Gets the selected items. /// protected IList SelectedItems { get { return _selectedItems; } } /// /// Gets or sets the selection mode. /// protected SelectionMode SelectionMode { get { return GetValue(SelectionModeProperty); } set { SetValue(SelectionModeProperty, value); } } /// protected override void ItemsChanged(PerspexPropertyChangedEventArgs e) { base.ItemsChanged(e); if (SelectedIndex != -1) { SelectedIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem); } else if (SelectionMode == SelectionMode.SingleAlways && Items != null & Items.Cast().Any()) { SelectedIndex = 0; } } /// protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { base.ItemsCollectionChanged(sender, e); switch (e.Action) { case NotifyCollectionChangedAction.Add: if (SelectionMode == SelectionMode.SingleAlways && SelectedIndex == -1) { SelectedIndex = 0; } break; case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Replace: var selectedIndex = SelectedIndex; if (selectedIndex >= e.OldStartingIndex && selectedIndex < e.OldStartingIndex + e.OldItems.Count) { if (SelectionMode != SelectionMode.SingleAlways) { SelectedIndex = -1; } else { LostSelection(); } } break; case NotifyCollectionChangedAction.Reset: SelectedIndex = IndexOf(e.NewItems, SelectedItem); break; } } /// protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); if (e.NavigationMethod == NavigationMethod.Pointer || e.NavigationMethod == NavigationMethod.Directional) { TrySetSelectionFromContainerEvent(e.Source, true); } } /// protected override void OnPointerPressed(PointerPressEventArgs e) { base.OnPointerPressed(e); e.Handled = 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 (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"); } } } /// /// Called when new containers are initialized by the . /// /// The containers. private void ContainersInitialized(ItemContainers containers) { var selectedIndex = SelectedIndex; var selectedContainer = containers.Items.OfType().FirstOrDefault(x => x.IsSelected); if (selectedContainer != null) { 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) { 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 = ItemContainerGenerator.ContainerFromIndex(index); MarkContainerSelected(container, false); } index = (int)e.NewValue; if (index == -1) { SelectedItem = null; } else { SelectedItem = Items.Cast().ElementAt((int)e.NewValue); var container = ItemContainerGenerator.ContainerFromIndex(index); MarkContainerSelected(container, true); var inputElement = container as IInputElement; if (inputElement != null && Presenter != null && Presenter.Panel != null) { KeyboardNavigation.SetTabOnceActiveElement( (InputElement)Presenter.Panel, inputElement); } } } /// /// Called when the property changes. /// /// The event args. private void SelectedItemChanged(PerspexPropertyChangedEventArgs e) { SelectedIndex = IndexOf(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 = Items?.Cast(); if (items != null && SelectionMode == SelectionMode.SingleAlways) { var index = Math.Min(SelectedIndex, items.Count() - 1); if (index > -1) { SelectedItem = items.ElementAt(index); return; } } 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 = GetContainerFromEvent(eventSource); if (item != null) { var index = ItemContainerGenerator.IndexFromContainer(item); if (index != -1) { if (select) { SelectedIndex = index; } else { LostSelection(); } } } } } }