// 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.
///
///
///
/// provides a base class for s
/// that maintain a selection (single or multiple). By default only its
/// and properties are visible; the
/// current multiple selection together with the
/// properties are protected, however a derived class can expose
/// these if it wishes to support multiple selection.
///
///
/// maintains a selection respecting the current
/// but it does not react to user input; this must be handled in a
/// derived class. It does, however, respond to events
/// from items and updates the selection accordingly.
///
///
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> SelectedItemsProperty =
PerspexProperty.RegisterDirect>(
nameof(SelectedItems),
o => o.SelectedItems,
(o, v) => o.SelectedItems = v);
///
/// 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 int _selectedIndex = -1;
private object _selectedItem;
private IList _selectedItems;
private bool _ignoreContainerSelectionChanged;
///
/// Initializes static members of the class.
///
static SelectingItemsControl()
{
IsSelectedChangedEvent.AddClassHandler(x => x.ContainerSelectionChanged);
}
///
/// 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 _selectedIndex;
}
set
{
var old = SelectedIndex;
var effective = (value >= 0 && value < Items?.Cast().Count()) ? value : -1;
if (old != effective)
{
_selectedIndex = effective;
RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue);
SelectedItem = ElementAt(Items, effective);
}
}
}
///
/// Gets or sets the selected item.
///
public object SelectedItem
{
get
{
return _selectedItem;
}
set
{
var old = SelectedItem;
var index = IndexOf(Items, value);
var effective = index != -1 ? value : null;
if (effective != old)
{
_selectedItem = effective;
RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue);
SelectedIndex = index;
if (effective != null)
{
if (SelectedItems.Count != 1 || SelectedItems[0] != effective)
{
SelectedItems.Clear();
SelectedItems.Add(effective);
}
}
else
{
SelectedItems.Clear();
}
}
}
}
///
/// Gets the selected items.
///
protected IList SelectedItems
{
get
{
if (_selectedItems == null)
{
_selectedItems = new PerspexList();
SubscribeToSelectedItems();
}
return _selectedItems;
}
set
{
if (value != null)
{
UnsubscribeFromSelectedItems();
_selectedItems = value;
SubscribeToSelectedItems();
}
}
}
///
/// Gets or sets the selection mode.
///
protected SelectionMode SelectionMode
{
get { return GetValue(SelectionModeProperty); }
set { SetValue(SelectionModeProperty, value); }
}
///
/// Gets a value indicating whether is set.
///
protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
///
/// 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.
protected IControl GetContainerFromEventSource(IInteractive eventSource)
{
var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
.OfType()
.FirstOrDefault(x => x.LogicalParent == this);
return item as IControl;
}
///
protected override void ItemsChanged(PerspexPropertyChangedEventArgs e)
{
base.ItemsChanged(e);
if (SelectedIndex != -1)
{
SelectedIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem);
}
else if (AlwaysSelected && 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 (AlwaysSelected && 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 (!AlwaysSelected)
{
SelectedIndex = -1;
}
else
{
LostSelection();
}
}
break;
case NotifyCollectionChangedAction.Reset:
SelectedIndex = IndexOf(e.NewItems, SelectedItem);
break;
}
}
///
/// Updates the selection for an item based on user interaction.
///
/// The index of the item.
/// Whether the item should be selected or unselected.
/// Whether the range modifier is enabled (i.e. shift key).
/// Whether the toggle modifier is enabled (i.e. ctrl key).
protected void UpdateSelection(
int index,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
{
if (index != -1)
{
if (select)
{
var mode = SelectionMode;
var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && SelectedIndex != -1 ? rangeModifier : false;
if (!toggle && !range)
{
SelectedIndex = index;
}
else if (multi && range)
{
SynchronizeItems(
SelectedItems,
GetRange(Items, SelectedIndex, index));
}
else
{
var item = ElementAt(Items, index);
var i = SelectedItems.IndexOf(item);
if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1))
{
SelectedItems.Remove(item);
}
else
{
if (multi)
{
SelectedItems.Add(item);
}
else
{
SelectedIndex = index;
}
}
}
if (Presenter?.Panel != null)
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
KeyboardNavigation.SetTabOnceActiveElement(
(InputElement)Presenter.Panel,
container);
}
}
else
{
LostSelection();
}
}
}
///
/// Updates the selection for a container based on user interaction.
///
/// The container.
/// Whether the container should be selected or unselected.
/// Whether the range modifier is enabled (i.e. shift key).
/// Whether the toggle modifier is enabled (i.e. ctrl key).
protected void UpdateSelection(
IControl container,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
{
var index = ItemContainerGenerator.IndexFromContainer(container);
if (index != -1)
{
UpdateSelection(index, select, rangeModifier, toggleModifier);
}
}
///
/// Updates the selection based on an event source that may have originated in a container
/// that belongs to the control.
///
/// The control that raised the event.
/// Whether the container should be selected or unselected.
/// Whether the range modifier is enabled (i.e. shift key).
/// Whether the toggle modifier is enabled (i.e. ctrl key).
///
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
///
protected bool UpdateSelectionFromEventSource(
IInteractive eventSource,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
{
var item = GetContainerFromEventSource(eventSource);
if (item != null)
{
UpdateSelection(item, select, rangeModifier, toggleModifier);
return true;
}
return false;
}
///
/// Gets the item at the specified index in a collection.
///
/// The collection.
/// The index.
/// The index of the item or -1 if the item was not found.
private static object ElementAt(IEnumerable items, int index)
{
var typedItems = items?.Cast();
if (index != -1 && typedItems != null && index < typedItems.Count())
{
return typedItems.ElementAt(index) ?? null;
}
else
{
return null;
}
}
///
/// 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;
}
///
/// Gets a range of items from an IEnumerable.
///
/// The items.
/// The index of the first item.
/// The index of the last item.
/// The items.
private static IEnumerable GetRange(IEnumerable items, int first, int last)
{
var list = (items as IList) ?? items.Cast().ToList();
int step = first > last ? -1 : 1;
for (int i = first; i != last; i += step)
{
yield return list[i];
}
yield return list[last];
}
///
/// Makes a list of objects equal another.
///
/// The items collection.
/// The desired items.
private static void SynchronizeItems(IList items, IEnumerable desired)
{
int index = 0;
foreach (var i in desired)
{
if (index < items.Count)
{
if (items[index] != i)
{
items[index] = i;
}
}
else
{
items.Add(i);
}
++index;
}
while (index < items.Count)
{
items.RemoveAt(items.Count - 1);
}
}
///
/// 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)
{
if (!_ignoreContainerSelectionChanged)
{
var selectable = (ISelectable)e.Source;
if (selectable != null)
{
UpdateSelectionFromEventSource(e.Source, selectable.IsSelected);
}
}
}
///
/// 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 && AlwaysSelected)
{
var index = Math.Min(SelectedIndex, items.Count() - 1);
if (index > -1)
{
SelectedItem = items.ElementAt(index);
return;
}
}
SelectedIndex = -1;
}
///
/// Sets a container's 'selected' class or .
///
/// The container.
/// Whether the control is selected
private void MarkContainerSelected(IControl container, bool selected)
{
try
{
var selectable = container as ISelectable;
var styleable = container as IStyleable;
_ignoreContainerSelectionChanged = true;
if (selectable != null)
{
selectable.IsSelected = selected;
}
else if (styleable != null)
{
if (selected)
{
styleable.Classes.Add(":selected");
}
else
{
styleable.Classes.Remove(":selected");
}
}
}
finally
{
_ignoreContainerSelectionChanged = false;
}
}
///
/// Sets an item container's 'selected' class or .
///
/// The index of the item.
/// Whether the item should be selected or deselected.
private void MarkItemSelected(int index, bool selected)
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
if (container != null)
{
MarkContainerSelected(container, selected);
}
}
///
/// Sets an item container's 'selected' class or .
///
/// The item.
/// Whether the item should be selected or deselected.
private void MarkItemSelected(object item, bool selected)
{
var index = IndexOf(Items, item);
if (index != -1)
{
MarkItemSelected(index, selected);
}
}
///
/// Called when the CollectionChanged event is raised.
///
/// The event sender.
/// The event args.
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
SelectedItemsAdded(e.NewItems.Cast().ToList());
break;
case NotifyCollectionChangedAction.Remove:
if (SelectedItems.Count == 0)
{
SelectedIndex = -1;
}
else
{
foreach (var item in e.OldItems)
{
MarkItemSelected(item, false);
}
}
break;
case NotifyCollectionChangedAction.Reset:
foreach (var item in ItemContainerGenerator.Containers)
{
MarkContainerSelected(item, false);
}
SelectedIndex = -1;
SelectedItemsAdded(SelectedItems);
break;
case NotifyCollectionChangedAction.Replace:
foreach (var item in e.OldItems)
{
MarkItemSelected(item, false);
}
foreach (var item in e.NewItems)
{
MarkItemSelected(item, true);
}
if (SelectedItem != SelectedItems[0])
{
var oldItem = SelectedItem;
var oldIndex = SelectedIndex;
var item = SelectedItems[0];
var index = IndexOf(Items, item);
_selectedIndex = index;
_selectedItem = item;
RaisePropertyChanged(SelectedIndexProperty, oldIndex, index, BindingPriority.LocalValue);
RaisePropertyChanged(SelectedItemProperty, oldItem, item, BindingPriority.LocalValue);
}
break;
}
}
///
/// Called when items are added to the collection.
///
/// The added items.
private void SelectedItemsAdded(IList items)
{
if (items.Count > 0)
{
foreach (var item in items)
{
MarkItemSelected(item, true);
}
if (SelectedItem == null)
{
var index = IndexOf(Items, items[0]);
if (index != -1)
{
_selectedItem = items[0];
_selectedIndex = index;
RaisePropertyChanged(SelectedIndexProperty, -1, index, BindingPriority.LocalValue);
RaisePropertyChanged(SelectedItemProperty, null, items[0], BindingPriority.LocalValue);
}
}
}
}
///
/// Subscribes to the CollectionChanged event, if any.
///
private void SubscribeToSelectedItems()
{
var incc = _selectedItems as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged += SelectedItemsCollectionChanged;
}
SelectedItemsCollectionChanged(
_selectedItems,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
///
/// Unsubscribes from the CollectionChanged event, if any.
///
private void UnsubscribeFromSelectedItems()
{
var incc = _selectedItems as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged -= SelectedItemsCollectionChanged;
}
}
}
}