// ----------------------------------------------------------------------- // // 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.Utils; using Perspex.Input; using Perspex.Interactivity; 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.ItemIsSelectedChanged); SelectedIndexProperty.Changed.Subscribe(SelectedIndexChanged); SelectedItemProperty.Changed.Subscribe(SelectedItemChanged); } /// /// 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); } } /// /// Called when the property changes. /// /// The old value of the property. /// The new value of the property. protected override void ItemsChanged(IEnumerable oldValue, IEnumerable newValue) { base.ItemsChanged(oldValue, newValue); var selected = this.SelectedItem; if (selected != null) { if (newValue == null || !newValue.Contains(selected)) { this.SelectedItem = null; } } } /// /// Called when a event is raised /// on . /// /// The event sender. /// The event args. protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { base.ItemsCollectionChanged(sender, e); var selected = this.SelectedItem; switch (e.Action) { case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Reset: if (e.OldItems.Contains(selected)) { this.SelectedItem = null; } break; case NotifyCollectionChangedAction.Move: this.SelectedItem = this.Items.IndexOf(selected); break; } } /// /// Called when the selection on a child item changes. /// /// The event args. protected virtual void ItemIsSelectedChanged(RoutedEventArgs e) { var selectable = e.Source as ISelectable; if (selectable != null && selectable != this && selectable.IsSelected) { var container = this.ItemContainerGenerator.GetItemForContainer((Control)selectable); if (container != null) { this.SelectedItem = container; e.Handled = true; } } } /// /// Moves the selection in the specified direction. /// /// The direction. protected virtual void MoveSelection(FocusNavigationDirection direction) { var panel = this.Presenter?.Panel as INavigablePanel; var selected = this.SelectedItem; var container = selected != null ? this.ItemContainerGenerator.GetContainerForItem(selected) : null; if (panel != null) { var next = panel.GetControl(direction, container); if (next != null) { this.SelectedItem = this.ItemContainerGenerator.GetItemForContainer(next); } } else { // TODO: Try doing a visual search? } } /// /// Called when a key is pressed within the control. /// /// The event args. protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (!e.Handled) { switch (e.Key) { case Key.Up: this.MoveSelection(FocusNavigationDirection.Up); break; case Key.Down: this.MoveSelection(FocusNavigationDirection.Down); break; case Key.Left: this.MoveSelection(FocusNavigationDirection.Left); break; case Key.Right: this.MoveSelection(FocusNavigationDirection.Right); break; default: return; } var selected = this.SelectedItem; if (selected != null) { var container = this.ItemContainerGenerator.GetContainerForItem(selected); if (container != null) { container.BringIntoView(); FocusManager.Instance.Focus(container, true); } } e.Handled = true; } } /// /// Called when the pointer is pressed within the control. /// /// The event args. protected override void OnPointerPressed(PointerPressEventArgs e) { IVisual source = (IVisual)e.Source; var selectable = source.GetVisualAncestors() .OfType() .OfType() .FirstOrDefault(); if (selectable != null) { var item = this.ItemContainerGenerator.GetItemForContainer(selectable); if (item != null) { this.SelectedItem = item; selectable.BringIntoView(); FocusManager.Instance.Focus(selectable); } } e.Handled = true; } /// /// Called when the control's template has been applied. /// protected override void OnTemplateApplied() { base.OnTemplateApplied(); this.SelectedItemChanged(this.SelectedItem); } /// /// Provides coercion for the property. /// /// The object on which the property has changed. /// The proposed value. /// The coerced value. private static int ValidateSelectedIndex(PerspexObject o, int value) { var control = o as SelectingItemsControl; if (control != null) { if (value < -1) { return -1; } else if (value > -1) { var items = control.Items; if (items != null) { var count = items.Count(); return Math.Min(value, count - 1); } else { return -1; } } } return value; } /// /// Provides coercion for the property. /// /// The object on which the property has changed. /// The proposed value. /// The coerced value. private static object ValidateSelectedItem(PerspexObject o, object value) { var control = o as SelectingItemsControl; if (control != null) { if (value != null && (control.Items == null || control.Items.IndexOf(value) == -1)) { return null; } } return value; } /// /// Called when the property changes. /// /// The event args. private static void SelectedIndexChanged(PerspexPropertyChangedEventArgs e) { var control = e.Sender as SelectingItemsControl; if (control != null) { var index = (int)e.NewValue; if (index == -1) { control.SelectedItem = null; } else { control.SelectedItem = control.Items.ElementAt((int)e.NewValue); } } } /// /// Called when the property changes. /// /// The event args. private static void SelectedItemChanged(PerspexPropertyChangedEventArgs e) { var control = e.Sender as SelectingItemsControl; if (control != null) { control.SelectedItemChanged(e.NewValue); } } /// /// Called when the property changes. /// /// The new selected item. private void SelectedItemChanged(object selected) { var containers = this.ItemContainerGenerator.GetAll() .Select(x => x.Item2) .OfType(); var selectedContainer = (selected != null) ? this.ItemContainerGenerator.GetContainerForItem(selected) : null; if (this.Presenter != null && this.Presenter.Panel != null) { KeyboardNavigation.SetTabOnceActiveElement(this.Presenter.Panel, selectedContainer); } foreach (var item in containers) { item.IsSelected = item == selectedContainer; } if (selected == null) { this.SelectedIndex = -1; } else { var items = this.Items; if (items != null) { this.SelectedIndex = items.IndexOf(selected); } } } } }