Browse Source

Make selecting ranges work.

pull/278/head
Steven Kirk 11 years ago
parent
commit
7d73960ef6
  1. 2
      samples/XamlTestApplicationPcl/Views/MainWindow.paml
  2. 8
      src/Perspex.Base/Collections/IPerspexList.cs
  3. 47
      src/Perspex.Base/Collections/PerspexList.cs
  4. 61
      src/Perspex.Base/Collections/PerspexListExtensions.cs
  5. 8
      src/Perspex.Controls/ListBox.cs
  6. 117
      src/Perspex.Controls/Primitives/SelectingItemsControl.cs
  7. 4
      src/Perspex.Controls/Primitives/TabStrip.cs
  8. 63
      tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

2
samples/XamlTestApplicationPcl/Views/MainWindow.paml

@ -49,7 +49,7 @@
</TabItem>
<TabItem Header="Lists">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<ListBox Items="{Binding Items}" SelectionMode="Toggle">
<ListBox Items="{Binding Items}" SelectionMode="Multiple">
<ListBox.DataTemplates>
<DataTemplate DataType="vm:TestItem">
<StackPanel>

8
src/Perspex.Base/Collections/IPerspexList.cs

@ -16,6 +16,14 @@ namespace Perspex.Collections
/// </summary>
new int Count { get; }
/// <summary>
/// Gets or sets the element at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The element at the requested index.</returns>
new T this[int index] { get; set; }
/// <summary>
/// Adds multiple items to the collection.
/// </summary>

47
src/Perspex.Base/Collections/PerspexList.cs

@ -187,17 +187,22 @@ namespace Perspex.Collections
{
Contract.Requires<ArgumentNullException>(items != null);
if (Validate != null)
var list = (items as IList) ?? items.ToList();
if (list.Count > 0)
{
foreach (var item in items)
if (Validate != null)
{
Validate(item);
foreach (var item in list)
{
Validate((T)item);
}
}
}
int index = _inner.Count;
_inner.AddRange(items);
NotifyAdd((items as IList) ?? items.ToList(), index);
int index = _inner.Count;
_inner.AddRange(items);
NotifyAdd(list, index);
}
}
/// <summary>
@ -205,9 +210,12 @@ namespace Perspex.Collections
/// </summary>
public void Clear()
{
var old = _inner;
_inner = new List<T>();
NotifyReset(old);
if (this.Count > 0)
{
var old = _inner;
_inner = new List<T>();
NotifyReset(old);
}
}
/// <summary>
@ -272,16 +280,21 @@ namespace Perspex.Collections
{
Contract.Requires<ArgumentNullException>(items != null);
if (Validate != null)
var list = (items as IList) ?? items.ToList();
if (list.Count > 0)
{
foreach (var item in items)
if (Validate != null)
{
Validate(item);
foreach (var item in list)
{
Validate((T)item);
}
}
}
_inner.InsertRange(index, items);
NotifyAdd((items as IList) ?? items.ToList(), index);
_inner.InsertRange(index, items);
NotifyAdd((items as IList) ?? items.ToList(), index);
}
}
/// <summary>
@ -311,7 +324,7 @@ namespace Perspex.Collections
{
Contract.Requires<ArgumentNullException>(items != null);
List<T> removed = new List<T>();
var list = (items as IList) ?? items.ToList();
foreach (var i in items)
{

61
src/Perspex.Base/Collections/PerspexListExtensions.cs

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
namespace Perspex.Collections
@ -133,6 +134,66 @@ namespace Perspex.Collections
return Disposable.Create(() => collection.CollectionChanged -= handler);
}
/// <summary>
/// Invokes an action for each item in a collection and subsequently each item added or
/// removed from the collection.
/// </summary>
/// <typeparam name="T">The type of the collection items.</typeparam>
/// <param name="collection">The collection.</param>
/// <param name="added">
/// An action called initially with all items in the collection and subsequently with a
/// list of items added to the collection. The parameters passed are the index of the
/// first item added to the collection and the items added.
/// </param>
/// <param name="removed">
/// An action called with all items removed from the collection. The parameters passed
/// are the index of the first item removed from the collection and the items removed.
/// </param>
/// <param name="reset">
/// An action called when the collection is reset.
/// </param>
/// <returns>A disposable used to terminate the subscription.</returns>
public static IDisposable ForEachItem<T>(
this IPerspexReadOnlyList<T> collection,
Action<int, IEnumerable<T>> added,
Action<int, IEnumerable<T>> removed,
Action reset)
{
NotifyCollectionChangedEventHandler handler = (_, e) =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
added(e.NewStartingIndex, e.NewItems.Cast<T>());
break;
case NotifyCollectionChangedAction.Replace:
removed(e.OldStartingIndex, e.OldItems.Cast<T>());
added(e.NewStartingIndex, e.NewItems.Cast<T>());
break;
case NotifyCollectionChangedAction.Remove:
removed(e.OldStartingIndex, e.OldItems.Cast<T>());
break;
case NotifyCollectionChangedAction.Reset:
if (reset == null)
{
throw new InvalidOperationException(
"Reset called on collection without reset handler.");
}
reset();
break;
}
};
added(0, collection);
collection.CollectionChanged += handler;
return Disposable.Create(() => collection.CollectionChanged -= handler);
}
/// <summary>
/// Listens for property changed events from all items in a collection.
/// </summary>

