// -----------------------------------------------------------------------
//
// 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();
}
}
}
}
}
}