Browse Source

Merge remote-tracking branch 'refs/remotes/origin/multiple-selection'

pull/278/head
Steven Kirk 11 years ago
parent
commit
f200169d89
  1. 2
      samples/TestApplication/GalleryStyle.cs
  2. 2
      samples/TestApplication/Program.cs
  3. 2
      samples/XamlTestApplicationPcl/Views/MainWindow.paml
  4. 7
      src/Perspex.Animation/PropertyTransitions.cs
  5. 19
      src/Perspex.Base/Collections/IPerspexList.cs
  6. 140
      src/Perspex.Base/Collections/PerspexList.cs
  7. 143
      src/Perspex.Base/Collections/PerspexListExtensions.cs
  8. 2
      src/Perspex.Controls/ColumnDefinitions.cs
  9. 4
      src/Perspex.Controls/Control.cs
  10. 2
      src/Perspex.Controls/Controls.cs
  11. 2
      src/Perspex.Controls/Deck.cs
  12. 5
      src/Perspex.Controls/Generators/IItemContainerGenerator.cs
  13. 5
      src/Perspex.Controls/Generators/ItemContainerGenerator.cs
  14. 5
      src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs
  15. 43
      src/Perspex.Controls/ListBox.cs
  16. 4
      src/Perspex.Controls/Mixins/SelectableMixin.cs
  17. 1
      src/Perspex.Controls/Perspex.Controls.csproj
  18. 530
      src/Perspex.Controls/Primitives/SelectingItemsControl.cs
  19. 25
      src/Perspex.Controls/Primitives/TabStrip.cs
  20. 2
      src/Perspex.Controls/RowDefinitions.cs
  21. 34
      src/Perspex.Controls/SelectionMode.cs
  22. 2
      src/Perspex.Controls/TabControl.cs
  23. 7
      src/Perspex.Controls/Templates/DataTemplates.cs
  24. 14
      src/Perspex.Controls/TreeView.cs
  25. 16
      src/Perspex.Input/FocusManager.cs
  26. 5
      src/Perspex.Input/GotFocusEventArgs.cs
  27. 6
      src/Perspex.Input/IFocusManager.cs
  28. 5
      src/Perspex.Input/IKeyboardDevice.cs
  29. 6
      src/Perspex.Input/IKeyboardNavigationHandler.cs
  30. 6
      src/Perspex.Input/KeyboardDevice.cs
  31. 10
      src/Perspex.Input/KeyboardNavigationHandler.cs
  32. 1
      src/Perspex.SceneGraph/Visual.cs
  33. 4
      src/Perspex.Themes.Default/ListBoxItemStyle.cs
  34. 2
      src/Perspex.Themes.Default/MenuItemStyle.cs
  35. 2
      src/Perspex.Themes.Default/TabItemStyle.cs
  36. 4
      src/Perspex.Themes.Default/TreeViewItemStyle.cs
  37. 2
      src/Windows/Perspex.Win32/Input/WindowsKeyboardDevice.cs
  38. 20
      tests/Perspex.Base.UnitTests/Collections/PerspexListTests.cs
  39. 19
      tests/Perspex.Controls.UnitTests/ListBoxTests.cs
  40. 232
      tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs
  41. 2
      tests/Perspex.Controls.UnitTests/Mixins/SelectableMixinTests.cs
  42. 2
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
  43. 95
      tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  44. 20
      tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs
  45. 336
      tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

2
samples/TestApplication/GalleryStyle.cs

@ -44,7 +44,7 @@ namespace TestApplication
} }
}, },
new Style(s => s.Name("internalStrip").OfType<TabStrip>().Child().OfType<TabItem>().Class("selected")) new Style(s => s.Name("internalStrip").OfType<TabStrip>().Child().OfType<TabItem>().Class(":selected"))
{ {
Setters = new[] Setters = new[]
{ {

2
samples/TestApplication/Program.cs

@ -483,8 +483,6 @@ namespace TestApplication
} }
}; };
imageDeck.AutoSelect = true;
var next = new Button var next = new Button
{ {
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,

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}"> <ListBox Items="{Binding Items}" SelectionMode="Multiple">
<ListBox.DataTemplates> <ListBox.DataTemplates>
<DataTemplate DataType="vm:TestItem"> <DataTemplate DataType="vm:TestItem">
<StackPanel> <StackPanel>

7
src/Perspex.Animation/PropertyTransitions.cs

@ -10,5 +10,12 @@ namespace Perspex.Animation
/// </summary> /// </summary>
public class PropertyTransitions : PerspexList<PropertyTransition> public class PropertyTransitions : PerspexList<PropertyTransition>
{ {
/// <summary>
/// Initializes a new instance of the <see cref="PropertyTransitions"/> class.
/// </summary>
public PropertyTransitions()
{
ResetBehavior = ResetBehavior.Remove;
}
} }
} }

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

@ -11,6 +11,18 @@ namespace Perspex.Collections
/// <typeparam name="T">The type of the items in the list.</typeparam> /// <typeparam name="T">The type of the items in the list.</typeparam>
public interface IPerspexList<T> : IList<T>, IPerspexReadOnlyList<T> public interface IPerspexList<T> : IList<T>, IPerspexReadOnlyList<T>
{ {
/// <summary>
/// Gets the number of items in the list.
/// </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> /// <summary>
/// Adds multiple items to the collection. /// Adds multiple items to the collection.
/// </summary> /// </summary>
@ -29,5 +41,12 @@ namespace Perspex.Collections
/// </summary> /// </summary>
/// <param name="items">The items.</param> /// <param name="items">The items.</param>
void RemoveAll(IEnumerable<T> items); void RemoveAll(IEnumerable<T> items);
/// <summary>
/// Removes a range of elements from the collection.
/// </summary>
/// <param name="index">The first index to remove.</param>
/// <param name="count">The number of items to remove.</param>
void RemoveRange(int index, int count);
} }
} }

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