8
src/Perspex.Controls/ListBox.cs

@ -38,7 +38,7 @@ namespace Perspex.Controls
if (e.NavigationMethod == NavigationMethod.Directional)
{
UpdateSelectionFromEventSource(e.Source, true);
UpdateSelectionFromEventSource(e.Source);
}
}
@ -49,7 +49,11 @@ namespace Perspex.Controls
if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right)
{
UpdateSelectionFromEventSource(e.Source, true);
UpdateSelectionFromEventSource(
e.Source,
true,
(e.InputModifiers & InputModifiers.Shift) != 0,
(e.InputModifiers & InputModifiers.Control) != 0);
}
}
}

117
src/Perspex.Controls/Primitives/SelectingItemsControl.cs

@ -3,6 +3,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Perspex.Collections;
@ -103,8 +104,8 @@ namespace Perspex.Controls.Primitives
{
ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized);
_selectedIndexes.Validate = ValidateIndex;
_selectedIndexes.ForEachItem(SelectedIndexAdded, SelectedIndexRemoved, SelectionReset);
_selectedItems.ForEachItem(SelectedItemAdded, SelectedItemRemoved, SelectionReset);
_selectedIndexes.ForEachItem(SelectedIndexesAdded, SelectedIndexesRemoved, SelectionReset);
_selectedItems.ForEachItem(SelectedItemsAdded, SelectedItemsRemoved, SelectionReset);
}
/// <summary>
@ -269,20 +270,35 @@ namespace Perspex.Controls.Primitives
/// </summary>
/// <param name="index">The index of the item.</param>
/// <param name="select">Whether the item should be selected or unselected.</param>
protected void UpdateSelection(int index, bool select)
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
protected void UpdateSelection(
int index,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
{
if (index != -1)
{
if (select)
{
var mode = SelectionMode;
var toggle = (mode & SelectionMode.Toggle) != 0;
var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && SelectedIndexes.Count > 0 ? rangeModifier : false;
if (!toggle)
if (!toggle && !range)
{
SelectedIndex = index;
}
else if (multi && range)
{
var first = SelectedIndexes[0];
// TODO: Don't deselect items in new selection.
SelectedIndexes.Clear();
SelectedIndexes.AddRange(Range(first, index));
}
else
{
var i = SelectedIndexes.IndexOf(index);
@ -311,18 +327,36 @@ namespace Perspex.Controls.Primitives
}
}
private IEnumerable<int> Range(int first, int last)
{
int step = first > last ? -1 : 1;
for (int i = first; i != last; i += step)
{
yield return i;
}
yield return last;
}
/// <summary>
/// Updates the selection for a container based on user interaction.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="select">Whether the container should be selected or unselected.</param>
protected void UpdateSelection(IControl container, bool select)
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
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);
UpdateSelection(index, select, rangeModifier, toggleModifier);
}
}
@ -332,17 +366,23 @@ namespace Perspex.Controls.Primitives
/// </summary>
/// <param name="eventSource">The control that raised the event.</param>
/// <param name="select">Whether the container should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <returns>
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
/// </returns>
protected bool UpdateSelectionFromEventSource(IInteractive eventSource, bool select)
protected bool UpdateSelectionFromEventSource(
IInteractive eventSource,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
{
var item = GetContainerFromEventSource(eventSource);
if (item != null)
{
UpdateSelection(item, select);
UpdateSelection(item, select, rangeModifier, toggleModifier);
return true;
}
@ -480,21 +520,27 @@ namespace Perspex.Controls.Primitives
/// Called when an index is added to the <see cref="SelectedIndexes"/> collection.
/// </summary>
/// <param name="listIndex">The index in the SelectedIndexes collection.</param>
/// <param name="itemIndex">The item index.</param>
private void SelectedIndexAdded(int listIndex, int itemIndex)
/// <param name="itemIndexes">The item indexes.</param>
private void SelectedIndexesAdded(int listIndex, IEnumerable<int> itemIndexes)
{
if (SelectedIndexes.Count == 1)
var indexes = (itemIndexes as IList<int>) ?? itemIndexes.ToList();
IControl container = null;
if (SelectedItems.Count != SelectedIndexes.Count)
{
RaisePropertyChanged(SelectedIndexProperty, -1, itemIndex, BindingPriority.LocalValue);
var items = indexes.Select(x => Items.Cast<object>().ElementAt(x));
SelectedItems.AddRange(items);
}
if (SelectedItems.Count != SelectedIndexes.Count)
foreach (var itemIndex in indexes)
{
var item = Items.Cast<object>().ElementAt(itemIndex);
SelectedItems.Insert(listIndex, item);
container = MarkIndexSelected(itemIndex, true);
}
var container = MarkIndexSelected(itemIndex, true);
if (SelectedIndexes.Count == 1)
{
RaisePropertyChanged(SelectedIndexProperty, -1, SelectedIndexes[0], BindingPriority.LocalValue);
}
if (container != null && Presenter?.Panel != null)
{
@ -506,20 +552,29 @@ namespace Perspex.Controls.Primitives
/// Called when an index is removed from the <see cref="SelectedIndexes"/> collection.
/// </summary>
/// <param name="listIndex">The index in the SelectedIndexes collection.</param>
/// <param name="itemIndex">The item index.</param>
private void SelectedIndexRemoved(int listIndex, int itemIndex)
/// <param name="itemIndexes">The item indexes.</param>
private void SelectedIndexesRemoved(int listIndex, IEnumerable<int> itemIndexes)
{
if (SelectedIndexes.Count == 0)
var sync = SelectedIndexes.Count != SelectedItems.Count;
foreach (var itemIndex in itemIndexes)
{
RaisePropertyChanged(SelectedIndexProperty, itemIndex, -1, BindingPriority.LocalValue);
if (sync)
{
SelectedItems.RemoveAt(listIndex++);
}
MarkIndexSelected(itemIndex, false);
}
if (SelectedIndexes.Count != SelectedItems.Count)
if (SelectedIndexes.Count == 0)
{
SelectedItems.RemoveAt(listIndex);
RaisePropertyChanged(
SelectedIndexProperty,
itemIndexes.First(),
-1,
BindingPriority.LocalValue);
}
MarkIndexSelected(itemIndex, false);
}
/// <summary>
@ -527,16 +582,16 @@ namespace Perspex.Controls.Primitives
/// </summary>
/// <param name="index">The index in the SelectedItems collection.</param>
/// <param name="item">The item.</param>
private void SelectedItemAdded(int index, object item)
private void SelectedItemsAdded(int index, object item)
{
if (SelectedItems.Count == 1)
if (SelectedIndexes.Count != SelectedItems.Count)
{
RaisePropertyChanged(SelectedItemProperty, null, item, BindingPriority.LocalValue);
SelectedIndexes.Insert(index, IndexOf(Items, item));
}
if (SelectedIndexes.Count != SelectedItems.Count)
if (SelectedItems.Count == 1)
{
SelectedIndexes.Insert(index, IndexOf(Items, item));
RaisePropertyChanged(SelectedItemProperty, null, item, BindingPriority.LocalValue);
}
}
@ -545,7 +600,7 @@ namespace Perspex.Controls.Primitives
/// </summary>
/// <param name="index">The index in the SelectedItems collection.</param>
/// <param name="item">The item.</param>
private void SelectedItemRemoved(int index, object item)
private void SelectedItemsRemoved(int index, object item)
{
if (SelectedIndexes.Count != SelectedItems.Count)
{

4
src/Perspex.Controls/Primitives/TabStrip.cs

@ -56,7 +56,7 @@ namespace Perspex.Controls.Primitives
if (e.NavigationMethod == NavigationMethod.Directional)
{
UpdateSelectionFromEventSource(e.Source, true);
UpdateSelectionFromEventSource(e.Source);
}
}
@ -67,7 +67,7 @@ namespace Perspex.Controls.Primitives
if (e.MouseButton == MouseButton.Left)
{
UpdateSelectionFromEventSource(e.Source, true);
UpdateSelectionFromEventSource(e.Source);
}
}
}

63
tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -219,6 +219,58 @@ namespace Perspex.Controls.UnitTests.Primitives
Assert.False(((ListBoxItem)target.Presenter.Panel.Children[1]).IsSelected);
}
[Fact]
public void Range_Select_Should_Select_Range()
{
var target = new TestSelector
{
Items = new[]
{
"foo",
"bar",
"baz",
"qux",
"qiz",
"lol",
},
SelectionMode = SelectionMode.Multiple,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndex = 1;
target.SelectRange(3);
Assert.Equal(new[] { 1, 2, 3 }, target.SelectedIndexes);
Assert.Equal(new[] { "bar", "baz", "qux" }, target.SelectedItems);
}
[Fact]
public void Range_Select_Backwards_Should_Select_Range()
{
var target = new TestSelector
{
Items = new[]
{
"foo",
"bar",
"baz",
"qux",
"qiz",
"lol",
},
SelectionMode = SelectionMode.Multiple,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndex = 3;
target.SelectRange(1);
Assert.Equal(new[] { 3, 2, 1 }, target.SelectedIndexes);
Assert.Equal(new[] { "qux", "baz", "bar" }, target.SelectedItems);
}
private class TestSelector : SelectingItemsControl
{
public new IPerspexList<int> SelectedIndexes
@ -230,6 +282,17 @@ namespace Perspex.Controls.UnitTests.Primitives
{
get { return base.SelectedItems; }
}
public new SelectionMode SelectionMode
{
get { return base.SelectionMode; }
set { base.SelectionMode = value; }
}
public void SelectRange(int index)
{
UpdateSelection(index, true, true);
}
}
private ControlTemplate Template()

Loading…
Cancel
Save