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