@ -10,15 +10,47 @@ using System.Linq;
namespace Perspex.Collections namespace Perspex.Collections
{ {
/// <summary>
/// Describes the action notified on a clear of a <see cref="PerspexList{T}"/>.
/// </summary>
public enum ResetBehavior
{
/// <summary>
/// Clearing the list notifies a with a
/// <see cref="NotifyCollectionChangedAction.Reset"/>.
/// </summary>
Reset,
/// <summary>
/// Clearing the list notifies a with a
/// <see cref="NotifyCollectionChangedAction.Remove"/>.
/// </summary>
Remove,
}
/// <summary> /// <summary>
/// A notifying list. /// A notifying list.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the list items.</typeparam> /// <typeparam name="T">The type of the list items.</typeparam>
/// <remarks> /// <remarks>
/// <para>
/// PerspexList is similar to <see cref="System.Collections.ObjectModel.ObservableCollection{T}"/> /// PerspexList is similar to <see cref="System.Collections.ObjectModel.ObservableCollection{T}"/>
/// except that when the <see cref="Clear"/> method is called, it notifies with a /// with a few added features:
/// <see cref="NotifyCollectionChangedAction.Remove"/> action, passing the items that were /// </para>
/// removed. ///
/// <list type="bullet">
/// <item>
/// It can be configured to notify the <see cref="CollectionChanged"/> event with a
/// <see cref="NotifyCollectionChangedAction.Remove"/> action instead of a
/// <see cref="NotifyCollectionChangedAction.Reset"/> when the list is cleared by
/// setting <see cref="ResetBehavior"/> to <see cref="ResetBehavior.Remove"/>.
/// removed
/// </item>
/// <item>
/// A <see cref="Validate"/> function can be used to validate each item before insertion.
/// removed
/// </item>
/// </list>
/// </remarks> /// </remarks>
public class PerspexList<T> : IPerspexList<T>, IList, INotifyCollectionChanged, INotifyPropertyChanged public class PerspexList<T> : IPerspexList<T>, IList, INotifyCollectionChanged, INotifyPropertyChanged
{ {
@ -65,6 +97,17 @@ namespace Perspex.Collections
/// </summary> /// </summary>
public int Count => _inner.Count; public int Count => _inner.Count;
/// <summary>
/// Gets or sets the reset behavior of the list.
/// </summary>
public ResetBehavior ResetBehavior { get; set; }
/// <summary>
/// Gets or sets a validation routine that can be used to validate items before they are
/// added.
/// </summary>
public Action<T> Validate { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
bool IList.IsFixedSize => false; bool IList.IsFixedSize => false;
@ -97,6 +140,8 @@ namespace Perspex.Collections
set set
{ {
Validate?.Invoke(value);
T old = _inner[index]; T old = _inner[index];
_inner[index] = value; _inner[index] = value;
@ -128,6 +173,7 @@ namespace Perspex.Collections
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
public void Add(T item) public void Add(T item)
{ {
Validate?.Invoke(item);
int index = _inner.Count; int index = _inner.Count;
_inner.Add(item); _inner.Add(item);
NotifyAdd(new[] { item }, index); NotifyAdd(new[] { item }, index);
@ -141,9 +187,22 @@ namespace Perspex.Collections
{ {
Contract.Requires<ArgumentNullException>(items != null); Contract.Requires<ArgumentNullException>(items != null);
int index = _inner.Count; var list = (items as IList) ?? items.ToList();
_inner.AddRange(items);
NotifyAdd((items as IList) ?? items.ToList(), index); if (list.Count > 0)
{
if (Validate != null)
{
foreach (var item in list)
{
Validate((T)item);
}
}
int index = _inner.Count;
_inner.AddRange(items);
NotifyAdd(list, index);
}
} }
/// <summary> /// <summary>
@ -151,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>(); {
NotifyRemove(old, 0); var old = _inner;
_inner = new List<T>();
NotifyReset(old);
}
} }
/// <summary> /// <summary>
@ -204,6 +266,7 @@ namespace Perspex.Collections
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
public void Insert(int index, T item) public void Insert(int index, T item)
{ {
Validate?.Invoke(item);
_inner.Insert(index, item); _inner.Insert(index, item);
NotifyAdd(new[] { item }, index); NotifyAdd(new[] { item }, index);
} }
@ -217,8 +280,21 @@ namespace Perspex.Collections
{ {
Contract.Requires<ArgumentNullException>(items != null); Contract.Requires<ArgumentNullException>(items != null);
_inner.InsertRange(index, items); var list = (items as IList) ?? items.ToList();
NotifyAdd((items as IList) ?? items.ToList(), index);
if (list.Count > 0)
{
if (Validate != null)
{
foreach (var item in list)
{
Validate((T)item);
}
}
_inner.InsertRange(index, items);
NotifyAdd((items as IList) ?? items.ToList(), index);
}
} }
/// <summary> /// <summary>
@ -248,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)
{ {
@ -268,6 +344,21 @@ namespace Perspex.Collections
NotifyRemove(new[] { item }, index); NotifyRemove(new[] { item }, index);
} }
/// <summary>
/// Removes a range of elements from the collection.
/// </summary>
/// <param name="index">The first index to remove.</param>
/// <param name="count">The number of items to remove.</param>
public void RemoveRange(int index, int count)
{
if (count > 0)
{
var list = _inner.GetRange(index, count);
_inner.RemoveRange(index, count);
NotifyRemove(list, index);
}
}
/// <inheritdoc/> /// <inheritdoc/>
int IList.Add(object value) int IList.Add(object value)
{ {
@ -367,5 +458,30 @@ namespace Perspex.Collections
NotifyCountChanged(); NotifyCountChanged();
} }
/// <summary>
/// Raises the <see cref="CollectionChanged"/> event with a reset action.
/// </summary>
/// <param name="t">The items that were removed.</param>
private void NotifyReset(IList t)
{
if (CollectionChanged != null)
{
NotifyCollectionChangedEventArgs e;
if (ResetBehavior == ResetBehavior.Reset)
{
e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
}
else
{
e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, t, 0);
}
CollectionChanged(this, e);
}
NotifyCountChanged();
}
} }
} }

143
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
@ -22,55 +23,110 @@ namespace Perspex.Collections
/// <param name="collection">The collection.</param> /// <param name="collection">The collection.</param>
/// <param name="added"> /// <param name="added">
/// An action called initially for each item in the collection and subsequently for each /// An action called initially for each item in the collection and subsequently for each
/// item added to the collection. /// item added to the collection. The parameters passed are the index in the collection and
/// the item.
/// </param> /// </param>
/// <param name="removed"> /// <param name="removed">
/// An action called for each item removed from the collection. /// An action called for each item removed from the collection. The parameters passed are
/// the index in the collection and the item.
/// </param>
/// <param name="reset">
/// An action called when the collection is reset.
/// </param> /// </param>
/// <returns>A disposable used to terminate the subscription.</returns> /// <returns>A disposable used to terminate the subscription.</returns>
public static IDisposable ForEachItem<T>( public static IDisposable ForEachItem<T>(
this IPerspexReadOnlyList<T> collection, this IPerspexReadOnlyList<T> collection,
Action<T> added, Action<T> added,
Action<T> removed) Action<T> removed,
Action reset)
{ {
return collection.ForEachItem((_, i) => added(i), (_, i) => removed(i), reset);
}
/// <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 for each item in the collection and subsequently for each
/// item added to the collection. The parameters passed are the index in the collection and
/// the item.
/// </param>
/// <param name="removed">
/// An action called for each item removed from the collection. The parameters passed are
/// the index in the collection and the item.
/// </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, T> added,
Action<int, T> removed,
Action reset)
{
int index;
NotifyCollectionChangedEventHandler handler = (_, e) => NotifyCollectionChangedEventHandler handler = (_, e) =>
{ {
switch (e.Action) switch (e.Action)
{ {
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
foreach (T i in e.NewItems) index = e.NewStartingIndex;
foreach (T item in e.NewItems)
{ {
added(i); added(index++, item);
} }
break; break;
case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Replace:
foreach (T i in e.OldItems) index = e.OldStartingIndex;
foreach (T item in e.OldItems)
{ {
removed(i); removed(index++, item);
} }
foreach (T i in e.NewItems) index = e.NewStartingIndex;
foreach (T item in e.NewItems)
{ {
added(i); added(index++, item);
} }
break; break;
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
foreach (T i in e.OldItems) index = e.OldStartingIndex;
foreach (T item in e.OldItems)
{ {
removed(i); removed(index++, item);
} }
break; break;
case NotifyCollectionChangedAction.Reset:
if (reset == null)
{
throw new InvalidOperationException(
"Reset called on collection without reset handler.");
}
reset();
break;
} }
}; };
index = 0;
foreach (T i in collection) foreach (T i in collection)
{ {
added(i); added(index++, i);
} }
collection.CollectionChanged += handler; collection.CollectionChanged += handler;
@ -78,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>
@ -116,7 +232,8 @@ namespace Perspex.Collections
inpc.PropertyChanged -= handler; inpc.PropertyChanged -= handler;
tracked.Remove(inpc); tracked.Remove(inpc);
} }
}); },
null);
return Disposable.Create(() => return Disposable.Create(() =>
{ {

2
src/Perspex.Controls/ColumnDefinitions.cs

@ -17,6 +17,7 @@ namespace Perspex.Controls
/// </summary> /// </summary>
public ColumnDefinitions() public ColumnDefinitions()
{ {
ResetBehavior = ResetBehavior.Remove;
} }
/// <summary> /// <summary>
@ -24,6 +25,7 @@ namespace Perspex.Controls
/// </summary> /// </summary>
/// <param name="s">A string representation of the column definitions.</param> /// <param name="s">A string representation of the column definitions.</param>
public ColumnDefinitions(string s) public ColumnDefinitions(string s)
: this()
{ {
AddRange(GridLength.ParseLengths(s, CultureInfo.InvariantCulture).Select(x => new ColumnDefinition(x))); AddRange(GridLength.ParseLengths(s, CultureInfo.InvariantCulture).Select(x => new ColumnDefinition(x)));
} }

4
src/Perspex.Controls/Control.cs

@ -271,7 +271,9 @@ namespace Perspex.Controls
{ {
if (_logicalChildren == null) if (_logicalChildren == null)
{ {
_logicalChildren = new PerspexList<ILogical>(); var list = new PerspexList<ILogical>();
list.ResetBehavior = ResetBehavior.Remove;
_logicalChildren = list;
} }
return _logicalChildren; return _logicalChildren;

2
src/Perspex.Controls/Controls.cs

@ -16,6 +16,7 @@ namespace Perspex.Controls
/// </summary> /// </summary>
public Controls() public Controls()
{ {
ResetBehavior = ResetBehavior.Remove;
} }
/// <summary> /// <summary>
@ -25,6 +26,7 @@ namespace Perspex.Controls
public Controls(IEnumerable<IControl> items) public Controls(IEnumerable<IControl> items)
: base(items) : base(items)
{ {
ResetBehavior = ResetBehavior.Remove;
} }
} }
} }

2
src/Perspex.Controls/Deck.cs

@ -31,7 +31,7 @@ namespace Perspex.Controls
/// </summary> /// </summary>
static Deck() static Deck()
{ {
AutoSelectProperty.OverrideDefaultValue<Deck>(true); SelectionModeProperty.OverrideDefaultValue<Deck>(SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue<Deck>(PanelTemplate); ItemsPanelProperty.OverrideDefaultValue<Deck>(PanelTemplate);
} }

5
src/Perspex.Controls/Generators/IItemContainerGenerator.cs

@ -13,6 +13,11 @@ namespace Perspex.Controls.Generators
/// </summary> /// </summary>
public interface IItemContainerGenerator public interface IItemContainerGenerator
{ {
/// <summary>
/// Gets the currently realized containers.
/// </summary>
IEnumerable<IControl> Containers { get; }
/// <summary> /// <summary>
/// Signalled whenever new containers are initialized. /// Signalled whenever new containers are initialized.
/// </summary> /// </summary>

5
src/Perspex.Controls/Generators/ItemContainerGenerator.cs

@ -28,6 +28,11 @@ namespace Perspex.Controls.Generators
Owner = owner; Owner = owner;
} }
/// <summary>
/// Gets the currently realized containers.
/// </summary>
public IEnumerable<IControl> Containers => _containers.Values;
/// <summary> /// <summary>
/// Signalled whenever new containers are initialized. /// Signalled whenever new containers are initialized.
/// </summary> /// </summary>

5
src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs

@ -29,6 +29,11 @@ namespace Perspex.Controls.Generators
Owner = owner; Owner = owner;
} }
/// <summary>
/// Gets the currently realized containers.
/// </summary>
public IEnumerable<IControl> Containers => _containers.Values;
/// <summary> /// <summary>
/// Signalled whenever new containers are initialized. /// Signalled whenever new containers are initialized.
/// </summary> /// </summary>

43
src/Perspex.Controls/ListBox.cs

@ -3,6 +3,7 @@
using Perspex.Controls.Generators; using Perspex.Controls.Generators;
using Perspex.Controls.Primitives; using Perspex.Controls.Primitives;
using Perspex.Input;
namespace Perspex.Controls namespace Perspex.Controls
{ {
@ -11,10 +12,52 @@ namespace Perspex.Controls
/// </summary> /// </summary>
public class ListBox : SelectingItemsControl public class ListBox : SelectingItemsControl
{ {
/// <summary>
/// Defines the <see cref="SelectionMode"/> property.
/// </summary>
public static readonly new PerspexProperty<SelectionMode> SelectionModeProperty =
SelectingItemsControl.SelectionModeProperty;
/// <inheritdoc/>
public new SelectionMode SelectionMode
{
get { return base.SelectionMode; }
set { base.SelectionMode = value; }
}
/// <inheritdoc/> /// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator() protected override IItemContainerGenerator CreateItemContainerGenerator()
{ {
return new ItemContainerGenerator<ListBoxItem>(this); return new ItemContainerGenerator<ListBoxItem>(this);
} }
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
if (e.NavigationMethod == NavigationMethod.Directional)
{
UpdateSelectionFromEventSource(
e.Source,
true,
(e.InputModifiers & InputModifiers.Shift) != 0);
}
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressEventArgs e)
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right)
{
UpdateSelectionFromEventSource(
e.Source,
true,
(e.InputModifiers & InputModifiers.Shift) != 0,
(e.InputModifiers & InputModifiers.Control) != 0);
}
}
} }
} }

