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>
<TabItem Header="Lists"> <TabItem Header="Lists">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<ListBox Items="{Binding Items}" SelectionMode="Toggle"> <ListBox Items="{Binding Items}" SelectionMode="Multiple">
<ListBox.DataTemplates> <ListBox.DataTemplates>
<DataTemplate DataType="vm:TestItem"> <DataTemplate DataType="vm:TestItem">
<StackPanel> <StackPanel>

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

@ -16,6 +16,14 @@ namespace Perspex.Collections
/// </summary> /// </summary>
new int Count { get; } 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> /// <summary>
/// Adds multiple items to the collection. /// Adds multiple items to the collection.
/// </summary> /// </summary>

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

@ -187,17 +187,22 @@ namespace Perspex.Collections
{ {
Contract.Requires<ArgumentNullException>(items != null); 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; int index = _inner.Count;
_inner.AddRange(items); _inner.AddRange(items);
NotifyAdd((items as IList) ?? items.ToList(), index); NotifyAdd(list, index);
}
} }
/// <summary> /// <summary>
@ -205,9 +210,12 @@ namespace Perspex.Collections
/// </summary> /// </summary>
public void Clear() public void Clear()
{ {
var old = _inner; if (this.Count > 0)
_inner = new List<T>(); {
NotifyReset(old); var old = _inner;
_inner = new List<T>();
NotifyReset(old);
}
} }
/// <summary> /// <summary>
@ -272,16 +280,21 @@ namespace Perspex.Collections
{ {
Contract.Requires<ArgumentNullException>(items != null); 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); _inner.InsertRange(index, items);
NotifyAdd((items as IList) ?? items.ToList(), index); NotifyAdd((items as IList) ?? items.ToList(), index);
}
} }
/// <summary> /// <summary>
@ -311,7 +324,7 @@ namespace Perspex.Collections
{ {
Contract.Requires<ArgumentNullException>(items != null); Contract.Requires<ArgumentNullException>(items != null);
List<T> removed = new List<T>(); var list = (items as IList) ?? items.ToList();
foreach (var i in items) 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.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
namespace Perspex.Collections namespace Perspex.Collections
@ -133,6 +134,66 @@ namespace Perspex.Collections
return Disposable.Create(() => collection.CollectionChanged -= handler); 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> /// <summary>
/// Listens for property changed events from all items in a collection. /// Listens for property changed events from all items in a collection.
/// </summary> /// </summary>

8
src/Perspex.Controls/ListBox.cs

@ -38,7 +38,7 @@ namespace Perspex.Controls
if (e.NavigationMethod == NavigationMethod.Directional) 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) 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;
using System.Collections; using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using Perspex.Collections; using Perspex.Collections;
@ -103,8 +104,8 @@ namespace Perspex.Controls.Primitives
{ {
ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized); ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized);
_selectedIndexes.Validate = ValidateIndex; _selectedIndexes.Validate = ValidateIndex;
_selectedIndexes.ForEachItem(SelectedIndexAdded, SelectedIndexRemoved, SelectionReset); _selectedIndexes.ForEachItem(SelectedIndexesAdded, SelectedIndexesRemoved, SelectionReset);
_selectedItems.ForEachItem(SelectedItemAdded, SelectedItemRemoved, SelectionReset); _selectedItems.ForEachItem(SelectedItemsAdded, SelectedItemsRemoved, SelectionReset);
} }
/// <summary> /// <summary>
@ -269,20 +270,35 @@ namespace Perspex.Controls.Primitives
/// </summary> /// </summary>
/// <param name="index">The index of the item.</param> /// <param name="index">The index of the item.</param>
/// <param name="select">Whether the item should be selected or unselected.</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 (index != -1)
{ {
if (select) if (select)
{ {
var mode = SelectionMode; var mode = SelectionMode;
var toggle = (mode & SelectionMode.Toggle) != 0; var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
var multi = (mode & SelectionMode.Multiple) != 0; var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && SelectedIndexes.Count > 0 ? rangeModifier : false;
if (!toggle) if (!toggle && !range)
{ {
SelectedIndex = index; 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 else
{ {
var i = SelectedIndexes.IndexOf(index); 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> /// <summary>
/// Updates the selection for a container based on user interaction. /// Updates the selection for a container based on user interaction.
/// </summary> /// </summary>
/// <param name="container">The container.</param> /// <param name="container">The container.</param>
/// <param name="select">Whether the container should be selected or unselected.</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); var index = ItemContainerGenerator.IndexFromContainer(container);
if (index != -1) if (index != -1)
{ {
UpdateSelection(index, select); UpdateSelection(index, select, rangeModifier, toggleModifier);
} }
} }
@ -332,17 +366,23 @@ namespace Perspex.Controls.Primitives
/// </summary> /// </summary>
/// <param name="eventSource">The control that raised the event.</param> /// <param name="eventSource">The control that raised the event.</param>
/// <param name="select">Whether the container should be selected or unselected.</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> /// <returns>
/// True if the event originated from a container that belongs to the control; otherwise /// True if the event originated from a container that belongs to the control; otherwise
/// false. /// false.
/// </returns> /// </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); var item = GetContainerFromEventSource(eventSource);
if (item != null) if (item != null)
{ {
UpdateSelection(item, select); UpdateSelection(item, select, rangeModifier, toggleModifier);
return true; return true;
} }
@ -480,21 +520,27 @@ namespace Perspex.Controls.Primitives
/// Called when an index is added to the <see cref="SelectedIndexes"/> collection. /// Called when an index is added to the <see cref="SelectedIndexes"/> collection.
/// </summary> /// </summary>
/// <param name="listIndex">The index in the SelectedIndexes collection.</param> /// <param name="listIndex">The index in the SelectedIndexes collection.</param>
/// <param name="itemIndex">The item index.</param> /// <param name="itemIndexes">The item indexes.</param>
private void SelectedIndexAdded(int listIndex, int itemIndex) 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); container = MarkIndexSelected(itemIndex, true);
SelectedItems.Insert(listIndex, item);
} }
var container = MarkIndexSelected(itemIndex, true); if (SelectedIndexes.Count == 1)
{
RaisePropertyChanged(SelectedIndexProperty, -1, SelectedIndexes[0], BindingPriority.LocalValue);
}
if (container != null && Presenter?.Panel != null) 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. /// Called when an index is removed from the <see cref="SelectedIndexes"/> collection.
/// </summary> /// </summary>
/// <param name="listIndex">The index in the SelectedIndexes collection.</param> /// <param name="listIndex">The index in the SelectedIndexes collection.</param>
/// <param name="itemIndex">The item index.</param> /// <param name="itemIndexes">The item indexes.</param>
private void SelectedIndexRemoved(int listIndex, int itemIndex) 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> /// <summary>
@ -527,16 +582,16 @@ namespace Perspex.Controls.Primitives
/// </summary> /// </summary>
/// <param name="index">The index in the SelectedItems collection.</param> /// <param name="index">The index in the SelectedItems collection.</param>
/// <param name="item">The item.</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> /// </summary>
/// <param name="index">The index in the SelectedItems collection.</param> /// <param name="index">The index in the SelectedItems collection.</param>
/// <param name="item">The item.</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) 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) 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) 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); 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 private class TestSelector : SelectingItemsControl
{ {
public new IPerspexList<int> SelectedIndexes public new IPerspexList<int> SelectedIndexes
@ -230,6 +282,17 @@ namespace Perspex.Controls.UnitTests.Primitives
{ {
get { return base.SelectedItems; } 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() private ControlTemplate Template()

Loading…
Cancel
Save