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