4
src/Perspex.Controls/Mixins/SelectableMixin.cs

@ -52,7 +52,7 @@ namespace Perspex.Controls.Mixins
{ {
if ((bool)x.NewValue) if ((bool)x.NewValue)
{ {
sender.Classes.Add("selected"); sender.Classes.Add(":selected");
if (((IVisual)sender).IsAttachedToVisualTree) if (((IVisual)sender).IsAttachedToVisualTree)
{ {
@ -61,7 +61,7 @@ namespace Perspex.Controls.Mixins
} }
else else
{ {
sender.Classes.Remove("selected"); sender.Classes.Remove(":selected");
} }
sender.RaiseEvent(new RoutedEventArgs sender.RaiseEvent(new RoutedEventArgs

1
src/Perspex.Controls/Perspex.Controls.csproj

@ -44,6 +44,7 @@
<Compile Include="DockPanel.cs" /> <Compile Include="DockPanel.cs" />
<Compile Include="HotkeyManager.cs" /> <Compile Include="HotkeyManager.cs" />
<Compile Include="Platform\ITopLevelRenderer.cs" /> <Compile Include="Platform\ITopLevelRenderer.cs" />
<Compile Include="SelectionMode.cs" />
<Compile Include="SystemDialog.cs" /> <Compile Include="SystemDialog.cs" />
<Compile Include="Generators\ITreeItemContainerGenerator.cs" /> <Compile Include="Generators\ITreeItemContainerGenerator.cs" />
<Compile Include="Generators\ItemContainers.cs" /> <Compile Include="Generators\ItemContainers.cs" />

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

@ -3,8 +3,10 @@
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.Controls.Generators; using Perspex.Controls.Generators;
using Perspex.Input; using Perspex.Input;
using Perspex.Interactivity; using Perspex.Interactivity;
@ -17,16 +19,23 @@ namespace Perspex.Controls.Primitives
/// An <see cref="ItemsControl"/> that maintains a selection. /// An <see cref="ItemsControl"/> that maintains a selection.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// TODO: Support multiple selection. /// <para>
/// <see cref="SelectingItemsControl"/> provides a base class for <see cref="ItemsControl"/>s
/// that maintain a selection (single or multiple). By default only its
/// <see cref="SelectedIndex"/> and <see cref="SelectedItem"/> properties are visible; the
/// multiple selection properties <see cref="SelectedIndexes"/> and <see cref="SelectedItems"/>
/// together with the <see cref="SelectionMode"/> properties are protected, however a derived
/// class can expose these if it wishes to support multiple selection.
/// </para>
/// <para>
/// <see cref="SelectingItemsControl"/> maintains a selection respecting the current
/// <see cref="SelectionMode"/> but it does not react to user input; this must be handled in a
/// derived class. It does, however, respond to <see cref="IsSelectedChangedEvent"/> events
/// from items and updates the selection accordingly.
/// </para>
/// </remarks> /// </remarks>
public class SelectingItemsControl : ItemsControl public class SelectingItemsControl : ItemsControl
{ {
/// <summary>
/// Defines the <see cref="AutoSelect"/> property.
/// </summary>
public static readonly PerspexProperty<bool> AutoSelectProperty =
PerspexProperty.Register<SelectingItemsControl, bool>("AutoSelect");
/// <summary> /// <summary>
/// Defines the <see cref="SelectedIndex"/> property. /// Defines the <see cref="SelectedIndex"/> property.
/// </summary> /// </summary>
@ -45,6 +54,29 @@ namespace Perspex.Controls.Primitives
o => o.SelectedItem, o => o.SelectedItem,
(o, v) => o.SelectedItem = v); (o, v) => o.SelectedItem = v);
/// <summary>
/// Defines the <see cref="SelectedIndexes"/> property.
/// </summary>
protected static readonly PerspexProperty<IPerspexList<int>> SelectedIndexesProperty =
PerspexProperty.RegisterDirect<SelectingItemsControl, IPerspexList<int>>(
nameof(SelectedIndexes),
o => o.SelectedIndexes);
/// <summary>
/// Defines the <see cref="SelectedItems"/> property.
/// </summary>
protected static readonly PerspexProperty<IPerspexList<object>> SelectedItemsProperty =
PerspexProperty.RegisterDirect<SelectingItemsControl, IPerspexList<object>>(
nameof(SelectedItems),
o => o.SelectedItems);
/// <summary>
/// Defines the <see cref="SelectionMode"/> property.
/// </summary>
protected static readonly PerspexProperty<SelectionMode> SelectionModeProperty =
PerspexProperty.Register<SelectingItemsControl, SelectionMode>(
nameof(SelectionMode));
/// <summary> /// <summary>
/// Event that should be raised by items that implement <see cref="ISelectable"/> to /// Event that should be raised by items that implement <see cref="ISelectable"/> to
/// notify the parent <see cref="SelectingItemsControl"/> that their selection state /// notify the parent <see cref="SelectingItemsControl"/> that their selection state
@ -53,8 +85,9 @@ namespace Perspex.Controls.Primitives
public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent = public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent =
RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>("IsSelectedChanged", RoutingStrategies.Bubble); RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>("IsSelectedChanged", RoutingStrategies.Bubble);
private int _selectedIndex = -1; private PerspexList<int> _selectedIndexes = new PerspexList<int>();
private object _selectedItem; private PerspexList<object> _selectedItems = new PerspexList<object>();
private bool _ignoreContainerSelectionChanged;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> class. /// Initializes static members of the <see cref="SelectingItemsControl"/> class.
@ -62,8 +95,6 @@ namespace Perspex.Controls.Primitives
static SelectingItemsControl() static SelectingItemsControl()
{ {
IsSelectedChangedEvent.AddClassHandler<SelectingItemsControl>(x => x.ContainerSelectionChanged); IsSelectedChangedEvent.AddClassHandler<SelectingItemsControl>(x => x.ContainerSelectionChanged);
SelectedIndexProperty.Changed.AddClassHandler<SelectingItemsControl>(x => x.SelectedIndexChanged);
SelectedItemProperty.Changed.AddClassHandler<SelectingItemsControl>(x => x.SelectedItemChanged);
} }
/// <summary> /// <summary>
@ -72,16 +103,9 @@ namespace Perspex.Controls.Primitives
public SelectingItemsControl() public SelectingItemsControl()
{ {
ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized); ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized);
} _selectedIndexes.Validate = ValidateIndex;
_selectedIndexes.ForEachItem(SelectedIndexesAdded, SelectedIndexesRemoved, SelectionReset);
/// <summary> _selectedItems.ForEachItem(SelectedItemsAdded, SelectedItemsRemoved, SelectionReset);
/// Gets or sets a value indicating whether the control should always try to keep an item
/// selected where possible.
/// </summary>
public bool AutoSelect
{
get { return GetValue(AutoSelectProperty); }
set { SetValue(AutoSelectProperty, value); }
} }
/// <summary> /// <summary>
@ -91,13 +115,25 @@ namespace Perspex.Controls.Primitives
{ {
get get
{ {
return _selectedIndex; return _selectedIndexes.Count > 0 ? _selectedIndexes[0]: -1;
} }
set set
{ {
value = (value >= 0 && value < Items?.Cast<object>().Count()) ? value : -1; var old = SelectedIndex;
SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value); var effective = (value >= 0 && value < Items?.Cast<object>().Count()) ? value : -1;
if (old != effective)
{
_selectedIndexes.Clear();
if (effective != -1)
{
_selectedIndexes.Add(effective);
}
RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue);
}
} }
} }
@ -108,16 +144,72 @@ namespace Perspex.Controls.Primitives
{ {
get get
{ {
return _selectedItem; return _selectedItems.FirstOrDefault();
} }
set set
{ {
value = Items?.Cast<object>().Contains(value) == true ? value : null; var old = SelectedItem;
SetAndRaise(SelectedItemProperty, ref _selectedItem, value); var effective = Items?.Cast<object>().Contains(value) == true ? value : null;
if (effective != old)
{
_selectedItems.Clear();
if (effective != null)
{
_selectedItems.Add(effective);
}
RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue);
}
} }
} }
/// <summary>
/// Gets the selected indexes.
/// </summary>
protected IPerspexList<int> SelectedIndexes
{
get { return _selectedIndexes; }
}
/// <summary>
/// Gets the selected items.
/// </summary>
protected IPerspexList<object> SelectedItems
{
get { return _selectedItems; }
}
/// <summary>
/// Gets or sets the selection mode.
/// </summary>
protected SelectionMode SelectionMode
{
get { return GetValue(SelectionModeProperty); }
set { SetValue(SelectionModeProperty, value); }
}
/// <summary>
/// Gets a value indicating whether <see cref="SelectionMode.AlwaysSelected"/> is set.
/// </summary>
protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
/// <summary>
/// Tries to get the container that was the source of an event.
/// </summary>
/// <param name="eventSource">The control that raised the event.</param>
/// <returns>The container or null if the event did not originate in a container.</returns>
protected IControl GetContainerFromEventSource(IInteractive eventSource)
{
var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
.OfType<ILogical>()
.FirstOrDefault(x => x.LogicalParent == this);
return item as IControl;
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void ItemsChanged(PerspexPropertyChangedEventArgs e) protected override void ItemsChanged(PerspexPropertyChangedEventArgs e)
{ {
@ -127,7 +219,7 @@ namespace Perspex.Controls.Primitives
{ {
SelectedIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem); SelectedIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem);
} }
else if (AutoSelect && Items != null & Items.Cast<object>().Any()) else if (AlwaysSelected && Items != null & Items.Cast<object>().Any())
{ {
SelectedIndex = 0; SelectedIndex = 0;
} }
@ -141,7 +233,7 @@ namespace Perspex.Controls.Primitives
switch (e.Action) switch (e.Action)
{ {
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
if (AutoSelect && SelectedIndex == -1) if (AlwaysSelected && SelectedIndex == -1)
{ {
SelectedIndex = 0; SelectedIndex = 0;
} }
@ -155,7 +247,7 @@ namespace Perspex.Controls.Primitives
if (selectedIndex >= e.OldStartingIndex && if (selectedIndex >= e.OldStartingIndex &&
selectedIndex < e.OldStartingIndex + e.OldItems.Count) selectedIndex < e.OldStartingIndex + e.OldItems.Count)
{ {
if (!AutoSelect) if (!AlwaysSelected)
{ {
SelectedIndex = -1; SelectedIndex = -1;
} }
@ -173,23 +265,112 @@ namespace Perspex.Controls.Primitives
} }
} }
/// <inheritdoc/> /// <summary>
protected override void OnGotFocus(GotFocusEventArgs e) /// Updates the selection for an item based on user interaction.
/// </summary>
/// <param name="index">The index of the item.</param>
/// <param name="select">Whether the item 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>
protected void UpdateSelection(
int index,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
{ {
base.OnGotFocus(e); if (index != -1)
{
if (select)
{
var mode = SelectionMode;
var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && SelectedIndexes.Count > 0 ? rangeModifier : false;
if (e.NavigationMethod == NavigationMethod.Pointer || if (!toggle && !range)
e.NavigationMethod == NavigationMethod.Directional) {
SelectedIndex = index;
}
else if (multi && range)
{
SynchronizeIndexes(SelectedIndexes, SelectedIndexes[0], index);
}
else
{
var i = SelectedIndexes.IndexOf(index);
if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1))
{
SelectedIndexes.RemoveAt(i);
}
else
{
if (multi)
{
SelectedIndexes.Add(index);
}
else
{
SelectedIndex = index;
}
}
}
}
else
{
LostSelection();
}
}
}
/// <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>
/// <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)
{ {
TrySetSelectionFromContainerEvent(e.Source, true); UpdateSelection(index, select, rangeModifier, toggleModifier);
} }
} }
/// <inheritdoc/> /// <summary>
protected override void OnPointerPressed(PointerPressEventArgs e) /// Updates the selection based on an event source that may have originated in a container
/// that belongs to the control.
/// </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 = true,
bool rangeModifier = false,
bool toggleModifier = false)
{ {
base.OnPointerPressed(e); var item = GetContainerFromEventSource(eventSource);
e.Handled = true;
if (item != null)
{
UpdateSelection(item, select, rangeModifier, toggleModifier);
return true;
}
return false;
} }
/// <summary> /// <summary>
@ -228,30 +409,86 @@ namespace Perspex.Controls.Primitives
} }
/// <summary> /// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>. /// Generates a range of integers between the first and last inclusive.
/// </summary> /// </summary>
/// <param name="container">The container.</param> /// <param name="first">The first integer.</param>
/// <param name="selected">Whether the control is selected</param> /// <param name="last">The last integer.</param>
private static void MarkContainerSelected(IControl container, bool selected) /// <returns>The range.</returns>
private static IEnumerable<int> Range(int first, int last)
{ {
var selectable = container as ISelectable; int step = first > last ? -1 : 1;
var styleable = container as IStyleable;
if (selectable != null) for (int i = first; i != last; i += step)
{ {
selectable.IsSelected = selected; yield return i;
} }
else if (styleable != null)
yield return last;
}
/// <summary>
/// Makes a list of integers equal the range first...last.
/// </summary>
/// <param name="indexes">The list of indexes.</param>
/// <param name="first">The first in the range.</param>
/// <param name="last">The last in the range.</param>
private static void SynchronizeIndexes(IPerspexList<int> indexes, int first, int last)
{
var i = 0;
var next = first;
int step = first > last ? -1 : 1;
while (i < indexes.Count && indexes[i] == next && next != last)
{
++i;
next += step;
}
if (next != last || i != indexes.Count - 1)
{ {
if (selected) if (i < indexes.Count - 1)
{ {
styleable.Classes.Add("selected"); indexes.RemoveRange(i, indexes.Count - i);
} }
else
indexes.AddRange(Range(next, last));
}
}
/// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="selected">Whether the control is selected</param>
private void MarkContainerSelected(IControl container, bool selected)
{
try
{
var selectable = container as ISelectable;
var styleable = container as IStyleable;
_ignoreContainerSelectionChanged = true;
if (selectable != null)
{ {
styleable.Classes.Remove("selected"); selectable.IsSelected = selected;
}
else if (styleable != null)
{
if (selected)
{
styleable.Classes.Add(":selected");
}
else
{
styleable.Classes.Remove(":selected");
}
} }
} }
finally
{
_ignoreContainerSelectionChanged = false;
}
} }
/// <summary> /// <summary>
@ -281,120 +518,177 @@ namespace Perspex.Controls.Primitives
/// <param name="e">The event.</param> /// <param name="e">The event.</param>
private void ContainerSelectionChanged(RoutedEventArgs e) private void ContainerSelectionChanged(RoutedEventArgs e)
{ {
var selectable = (ISelectable)e.Source; if (!_ignoreContainerSelectionChanged)
if (selectable != null)
{ {
TrySetSelectionFromContainerEvent(e.Source, selectable.IsSelected); var selectable = (ISelectable)e.Source;
if (selectable != null)
{
UpdateSelectionFromEventSource(e.Source, selectable.IsSelected);
}
} }
} }
/// <summary> /// <summary>
/// Called when the <see cref="SelectedIndex"/> property changes. /// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary> /// </summary>
/// <param name="e">The event args.</param> /// <param name="index">The index of the item.</param>
private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e) /// <param name="selected">Whether the control is selected</param>
/// <returns>The container.</returns>
private IControl MarkIndexSelected(int index, bool selected)
{ {
var index = (int)e.OldValue; var container = ItemContainerGenerator.ContainerFromIndex(index);
if (index != -1) if (container != null)
{ {
var container = ItemContainerGenerator.ContainerFromIndex(index); MarkContainerSelected(container, selected);
MarkContainerSelected(container, false);
} }
index = (int)e.NewValue; return container;
}
/// <summary>
/// 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="itemIndexes">The item indexes.</param>
private void SelectedIndexesAdded(int listIndex, IEnumerable<int> itemIndexes)
{
var indexes = (itemIndexes as IList<int>) ?? itemIndexes.ToList();
IControl container = null;
if (index == -1) if (SelectedItems.Count != SelectedIndexes.Count)
{ {
SelectedItem = null; var items = indexes.Select(x => Items.Cast<object>().ElementAt(x));
SelectedItems.AddRange(items);
} }
else
foreach (var itemIndex in indexes)
{ {
SelectedItem = Items.Cast<object>().ElementAt((int)e.NewValue); container = MarkIndexSelected(itemIndex, true);
var container = ItemContainerGenerator.ContainerFromIndex(index); }
MarkContainerSelected(container, true);
var inputElement = container as IInputElement; if (SelectedIndexes.Count == 1)
if (inputElement != null && Presenter != null && Presenter.Panel != null) {
{ RaisePropertyChanged(SelectedIndexProperty, -1, SelectedIndexes[0], BindingPriority.LocalValue);
KeyboardNavigation.SetTabOnceActiveElement( }
(InputElement)Presenter.Panel,
inputElement); if (container != null && Presenter?.Panel != null)
} {
KeyboardNavigation.SetTabOnceActiveElement((InputElement)Presenter.Panel, container);
} }
} }
/// <summary> /// <summary>
/// Called when the <see cref="SelectedItem"/> property changes. /// Called when an index is removed from the <see cref="SelectedIndexes"/> collection.
/// </summary> /// </summary>
/// <param name="e">The event args.</param> /// <param name="listIndex">The index in the SelectedIndexes collection.</param>
private void SelectedItemChanged(PerspexPropertyChangedEventArgs e) /// <param name="itemIndexes">The item indexes.</param>
private void SelectedIndexesRemoved(int listIndex, IEnumerable<int> itemIndexes)
{ {
SelectedIndex = IndexOf(Items, e.NewValue); var sync = SelectedIndexes.Count != SelectedItems.Count;
SelectedItems.RemoveRange(listIndex, itemIndexes.Count());
foreach (var itemIndex in itemIndexes)
{
MarkIndexSelected(itemIndex, false);
}
if (SelectedIndexes.Count == 0)
{
RaisePropertyChanged(
SelectedIndexProperty,
itemIndexes.First(),
-1,
BindingPriority.LocalValue);
}
} }
/// <summary> /// <summary>
/// Tries to get the container that was the source of an event. /// Called when an item is added to the <see cref="SelectedItems"/> collection.
/// </summary> /// </summary>
/// <param name="eventSource">The control that raised the event.</param> /// <param name="index">The index in the SelectedItems collection.</param>
/// <returns>The container or null if the event did not originate in a container.</returns> /// <param name="item">The item.</param>
private IControl GetContainerFromEvent(IInteractive eventSource) private void SelectedItemsAdded(int index, object item)
{ {
var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() if (SelectedIndexes.Count != SelectedItems.Count)
.OfType<ILogical>() {
.FirstOrDefault(x => x.LogicalParent == this); SelectedIndexes.Insert(index, IndexOf(Items, item));
}
return item as IControl; if (SelectedItems.Count == 1)
{
RaisePropertyChanged(SelectedItemProperty, null, item, BindingPriority.LocalValue);
}
} }
/// <summary> /// <summary>
/// Called when the currently selected item is lost and the selection must be changed /// Called when an item is removed from the <see cref="SelectedItems"/> collection.
/// depending on the <see cref="AutoSelect"/> property.
/// </summary> /// </summary>
private void LostSelection() /// <param name="index">The index in the SelectedItems collection.</param>
/// <param name="item">The item.</param>
private void SelectedItemsRemoved(int index, object item)
{ {
var items = Items?.Cast<object>(); if (SelectedIndexes.Count != SelectedItems.Count)
{
SelectedIndexes.RemoveAt(index);
}
}
if (items != null && AutoSelect) /// <summary>
/// Called when the <see cref="SelectedItems"/> collection is reset.
/// </summary>
private void SelectionReset()
{
if (SelectedIndexes.Count > 0)
{ {
var index = Math.Min(SelectedIndex, items.Count() - 1); SelectedIndexes.Clear();
}
if (index > -1) if (SelectedItems.Count > 0)
{ {
SelectedItem = items.ElementAt(index); SelectedItems.Clear();
return;
}
} }
SelectedIndex = -1; foreach (var container in ItemContainerGenerator.Containers)
{
MarkContainerSelected(container, false);
}
} }
/// <summary> /// <summary>
/// Tries to set the selection to a container that raised an event. /// Validates items added to the <see cref="SelectedIndexes"/> collection.
/// </summary> /// </summary>
/// <param name="eventSource">The control that raised the event.</param> /// <param name="index">The index to be added.</param>
/// <param name="select">Whether the container should be selected or unselected.</param> private void ValidateIndex(int index)
private void TrySetSelectionFromContainerEvent(IInteractive eventSource, bool select)
{ {
var item = GetContainerFromEvent(eventSource); if (index < 0 || index >= Items?.Cast<object>().Count())
{
throw new IndexOutOfRangeException();
}
}
if (item != null) /// <summary>
/// Called when the currently selected item is lost and the selection must be changed
/// depending on the <see cref="SelectionMode"/> property.
/// </summary>
private void LostSelection()
{
var items = Items?.Cast<object>();
if (items != null && AlwaysSelected)
{ {
var index = ItemContainerGenerator.IndexFromContainer(item); var index = Math.Min(SelectedIndex, items.Count() - 1);
if (index != -1) if (index > -1)
{ {
if (select) SelectedItem = items.ElementAt(index);
{ return;
SelectedIndex = index;
}
else
{
LostSelection();
}
} }
} }
SelectedIndex = -1;
} }
} }
} }

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

@ -5,6 +5,7 @@ using System;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Perspex.Controls.Generators; using Perspex.Controls.Generators;
using Perspex.Input;
namespace Perspex.Controls.Primitives namespace Perspex.Controls.Primitives
{ {
@ -15,7 +16,7 @@ namespace Perspex.Controls.Primitives
static TabStrip() static TabStrip()
{ {
AutoSelectProperty.OverrideDefaultValue<TabStrip>(true); SelectionModeProperty.OverrideDefaultValue<TabStrip>(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false);
} }
@ -47,5 +48,27 @@ namespace Perspex.Controls.Primitives
return result; return result;
} }
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
if (e.NavigationMethod == NavigationMethod.Directional)
{
UpdateSelectionFromEventSource(e.Source);
}
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressEventArgs e)
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left)
{
UpdateSelectionFromEventSource(e.Source);
}
}
} }
} }

2
src/Perspex.Controls/RowDefinitions.cs

@ -17,6 +17,7 @@ namespace Perspex.Controls
/// </summary> /// </summary>
public RowDefinitions() public RowDefinitions()
{ {
ResetBehavior = ResetBehavior.Remove;
} }
/// <summary> /// <summary>
@ -24,6 +25,7 @@ namespace Perspex.Controls
/// </summary> /// </summary>
/// <param name="s">A string representation of the row definitions.</param> /// <param name="s">A string representation of the row definitions.</param>
public RowDefinitions(string s) public RowDefinitions(string s)
: this()
{ {
AddRange(GridLength.ParseLengths(s, CultureInfo.InvariantCulture).Select(x => new RowDefinition(x))); AddRange(GridLength.ParseLengths(s, CultureInfo.InvariantCulture).Select(x => new RowDefinition(x)));
} }

34
src/Perspex.Controls/SelectionMode.cs

@ -0,0 +1,34 @@
// 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;
namespace Perspex.Controls
{
/// <summary>
/// Defines the selection mode for a control which can select multiple items.
/// </summary>
[Flags]
public enum SelectionMode
{
/// <summary>
/// One item can be selected.
/// </summary>
Single = 0x00,
/// <summary>
/// Multiple items can be selected.
/// </summary>
Multiple = 0x01,
/// <summary>
/// Item selection can be toggled by tapping/spacebar.
/// </summary>
Toggle = 0x02,
/// <summary>
/// An item will always be selected as long as there are items to select.
/// </summary>
AlwaysSelected = 0x04,
}
}

2
src/Perspex.Controls/TabControl.cs

@ -33,7 +33,7 @@ namespace Perspex.Controls
/// </summary> /// </summary>
static TabControl() static TabControl()
{ {
AutoSelectProperty.OverrideDefaultValue<TabControl>(true); SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue<TabControl>(false); FocusableProperty.OverrideDefaultValue<TabControl>(false);
SelectedIndexProperty.Changed.AddClassHandler<TabControl>(x => x.SelectedIndexChanged); SelectedIndexProperty.Changed.AddClassHandler<TabControl>(x => x.SelectedIndexChanged);
} }

7
src/Perspex.Controls/Templates/DataTemplates.cs

@ -10,5 +10,12 @@ namespace Perspex.Controls.Templates
/// </summary> /// </summary>
public class DataTemplates : PerspexList<IDataTemplate> public class DataTemplates : PerspexList<IDataTemplate>
{ {
/// <summary>
/// Initializes a new instance of the <see cref="DataTemplates"/> class.
/// </summary>
public DataTemplates()
{
ResetBehavior = ResetBehavior.Remove;
}
} }
} }

14
src/Perspex.Controls/TreeView.cs

@ -2,19 +2,21 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Perspex.Controls.Generators; using Perspex.Controls.Generators;
using Perspex.Controls.Primitives;
using Perspex.Input; using Perspex.Input;
using Perspex.VisualTree;
namespace Perspex.Controls namespace Perspex.Controls
{ {
public class TreeView : ItemsControl public class TreeView : ItemsControl
{ {
public static readonly PerspexProperty<object> SelectedItemProperty = public static readonly PerspexProperty<object> SelectedItemProperty =
PerspexProperty.Register<TreeView, object>("SelectedItem"); SelectingItemsControl.SelectedItemProperty.AddOwner<TreeView>(
o => o.SelectedItem,
(o, v) => o.SelectedItem = v);
private object _selectedItem;
static TreeView() static TreeView()
{ {
@ -33,8 +35,8 @@ namespace Perspex.Controls
public object SelectedItem public object SelectedItem
{ {
get { return GetValue(SelectedItemProperty); } get { return _selectedItem; }
set { SetValue(SelectedItemProperty, value); } set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); }
} }
protected override IItemContainerGenerator CreateItemContainerGenerator() protected override IItemContainerGenerator CreateItemContainerGenerator()

16
src/Perspex.Input/FocusManager.cs

@ -55,7 +55,11 @@ namespace Perspex.Input
/// </summary> /// </summary>
/// <param name="control">The control to focus.</param> /// <param name="control">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</param> /// <param name="method">The method by which focus was changed.</param>
public void Focus(IInputElement control, NavigationMethod method = NavigationMethod.Unspecified) /// <param name="modifiers">Any input modifiers active at the time of focus.</param>
public void Focus(
IInputElement control,
NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None)
{ {
if (control != null) if (control != null)
{ {
@ -65,7 +69,7 @@ namespace Perspex.Input
if (scope != null) if (scope != null)
{ {
Scope = scope; Scope = scope;
SetFocusedElement(scope, control, method); SetFocusedElement(scope, control, method, modifiers);
} }
} }
else if (Current != null) else if (Current != null)
@ -90,6 +94,7 @@ namespace Perspex.Input
/// <param name="scope">The focus scope.</param> /// <param name="scope">The focus scope.</param>
/// <param name="element">The element to focus. May be null.</param> /// <param name="element">The element to focus. May be null.</param>
/// <param name="method">The method by which focus was changed.</param> /// <param name="method">The method by which focus was changed.</param>
/// <param name="modifiers">Any input modifiers active at the time of focus.</param>
/// <remarks> /// <remarks>
/// If the specified scope is the current <see cref="Scope"/> then the keyboard focus /// If the specified scope is the current <see cref="Scope"/> then the keyboard focus
/// will change. /// will change.
@ -97,7 +102,8 @@ namespace Perspex.Input
public void SetFocusedElement( public void SetFocusedElement(
IFocusScope scope, IFocusScope scope,
IInputElement element, IInputElement element,
NavigationMethod method = NavigationMethod.Unspecified) NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None)
{ {
Contract.Requires<ArgumentNullException>(scope != null); Contract.Requires<ArgumentNullException>(scope != null);
@ -105,7 +111,7 @@ namespace Perspex.Input
if (Scope == scope) if (Scope == scope)
{ {
KeyboardDevice.Instance.SetFocusedElement(element, method); KeyboardDevice.Instance.SetFocusedElement(element, method, modifiers);
} }
} }
@ -181,7 +187,7 @@ namespace Perspex.Input
if (element != null) if (element != null)
{ {
Focus(element, NavigationMethod.Pointer); Focus(element, NavigationMethod.Pointer, ev.InputModifiers);
} }
} }
} }

5
src/Perspex.Input/GotFocusEventArgs.cs

@ -14,5 +14,10 @@ namespace Perspex.Input
/// Gets or sets a value indicating how the change in focus occurred. /// Gets or sets a value indicating how the change in focus occurred.
/// </summary> /// </summary>
public NavigationMethod NavigationMethod { get; set; } public NavigationMethod NavigationMethod { get; set; }
/// <summary>
/// Gets or sets any input modifiers active at the time of focus.
/// </summary>
public InputModifiers InputModifiers { get; set; }
} }
} }

6
src/Perspex.Input/IFocusManager.cs

@ -23,7 +23,11 @@ namespace Perspex.Input
/// </summary> /// </summary>
/// <param name="control">The control to focus.</param> /// <param name="control">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</param> /// <param name="method">The method by which focus was changed.</param>
void Focus(IInputElement control, NavigationMethod method = NavigationMethod.Unspecified); /// <param name="modifiers">Any input modifiers active at the time of focus.</param>
void Focus(
IInputElement control,
NavigationMethod method = NavigationMethod.Unspecified,
InputModifiers modifiers = InputModifiers.None);
/// <summary> /// <summary>
/// Notifies the focus manager of a change in focus scope. /// Notifies the focus manager of a change in focus scope.

5
src/Perspex.Input/IKeyboardDevice.cs

@ -30,6 +30,9 @@ namespace Perspex.Input
{ {
IInputElement FocusedElement { get; } IInputElement FocusedElement { get; }
void SetFocusedElement(IInputElement element, NavigationMethod method); void SetFocusedElement(
IInputElement element,
NavigationMethod method,
InputModifiers modifiers);
} }
} }

6
src/Perspex.Input/IKeyboardNavigationHandler.cs

@ -22,6 +22,10 @@ namespace Perspex.Input
/// </summary> /// </summary>
/// <param name="element">The current element.</param> /// <param name="element">The current element.</param>
/// <param name="direction">The direction to move.</param> /// <param name="direction">The direction to move.</param>
void Move(IInputElement element, FocusNavigationDirection direction); /// <param name="modifiers">Any input modifiers active at the time of focus.</param>
void Move(
IInputElement element,
FocusNavigationDirection direction,
InputModifiers modifiers = InputModifiers.None);
} }
} }

6
src/Perspex.Input/KeyboardDevice.cs

@ -45,7 +45,10 @@ namespace Perspex.Input
} }
} }
public void SetFocusedElement(IInputElement element, NavigationMethod method) public void SetFocusedElement(
IInputElement element,
NavigationMethod method,
InputModifiers modifiers)
{ {
if (element != FocusedElement) if (element != FocusedElement)
{ {
@ -68,6 +71,7 @@ namespace Perspex.Input
{ {
RoutedEvent = InputElement.GotFocusEvent, RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = method, NavigationMethod = method,
InputModifiers = modifiers,
}); });
} }
} }

10
src/Perspex.Input/KeyboardNavigationHandler.cs

@ -67,7 +67,11 @@ namespace Perspex.Input
/// </summary> /// </summary>
/// <param name="element">The current element.</param> /// <param name="element">The current element.</param>
/// <param name="direction">The direction to move.</param> /// <param name="direction">The direction to move.</param>
public void Move(IInputElement element, FocusNavigationDirection direction) /// <param name="modifiers">Any input modifiers active at the time of focus.</param>
public void Move(
IInputElement element,
FocusNavigationDirection direction,
InputModifiers modifiers = InputModifiers.None)
{ {
Contract.Requires<ArgumentNullException>(element != null); Contract.Requires<ArgumentNullException>(element != null);
@ -78,7 +82,7 @@ namespace Perspex.Input
var method = direction == FocusNavigationDirection.Next || var method = direction == FocusNavigationDirection.Next ||
direction == FocusNavigationDirection.Previous ? direction == FocusNavigationDirection.Previous ?
NavigationMethod.Tab : NavigationMethod.Directional; NavigationMethod.Tab : NavigationMethod.Directional;
FocusManager.Instance.Focus(next, method); FocusManager.Instance.Focus(next, method, modifiers);
} }
} }
@ -117,7 +121,7 @@ namespace Perspex.Input
if (direction.HasValue) if (direction.HasValue)
{ {
Move(current, direction.Value); Move(current, direction.Value, e.Modifiers);
e.Handled = true; e.Handled = true;
} }
} }

1
src/Perspex.SceneGraph/Visual.cs

@ -124,6 +124,7 @@ namespace Perspex
}); });
_visualChildren = new PerspexList<IVisual>(); _visualChildren = new PerspexList<IVisual>();
_visualChildren.ResetBehavior = ResetBehavior.Remove;
_visualChildren.CollectionChanged += VisualChildrenChanged; _visualChildren.CollectionChanged += VisualChildrenChanged;
} }

4
src/Perspex.Themes.Default/ListBoxItemStyle.cs

@ -30,14 +30,14 @@ namespace Perspex.Themes.Default
new Setter(TemplatedControl.TemplateProperty, new ControlTemplate<ListBoxItem>(Template)), new Setter(TemplatedControl.TemplateProperty, new ControlTemplate<ListBoxItem>(Template)),
}, },
}, },
new Style(x => x.OfType<ListBoxItem>().Class("selected").Template().Name("border")) new Style(x => x.OfType<ListBoxItem>().Class(":selected").Template().Name("border"))
{ {
Setters = new[] Setters = new[]
{ {
new Setter(TemplatedControl.BackgroundProperty, new SolidColorBrush(0xfff0f0f0)), new Setter(TemplatedControl.BackgroundProperty, new SolidColorBrush(0xfff0f0f0)),
}, },
}, },
new Style(x => x.OfType<ListBoxItem>().Class("selected").Class(":focus").Template().Name("border")) new Style(x => x.OfType<ListBoxItem>().Class(":selected").Class(":focus").Template().Name("border"))
{ {
Setters = new[] Setters = new[]
{ {

2
src/Perspex.Themes.Default/MenuItemStyle.cs

@ -47,7 +47,7 @@ namespace Perspex.Themes.Default
new Setter(TemplatedControl.TemplateProperty, new ControlTemplate<MenuItem>(TopLevelTemplate)), new Setter(TemplatedControl.TemplateProperty, new ControlTemplate<MenuItem>(TopLevelTemplate)),
}, },
}, },
new Style(x => x.OfType<MenuItem>().Class("selected").Template().Name("root")) new Style(x => x.OfType<MenuItem>().Class(":selected").Template().Name("root"))
{ {
Setters = new[] Setters = new[]
{ {

2
src/Perspex.Themes.Default/TabItemStyle.cs

@ -32,7 +32,7 @@ namespace Perspex.Themes.Default
new Setter(TemplatedControl.TemplateProperty, new ControlTemplate<TabItem>(Template)), new Setter(TemplatedControl.TemplateProperty, new ControlTemplate<TabItem>(Template)),
}, },
}, },
new Style(x => x.OfType<TabItem>().Class("selected")) new Style(x => x.OfType<TabItem>().Class(":selected"))
{ {
Setters = new[] Setters = new[]
{ {

4
src/Perspex.Themes.Default/TreeViewItemStyle.cs

@ -42,14 +42,14 @@ namespace Perspex.Themes.Default
new Setter(TemplatedControl.PaddingProperty, new Thickness(2)), new Setter(TemplatedControl.PaddingProperty, new Thickness(2)),
}, },
}, },
new Style(x => x.OfType<TreeViewItem>().Class("selected").Template().Name("header")) new Style(x => x.OfType<TreeViewItem>().Class(":selected").Template().Name("header"))
{ {
Setters = new[] Setters = new[]
{ {
new Setter(TemplatedControl.BackgroundProperty, new SolidColorBrush(0xfff0f0f0)), new Setter(TemplatedControl.BackgroundProperty, new SolidColorBrush(0xfff0f0f0)),
}, },
}, },
new Style(x => x.OfType<TreeViewItem>().Class("selected").Class(":focus").Template().Name("header")) new Style(x => x.OfType<TreeViewItem>().Class(":selected").Class(":focus").Template().Name("header"))
{ {
Setters = new[] Setters = new[]
{ {

2
src/Windows/Perspex.Win32/Input/WindowsKeyboardDevice.cs

@ -49,7 +49,7 @@ namespace Perspex.Win32.Input
public void WindowActivated(Window window) public void WindowActivated(Window window)
{ {
SetFocusedElement(window, NavigationMethod.Unspecified); SetFocusedElement(window, NavigationMethod.Unspecified, InputModifiers.None);
} }
public string StringFromVirtualKey(uint virtualKey) public string StringFromVirtualKey(uint virtualKey)

20
tests/Perspex.Base.UnitTests/Collections/PerspexListTests.cs

@ -158,12 +158,32 @@ namespace Perspex.Base.UnitTests.Collections
Assert.True(raised); Assert.True(raised);
} }
[Fact]
public void Clearing_Items_Should_Raise_CollectionChanged_Reset()
{
var target = new PerspexList<int>(new[] { 1, 2, 3 });
var raised = false;
target.CollectionChanged += (s, e) =>
{
Assert.Equal(target, s);
Assert.Equal(NotifyCollectionChangedAction.Reset, e.Action);
raised = true;
};
target.Clear();
Assert.True(raised);
}
[Fact] [Fact]
public void Clearing_Items_Should_Raise_CollectionChanged_Remove() public void Clearing_Items_Should_Raise_CollectionChanged_Remove()
{ {
var target = new PerspexList<int>(new[] { 1, 2, 3 }); var target = new PerspexList<int>(new[] { 1, 2, 3 });
var raised = false; var raised = false;
target.ResetBehavior = ResetBehavior.Remove;
target.CollectionChanged += (s, e) => target.CollectionChanged += (s, e) =>
{ {
Assert.Equal(target, s); Assert.Equal(target, s);

19
tests/Perspex.Controls.UnitTests/ListBoxTests.cs

@ -1,9 +1,7 @@
// Copyright (c) The Perspex Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq; using System.Linq;
using Perspex.Controls;
using Perspex.Controls.Presenters; using Perspex.Controls.Presenters;
using Perspex.Controls.Templates; using Perspex.Controls.Templates;
using Perspex.LogicalTree; using Perspex.LogicalTree;
@ -33,23 +31,6 @@ namespace Perspex.Controls.UnitTests
} }
} }
[Fact]
public void Setting_Item_IsSelected_Sets_ListBox_Selection()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
((ListBoxItem)target.GetLogicalChildren().ElementAt(1)).IsSelected = true;
Assert.Equal("Bar", target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
}
[Fact] [Fact]
public void DataContexts_Should_Be_Correctly_Set() public void DataContexts_Should_Be_Correctly_Set()
{ {

232
tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs

@ -0,0 +1,232 @@
// 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.Linq;
using Perspex.Controls.Presenters;
using Perspex.Controls.Templates;
using Perspex.Input;
using Perspex.LogicalTree;
using Perspex.Styling;
using Xunit;
namespace Perspex.Controls.UnitTests
{
public class ListBoxTests_Single
{
[Fact]
public void Focusing_Item_With_Tab_Should_Not_Select_It()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Tab,
});
Assert.Equal(-1, target.SelectedIndex);
}
[Fact]
public void Focusing_Item_With_Arrow_Key_Should_Select_It()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Directional,
});
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Clicking_Item_Should_Select_It()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Clicking_Selected_Item_Should_Not_Deselect_It()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
target.SelectedIndex = 0;
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Clicking_Item_Should_Select_It_When_SelectionMode_Toggle()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
SelectionMode = SelectionMode.Single | SelectionMode.Toggle,
};
target.ApplyTemplate();
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Clicking_Selected_Item_Should_Deselect_It_When_SelectionMode_Toggle()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
SelectionMode = SelectionMode.Toggle,
};
target.ApplyTemplate();
target.SelectedIndex = 0;
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
Assert.Equal(-1, target.SelectedIndex);
}
[Fact]
public void Clicking_Selected_Item_Should_Not_Deselect_It_When_SelectionMode_ToggleAlwaysSelected()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
SelectionMode = SelectionMode.Toggle | SelectionMode.AlwaysSelected,
};
target.ApplyTemplate();
target.SelectedIndex = 0;
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Clicking_Another_Item_Should_Select_It_When_SelectionMode_Toggle()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
SelectionMode = SelectionMode.Single | SelectionMode.Toggle,
};
target.ApplyTemplate();
target.SelectedIndex = 1;
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Setting_Item_IsSelected_Sets_ListBox_Selection()
{
var target = new ListBox
{
Template = new ControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
((ListBoxItem)target.GetLogicalChildren().ElementAt(1)).IsSelected = true;
Assert.Equal("Bar", target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
}
private Control CreateListBoxTemplate(ITemplatedControl parent)
{
return new ScrollViewer
{
Template = new ControlTemplate(CreateScrollViewerTemplate),
Content = new ItemsPresenter
{
Name = "itemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty),
}
};
}
private Control CreateScrollViewerTemplate(ITemplatedControl parent)
{
return new ScrollContentPresenter
{
[~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty),
};
}
private class Item
{
public Item(string value)
{
Value = value;
}
public string Value { get; }
}
}
}

2
tests/Perspex.Controls.UnitTests/Mixins/SelectableMixinTests.cs

@ -24,7 +24,7 @@ namespace Perspex.Controls.UnitTests.Mixins
target.IsSelected = true; target.IsSelected = true;
Assert.Equal(new[] { "selected" }, target.Classes); Assert.Equal(new[] { ":selected" }, target.Classes);
} }
[Fact] [Fact]

2
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@ -84,6 +84,8 @@
<Compile Include="GridLengthTests.cs" /> <Compile Include="GridLengthTests.cs" />
<Compile Include="ContentPresenterTests.cs" /> <Compile Include="ContentPresenterTests.cs" />
<Compile Include="BorderTests.cs" /> <Compile Include="BorderTests.cs" />
<Compile Include="ListBoxTests_Single.cs" />
<Compile Include="Primitives\SelectingItemsControlTests_Multiple.cs" />
<Compile Include="TreeViewTests.cs" /> <Compile Include="TreeViewTests.cs" />
<Compile Include="Mixins\SelectableMixinTests.cs" /> <Compile Include="Mixins\SelectableMixinTests.cs" />
<Compile Include="StackPanelTests.cs" /> <Compile Include="StackPanelTests.cs" />

95
tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -330,76 +330,6 @@ namespace Perspex.Controls.UnitTests.Primitives
Assert.Equal(-1, target.SelectedIndex); Assert.Equal(-1, target.SelectedIndex);
} }
[Fact]
public void Focusing_Item_With_Pointer_Should_Select_It()
{
var target = new SelectingItemsControl
{
Template = Template(),
Items = new[] { "foo", "bar" },
};
target.ApplyTemplate();
var e = new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Pointer,
};
target.Presenter.Panel.Children[1].RaiseEvent(e);
Assert.Equal(1, target.SelectedIndex);
// GotFocus should be raised on parent control.
Assert.False(e.Handled);
}
[Fact]
public void Focusing_Item_With_Directional_Keys_Should_Select_It()
{
var target = new SelectingItemsControl
{
Template = Template(),
Items = new[] { "foo", "bar" },
};
target.ApplyTemplate();
var e = new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Directional,
};
target.Presenter.Panel.Children[1].RaiseEvent(e);
Assert.Equal(1, target.SelectedIndex);
Assert.False(e.Handled);
}
[Fact]
public void Focusing_Item_With_Tab_Should_Not_Select_It()
{
var target = new SelectingItemsControl
{
Template = Template(),
Items = new[] { "foo", "bar" },
};
target.ApplyTemplate();
var e = new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Tab,
};
target.Presenter.Panel.Children[1].RaiseEvent(e);
Assert.Equal(-1, target.SelectedIndex);
}
[Fact] [Fact]
public void Raising_IsSelectedChanged_On_Item_Should_Update_Selection() public void Raising_IsSelectedChanged_On_Item_Should_Update_Selection()
{ {
@ -490,6 +420,31 @@ namespace Perspex.Controls.UnitTests.Primitives
Assert.Equal(target.SelectedItem, items[1]); Assert.Equal(target.SelectedItem, items[1]);
} }
[Fact]
public void Setting_SelectedItem_Should_Set_Panel_Keyboard_Navigation()
{
var items = new[]
{
new Item(),
new Item(),
};
var target = new SelectingItemsControl
{
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedItem = items[1];
var panel = target.Presenter.Panel;
Assert.Equal(
KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel),
panel.Children[1]);
}
private ControlTemplate Template() private ControlTemplate Template()
{ {
return new ControlTemplate<SelectingItemsControl>(control => return new ControlTemplate<SelectingItemsControl>(control =>

20
tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs

@ -14,9 +14,8 @@ namespace Perspex.Controls.UnitTests.Primitives
[Fact] [Fact]
public void First_Item_Should_Be_Selected() public void First_Item_Should_Be_Selected()
{ {
var target = new SelectingItemsControl var target = new TestSelector
{ {
AutoSelect = true,
Items = new[] { "foo", "bar" }, Items = new[] { "foo", "bar" },
Template = Template(), Template = Template(),
}; };
@ -31,9 +30,8 @@ namespace Perspex.Controls.UnitTests.Primitives
public void First_Item_Should_Be_Selected_When_Added() public void First_Item_Should_Be_Selected_When_Added()
{ {
var items = new PerspexList<string>(); var items = new PerspexList<string>();
var target = new SelectingItemsControl var target = new TestSelector
{ {
AutoSelect = true,
Items = items, Items = items,
Template = Template(), Template = Template(),
}; };
@ -50,9 +48,8 @@ namespace Perspex.Controls.UnitTests.Primitives
{ {
var items = new PerspexList<string>(new[] { "foo", "bar", "baz", "qux" }); var items = new PerspexList<string>(new[] { "foo", "bar", "baz", "qux" });
var target = new SelectingItemsControl var target = new TestSelector
{ {
AutoSelect = true,
Items = items, Items = items,
Template = Template(), Template = Template(),
}; };
@ -70,9 +67,8 @@ namespace Perspex.Controls.UnitTests.Primitives
{ {
var items = new PerspexList<string>(new[] { "foo", "bar" }); var items = new PerspexList<string>(new[] { "foo", "bar" });
var target = new SelectingItemsControl var target = new TestSelector
{ {
AutoSelect = true,
Items = items, Items = items,
Template = Template(), Template = Template(),
}; };
@ -97,6 +93,14 @@ namespace Perspex.Controls.UnitTests.Primitives
}); });
} }
private class TestSelector : SelectingItemsControl
{
static TestSelector()
{
SelectionModeProperty.OverrideDefaultValue<TestSelector>(SelectionMode.AlwaysSelected);
}
}
private class Item : Control, ISelectable private class Item : Control, ISelectable
{ {
public bool IsSelected { get; set; } public bool IsSelected { get; set; }

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

@ -0,0 +1,336 @@
// 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 Perspex.Collections;
using Perspex.Controls.Presenters;
using Perspex.Controls.Primitives;
using Perspex.Controls.Templates;
using Xunit;
namespace Perspex.Controls.UnitTests.Primitives
{
public class SelectingItemsControlTests_Multiple
{
[Fact]
public void Setting_SelectedIndex_Should_Add_To_SelectedIndexes()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndex = 1;
Assert.Equal(new[] { 1 }, target.SelectedIndexes);
}
[Fact]
public void Adding_SelectedIndexes_Should_Set_SelectedIndex()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void Adding_First_SelectedIndex_Should_Raise_SelectedIndex_SelectedItem_Changed()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
bool indexRaised = false;
bool itemRaised = false;
target.PropertyChanged += (s, e) =>
{
indexRaised |= e.Property.Name == "SelectedIndex" &&
(int)e.OldValue == -1 &&
(int)e.NewValue == 1;
itemRaised |= e.Property.Name == "SelectedItem" &&
(string)e.OldValue == null &&
(string)e.NewValue == "bar";
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
Assert.True(indexRaised);
Assert.True(itemRaised);
}
[Fact]
public void Adding_Subsequent_SelectedIndexes_Should_Not_Raise_SelectedIndex_SelectedItem_Changed()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(0);
bool raised = false;
target.PropertyChanged += (s, e) =>
raised |= e.Property.Name == "SelectedIndex" ||
e.Property.Name == "SelectedItem";
target.SelectedIndexes.Add(1);
Assert.False(raised);
}
[Fact]
public void Adding_First_SelectedItem_Should_Raise_SelectedIndex_SelectedItem_Changed()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
bool indexRaised = false;
bool itemRaised = false;
target.PropertyChanged += (s, e) =>
{
indexRaised |= e.Property.Name == "SelectedIndex" &&
(int)e.OldValue == -1 &&
(int)e.NewValue == 1;
itemRaised |= e.Property.Name == "SelectedItem" &&
(string)e.OldValue == null &&
(string)e.NewValue == "bar";
};
target.ApplyTemplate();
target.SelectedItems.Add("bar");
Assert.True(indexRaised);
Assert.True(itemRaised);
}
[Fact]
public void Removing_Last_SelectedIndex_Should_Raise_SelectedIndex_Changed()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar" },
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(0);
bool raised = false;
target.PropertyChanged += (s, e) =>
raised = e.Property.Name == "SelectedIndex" &&
(int)e.OldValue == 0 &&
(int)e.NewValue == -1;
target.SelectedIndexes.RemoveAt(0);
Assert.True(raised);
}
[Fact]
public void Adding_To_SelectedIndexes_Should_Add_To_SelectedItems()
{
var target = new TestSelector
{
Items = new[]
{
"foo",
"bar",
},
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
Assert.Equal(new[] { "bar" }, target.SelectedItems);
}
[Fact]
public void Adding_To_SelectedItems_Should_Add_To_SelectedIndexes()
{
var target = new TestSelector
{
Items = new[]
{
"foo",
"bar",
},
Template = Template(),
};
target.ApplyTemplate();
target.SelectedItems.Add("bar");
Assert.Equal(new[] { 1 }, target.SelectedIndexes);
}
[Fact]
public void Adding_SelectedIndexes_Should_Set_Item_IsSelected()
{
var target = new TestSelector
{
Items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
},
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
Assert.True(((ListBoxItem)target.Presenter.Panel.Children[1]).IsSelected);
}
[Fact]
public void Removing_SelectedIndexes_Should_Clear_Item_IsSelected()
{
var target = new TestSelector
{
Items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
},
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndexes.Add(1);
target.SelectedIndexes.Remove(1);
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);
}
[Fact]
public void Second_Range_Select_Backwards_Should_Select_From_Original_Selection()
{
var target = new TestSelector
{
Items = new[]
{
"foo",
"bar",
"baz",
"qux",
"qiz",
"lol",
},
SelectionMode = SelectionMode.Multiple,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedIndex = 2;
target.SelectRange(5);
target.SelectRange(4);
Assert.Equal(new[] { 2, 3, 4 }, target.SelectedIndexes);
Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems);
}
private class TestSelector : SelectingItemsControl
{
public new IPerspexList<int> SelectedIndexes
{
get { return base.SelectedIndexes; }
}
public new IPerspexList<object> 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()
{
return new ControlTemplate<SelectingItemsControl>(control =>
new ItemsPresenter
{
Name = "itemsPresenter",
[~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
[~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
});
}
}
}
Loading…
Cancel
Save