diff --git a/samples/TestApplication/GalleryStyle.cs b/samples/TestApplication/GalleryStyle.cs index 6bd137060a..44dac4cf85 100644 --- a/samples/TestApplication/GalleryStyle.cs +++ b/samples/TestApplication/GalleryStyle.cs @@ -44,7 +44,7 @@ namespace TestApplication } }, - new Style(s => s.Name("internalStrip").OfType().Child().OfType().Class("selected")) + new Style(s => s.Name("internalStrip").OfType().Child().OfType().Class(":selected")) { Setters = new[] { diff --git a/samples/TestApplication/Program.cs b/samples/TestApplication/Program.cs index 0df84054fc..2b2adb9b16 100644 --- a/samples/TestApplication/Program.cs +++ b/samples/TestApplication/Program.cs @@ -483,8 +483,6 @@ namespace TestApplication } }; - imageDeck.AutoSelect = true; - var next = new Button { VerticalAlignment = VerticalAlignment.Center, diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index 3b6433d5b4..3dfac47f36 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -49,7 +49,7 @@ - + diff --git a/src/Perspex.Animation/PropertyTransitions.cs b/src/Perspex.Animation/PropertyTransitions.cs index ab2fbc68c3..42d2b76904 100644 --- a/src/Perspex.Animation/PropertyTransitions.cs +++ b/src/Perspex.Animation/PropertyTransitions.cs @@ -10,5 +10,12 @@ namespace Perspex.Animation /// public class PropertyTransitions : PerspexList { + /// + /// Initializes a new instance of the class. + /// + public PropertyTransitions() + { + ResetBehavior = ResetBehavior.Remove; + } } } diff --git a/src/Perspex.Base/Collections/IPerspexList.cs b/src/Perspex.Base/Collections/IPerspexList.cs index 90278a6044..2c8debb684 100644 --- a/src/Perspex.Base/Collections/IPerspexList.cs +++ b/src/Perspex.Base/Collections/IPerspexList.cs @@ -11,6 +11,18 @@ namespace Perspex.Collections /// The type of the items in the list. public interface IPerspexList : IList, IPerspexReadOnlyList { + /// + /// Gets the number of items in the list. + /// + new int Count { get; } + + /// + /// Gets or sets the element at the specified index. + /// + /// The index. + /// The element at the requested index. + new T this[int index] { get; set; } + /// /// Adds multiple items to the collection. /// @@ -29,5 +41,12 @@ namespace Perspex.Collections /// /// The items. void RemoveAll(IEnumerable items); + + /// + /// Removes a range of elements from the collection. + /// + /// The first index to remove. + /// The number of items to remove. + void RemoveRange(int index, int count); } } \ No newline at end of file diff --git a/src/Perspex.Base/Collections/PerspexList.cs b/src/Perspex.Base/Collections/PerspexList.cs index 78c2de0b8d..9c200aedaf 100644 --- a/src/Perspex.Base/Collections/PerspexList.cs +++ b/src/Perspex.Base/Collections/PerspexList.cs @@ -10,15 +10,47 @@ using System.Linq; namespace Perspex.Collections { + /// + /// Describes the action notified on a clear of a . + /// + public enum ResetBehavior + { + /// + /// Clearing the list notifies a with a + /// . + /// + Reset, + + /// + /// Clearing the list notifies a with a + /// . + /// + Remove, + } + /// /// A notifying list. /// /// The type of the list items. /// + /// /// PerspexList is similar to - /// except that when the method is called, it notifies with a - /// action, passing the items that were - /// removed. + /// with a few added features: + /// + /// + /// + /// + /// It can be configured to notify the event with a + /// action instead of a + /// when the list is cleared by + /// setting to . + /// removed + /// + /// + /// A function can be used to validate each item before insertion. + /// removed + /// + /// /// public class PerspexList : IPerspexList, IList, INotifyCollectionChanged, INotifyPropertyChanged { @@ -65,6 +97,17 @@ namespace Perspex.Collections /// public int Count => _inner.Count; + /// + /// Gets or sets the reset behavior of the list. + /// + public ResetBehavior ResetBehavior { get; set; } + + /// + /// Gets or sets a validation routine that can be used to validate items before they are + /// added. + /// + public Action Validate { get; set; } + /// bool IList.IsFixedSize => false; @@ -97,6 +140,8 @@ namespace Perspex.Collections set { + Validate?.Invoke(value); + T old = _inner[index]; _inner[index] = value; @@ -128,6 +173,7 @@ namespace Perspex.Collections /// The item. public void Add(T item) { + Validate?.Invoke(item); int index = _inner.Count; _inner.Add(item); NotifyAdd(new[] { item }, index); @@ -141,9 +187,22 @@ namespace Perspex.Collections { Contract.Requires(items != null); - int index = _inner.Count; - _inner.AddRange(items); - NotifyAdd((items as IList) ?? items.ToList(), index); + var list = (items as IList) ?? items.ToList(); + + 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); + } } /// @@ -151,9 +210,12 @@ namespace Perspex.Collections /// public void Clear() { - var old = _inner; - _inner = new List(); - NotifyRemove(old, 0); + if (this.Count > 0) + { + var old = _inner; + _inner = new List(); + NotifyReset(old); + } } /// @@ -204,6 +266,7 @@ namespace Perspex.Collections /// The item. public void Insert(int index, T item) { + Validate?.Invoke(item); _inner.Insert(index, item); NotifyAdd(new[] { item }, index); } @@ -217,8 +280,21 @@ namespace Perspex.Collections { Contract.Requires(items != null); - _inner.InsertRange(index, items); - NotifyAdd((items as IList) ?? items.ToList(), index); + var list = (items as IList) ?? items.ToList(); + + 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); + } } /// @@ -248,7 +324,7 @@ namespace Perspex.Collections { Contract.Requires(items != null); - List removed = new List(); + var list = (items as IList) ?? items.ToList(); foreach (var i in items) { @@ -268,6 +344,21 @@ namespace Perspex.Collections NotifyRemove(new[] { item }, index); } + /// + /// Removes a range of elements from the collection. + /// + /// The first index to remove. + /// The number of items to remove. + public void RemoveRange(int index, int count) + { + if (count > 0) + { + var list = _inner.GetRange(index, count); + _inner.RemoveRange(index, count); + NotifyRemove(list, index); + } + } + /// int IList.Add(object value) { @@ -367,5 +458,30 @@ namespace Perspex.Collections NotifyCountChanged(); } + + /// + /// Raises the event with a reset action. + /// + /// The items that were removed. + 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(); + } } } \ No newline at end of file diff --git a/src/Perspex.Base/Collections/PerspexListExtensions.cs b/src/Perspex.Base/Collections/PerspexListExtensions.cs index 27973ecabe..af70a4aa86 100644 --- a/src/Perspex.Base/Collections/PerspexListExtensions.cs +++ b/src/Perspex.Base/Collections/PerspexListExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; +using System.Linq; using System.Reactive.Disposables; namespace Perspex.Collections @@ -22,55 +23,110 @@ namespace Perspex.Collections /// The collection. /// /// 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. /// /// - /// 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. + /// + /// + /// An action called when the collection is reset. /// /// A disposable used to terminate the subscription. public static IDisposable ForEachItem( this IPerspexReadOnlyList collection, Action added, - Action removed) + Action removed, + Action reset) { + return collection.ForEachItem((_, i) => added(i), (_, i) => removed(i), reset); + } + + /// + /// Invokes an action for each item in a collection and subsequently each item added or + /// removed from the collection. + /// + /// The type of the collection items. + /// The collection. + /// + /// 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. + /// + /// + /// An action called for each item removed from the collection. The parameters passed are + /// the index in the collection and the item. + /// + /// + /// An action called when the collection is reset. + /// + /// A disposable used to terminate the subscription. + public static IDisposable ForEachItem( + this IPerspexReadOnlyList collection, + Action added, + Action removed, + Action reset) + { + int index; + NotifyCollectionChangedEventHandler handler = (_, e) => { switch (e.Action) { case NotifyCollectionChangedAction.Add: - foreach (T i in e.NewItems) + index = e.NewStartingIndex; + + foreach (T item in e.NewItems) { - added(i); + added(index++, item); } break; 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; case NotifyCollectionChangedAction.Remove: - foreach (T i in e.OldItems) + index = e.OldStartingIndex; + + foreach (T item in e.OldItems) { - removed(i); + removed(index++, item); } 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) { - added(i); + added(index++, i); } collection.CollectionChanged += handler; @@ -78,6 +134,66 @@ namespace Perspex.Collections return Disposable.Create(() => collection.CollectionChanged -= handler); } + /// + /// Invokes an action for each item in a collection and subsequently each item added or + /// removed from the collection. + /// + /// The type of the collection items. + /// The collection. + /// + /// 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. + /// + /// + /// 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. + /// + /// + /// An action called when the collection is reset. + /// + /// A disposable used to terminate the subscription. + public static IDisposable ForEachItem( + this IPerspexReadOnlyList collection, + Action> added, + Action> removed, + Action reset) + { + NotifyCollectionChangedEventHandler handler = (_, e) => + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + added(e.NewStartingIndex, e.NewItems.Cast()); + break; + + case NotifyCollectionChangedAction.Replace: + removed(e.OldStartingIndex, e.OldItems.Cast()); + added(e.NewStartingIndex, e.NewItems.Cast()); + break; + + case NotifyCollectionChangedAction.Remove: + removed(e.OldStartingIndex, e.OldItems.Cast()); + 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); + } + /// /// Listens for property changed events from all items in a collection. /// @@ -116,7 +232,8 @@ namespace Perspex.Collections inpc.PropertyChanged -= handler; tracked.Remove(inpc); } - }); + }, + null); return Disposable.Create(() => { diff --git a/src/Perspex.Controls/ColumnDefinitions.cs b/src/Perspex.Controls/ColumnDefinitions.cs index 3d8ca60181..2ac191ee66 100644 --- a/src/Perspex.Controls/ColumnDefinitions.cs +++ b/src/Perspex.Controls/ColumnDefinitions.cs @@ -17,6 +17,7 @@ namespace Perspex.Controls /// public ColumnDefinitions() { + ResetBehavior = ResetBehavior.Remove; } /// @@ -24,6 +25,7 @@ namespace Perspex.Controls /// /// A string representation of the column definitions. public ColumnDefinitions(string s) + : this() { AddRange(GridLength.ParseLengths(s, CultureInfo.InvariantCulture).Select(x => new ColumnDefinition(x))); } diff --git a/src/Perspex.Controls/Control.cs b/src/Perspex.Controls/Control.cs index 443985f3eb..b48b4afca6 100644 --- a/src/Perspex.Controls/Control.cs +++ b/src/Perspex.Controls/Control.cs @@ -271,7 +271,9 @@ namespace Perspex.Controls { if (_logicalChildren == null) { - _logicalChildren = new PerspexList(); + var list = new PerspexList(); + list.ResetBehavior = ResetBehavior.Remove; + _logicalChildren = list; } return _logicalChildren; diff --git a/src/Perspex.Controls/Controls.cs b/src/Perspex.Controls/Controls.cs index a7435259da..16d84ab369 100644 --- a/src/Perspex.Controls/Controls.cs +++ b/src/Perspex.Controls/Controls.cs @@ -16,6 +16,7 @@ namespace Perspex.Controls /// public Controls() { + ResetBehavior = ResetBehavior.Remove; } /// @@ -25,6 +26,7 @@ namespace Perspex.Controls public Controls(IEnumerable items) : base(items) { + ResetBehavior = ResetBehavior.Remove; } } } diff --git a/src/Perspex.Controls/Deck.cs b/src/Perspex.Controls/Deck.cs index dc4ba99301..1cdfdad239 100644 --- a/src/Perspex.Controls/Deck.cs +++ b/src/Perspex.Controls/Deck.cs @@ -31,7 +31,7 @@ namespace Perspex.Controls /// static Deck() { - AutoSelectProperty.OverrideDefaultValue(true); + SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(PanelTemplate); } diff --git a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs index 7d098755c2..2009131bc5 100644 --- a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs @@ -13,6 +13,11 @@ namespace Perspex.Controls.Generators /// public interface IItemContainerGenerator { + /// + /// Gets the currently realized containers. + /// + IEnumerable Containers { get; } + /// /// Signalled whenever new containers are initialized. /// diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs index 4d47f91a36..9e8183c322 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs @@ -28,6 +28,11 @@ namespace Perspex.Controls.Generators Owner = owner; } + /// + /// Gets the currently realized containers. + /// + public IEnumerable Containers => _containers.Values; + /// /// Signalled whenever new containers are initialized. /// diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index 66a2db8a4f..fa8c699ec3 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -29,6 +29,11 @@ namespace Perspex.Controls.Generators Owner = owner; } + /// + /// Gets the currently realized containers. + /// + public IEnumerable Containers => _containers.Values; + /// /// Signalled whenever new containers are initialized. /// diff --git a/src/Perspex.Controls/ListBox.cs b/src/Perspex.Controls/ListBox.cs index 6b839656e7..773a5ceb6e 100644 --- a/src/Perspex.Controls/ListBox.cs +++ b/src/Perspex.Controls/ListBox.cs @@ -3,6 +3,7 @@ using Perspex.Controls.Generators; using Perspex.Controls.Primitives; +using Perspex.Input; namespace Perspex.Controls { @@ -11,10 +12,52 @@ namespace Perspex.Controls /// public class ListBox : SelectingItemsControl { + /// + /// Defines the property. + /// + public static readonly new PerspexProperty SelectionModeProperty = + SelectingItemsControl.SelectionModeProperty; + + /// + public new SelectionMode SelectionMode + { + get { return base.SelectionMode; } + set { base.SelectionMode = value; } + } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator(this); } + + /// + protected override void OnGotFocus(GotFocusEventArgs e) + { + base.OnGotFocus(e); + + if (e.NavigationMethod == NavigationMethod.Directional) + { + UpdateSelectionFromEventSource( + e.Source, + true, + (e.InputModifiers & InputModifiers.Shift) != 0); + } + } + + /// + 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); + } + } } } diff --git a/src/Perspex.Controls/Mixins/SelectableMixin.cs b/src/Perspex.Controls/Mixins/SelectableMixin.cs index b4160218dc..75abe9a849 100644 --- a/src/Perspex.Controls/Mixins/SelectableMixin.cs +++ b/src/Perspex.Controls/Mixins/SelectableMixin.cs @@ -52,7 +52,7 @@ namespace Perspex.Controls.Mixins { if ((bool)x.NewValue) { - sender.Classes.Add("selected"); + sender.Classes.Add(":selected"); if (((IVisual)sender).IsAttachedToVisualTree) { @@ -61,7 +61,7 @@ namespace Perspex.Controls.Mixins } else { - sender.Classes.Remove("selected"); + sender.Classes.Remove(":selected"); } sender.RaiseEvent(new RoutedEventArgs diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index 3cec434017..7510ce450c 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -44,6 +44,7 @@ + diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 2a3841d08a..db6838104c 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -3,8 +3,10 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using Perspex.Collections; using Perspex.Controls.Generators; using Perspex.Input; using Perspex.Interactivity; @@ -17,16 +19,23 @@ namespace Perspex.Controls.Primitives /// An that maintains a selection. /// /// - /// TODO: Support multiple selection. + /// + /// provides a base class for s + /// that maintain a selection (single or multiple). By default only its + /// and properties are visible; the + /// multiple selection properties and + /// together with the properties are protected, however a derived + /// class can expose these if it wishes to support multiple selection. + /// + /// + /// maintains a selection respecting the current + /// but it does not react to user input; this must be handled in a + /// derived class. It does, however, respond to events + /// from items and updates the selection accordingly. + /// /// public class SelectingItemsControl : ItemsControl { - /// - /// Defines the property. - /// - public static readonly PerspexProperty AutoSelectProperty = - PerspexProperty.Register("AutoSelect"); - /// /// Defines the property. /// @@ -45,6 +54,29 @@ namespace Perspex.Controls.Primitives o => o.SelectedItem, (o, v) => o.SelectedItem = v); + /// + /// Defines the property. + /// + protected static readonly PerspexProperty> SelectedIndexesProperty = + PerspexProperty.RegisterDirect>( + nameof(SelectedIndexes), + o => o.SelectedIndexes); + + /// + /// Defines the property. + /// + protected static readonly PerspexProperty> SelectedItemsProperty = + PerspexProperty.RegisterDirect>( + nameof(SelectedItems), + o => o.SelectedItems); + + /// + /// Defines the property. + /// + protected static readonly PerspexProperty SelectionModeProperty = + PerspexProperty.Register( + nameof(SelectionMode)); + /// /// Event that should be raised by items that implement to /// notify the parent that their selection state @@ -53,8 +85,9 @@ namespace Perspex.Controls.Primitives public static readonly RoutedEvent IsSelectedChangedEvent = RoutedEvent.Register("IsSelectedChanged", RoutingStrategies.Bubble); - private int _selectedIndex = -1; - private object _selectedItem; + private PerspexList _selectedIndexes = new PerspexList(); + private PerspexList _selectedItems = new PerspexList(); + private bool _ignoreContainerSelectionChanged; /// /// Initializes static members of the class. @@ -62,8 +95,6 @@ namespace Perspex.Controls.Primitives static SelectingItemsControl() { IsSelectedChangedEvent.AddClassHandler(x => x.ContainerSelectionChanged); - SelectedIndexProperty.Changed.AddClassHandler(x => x.SelectedIndexChanged); - SelectedItemProperty.Changed.AddClassHandler(x => x.SelectedItemChanged); } /// @@ -72,16 +103,9 @@ namespace Perspex.Controls.Primitives public SelectingItemsControl() { ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized); - } - - /// - /// Gets or sets a value indicating whether the control should always try to keep an item - /// selected where possible. - /// - public bool AutoSelect - { - get { return GetValue(AutoSelectProperty); } - set { SetValue(AutoSelectProperty, value); } + _selectedIndexes.Validate = ValidateIndex; + _selectedIndexes.ForEachItem(SelectedIndexesAdded, SelectedIndexesRemoved, SelectionReset); + _selectedItems.ForEachItem(SelectedItemsAdded, SelectedItemsRemoved, SelectionReset); } /// @@ -91,13 +115,25 @@ namespace Perspex.Controls.Primitives { get { - return _selectedIndex; + return _selectedIndexes.Count > 0 ? _selectedIndexes[0]: -1; } set { - value = (value >= 0 && value < Items?.Cast().Count()) ? value : -1; - SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value); + var old = SelectedIndex; + var effective = (value >= 0 && value < Items?.Cast().Count()) ? value : -1; + + if (old != effective) + { + _selectedIndexes.Clear(); + + if (effective != -1) + { + _selectedIndexes.Add(effective); + } + + RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue); + } } } @@ -108,16 +144,72 @@ namespace Perspex.Controls.Primitives { get { - return _selectedItem; + return _selectedItems.FirstOrDefault(); } set { - value = Items?.Cast().Contains(value) == true ? value : null; - SetAndRaise(SelectedItemProperty, ref _selectedItem, value); + var old = SelectedItem; + var effective = Items?.Cast().Contains(value) == true ? value : null; + + if (effective != old) + { + _selectedItems.Clear(); + + if (effective != null) + { + _selectedItems.Add(effective); + } + + RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue); + } } } + /// + /// Gets the selected indexes. + /// + protected IPerspexList SelectedIndexes + { + get { return _selectedIndexes; } + } + + /// + /// Gets the selected items. + /// + protected IPerspexList SelectedItems + { + get { return _selectedItems; } + } + + /// + /// Gets or sets the selection mode. + /// + protected SelectionMode SelectionMode + { + get { return GetValue(SelectionModeProperty); } + set { SetValue(SelectionModeProperty, value); } + } + + /// + /// Gets a value indicating whether is set. + /// + protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0; + + /// + /// Tries to get the container that was the source of an event. + /// + /// The control that raised the event. + /// The container or null if the event did not originate in a container. + protected IControl GetContainerFromEventSource(IInteractive eventSource) + { + var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(x => x.LogicalParent == this); + + return item as IControl; + } + /// protected override void ItemsChanged(PerspexPropertyChangedEventArgs e) { @@ -127,7 +219,7 @@ namespace Perspex.Controls.Primitives { SelectedIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem); } - else if (AutoSelect && Items != null & Items.Cast().Any()) + else if (AlwaysSelected && Items != null & Items.Cast().Any()) { SelectedIndex = 0; } @@ -141,7 +233,7 @@ namespace Perspex.Controls.Primitives switch (e.Action) { case NotifyCollectionChangedAction.Add: - if (AutoSelect && SelectedIndex == -1) + if (AlwaysSelected && SelectedIndex == -1) { SelectedIndex = 0; } @@ -155,7 +247,7 @@ namespace Perspex.Controls.Primitives if (selectedIndex >= e.OldStartingIndex && selectedIndex < e.OldStartingIndex + e.OldItems.Count) { - if (!AutoSelect) + if (!AlwaysSelected) { SelectedIndex = -1; } @@ -173,23 +265,112 @@ namespace Perspex.Controls.Primitives } } - /// - protected override void OnGotFocus(GotFocusEventArgs e) + /// + /// Updates the selection for an item based on user interaction. + /// + /// The index of the item. + /// Whether the item should be selected or unselected. + /// Whether the range modifier is enabled (i.e. shift key). + /// Whether the toggle modifier is enabled (i.e. ctrl key). + protected void UpdateSelection( + int index, + bool select = true, + bool rangeModifier = false, + bool toggleModifier = false) { - 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 || - e.NavigationMethod == NavigationMethod.Directional) + if (!toggle && !range) + { + 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(); + } + } + } + + /// + /// Updates the selection for a container based on user interaction. + /// + /// The container. + /// Whether the container should be selected or unselected. + /// Whether the range modifier is enabled (i.e. shift key). + /// Whether the toggle modifier is enabled (i.e. ctrl key). + protected void UpdateSelection( + IControl container, + bool select = true, + bool rangeModifier = false, + bool toggleModifier = false) + { + var index = ItemContainerGenerator.IndexFromContainer(container); + + if (index != -1) { - TrySetSelectionFromContainerEvent(e.Source, true); + UpdateSelection(index, select, rangeModifier, toggleModifier); } } - /// - 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. + /// + /// The control that raised the event. + /// Whether the container should be selected or unselected. + /// Whether the range modifier is enabled (i.e. shift key). + /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// + /// True if the event originated from a container that belongs to the control; otherwise + /// false. + /// + protected bool UpdateSelectionFromEventSource( + IInteractive eventSource, + bool select = true, + bool rangeModifier = false, + bool toggleModifier = false) { - base.OnPointerPressed(e); - e.Handled = true; + var item = GetContainerFromEventSource(eventSource); + + if (item != null) + { + UpdateSelection(item, select, rangeModifier, toggleModifier); + return true; + } + + return false; } /// @@ -228,30 +409,86 @@ namespace Perspex.Controls.Primitives } /// - /// Sets a container's 'selected' class or . + /// Generates a range of integers between the first and last inclusive. /// - /// The container. - /// Whether the control is selected - private static void MarkContainerSelected(IControl container, bool selected) + /// The first integer. + /// The last integer. + /// The range. + private static IEnumerable Range(int first, int last) { - var selectable = container as ISelectable; - var styleable = container as IStyleable; + int step = first > last ? -1 : 1; - if (selectable != null) + for (int i = first; i != last; i += step) { - selectable.IsSelected = selected; + yield return i; } - else if (styleable != null) + + yield return last; + } + + /// + /// Makes a list of integers equal the range first...last. + /// + /// The list of indexes. + /// The first in the range. + /// The last in the range. + private static void SynchronizeIndexes(IPerspexList 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)); + } + } + + /// + /// Sets a container's 'selected' class or . + /// + /// The container. + /// Whether the control is selected + private void MarkContainerSelected(IControl container, bool selected) + { + try + { + var selectable = container as ISelectable; + var styleable = container as IStyleable; + + _ignoreContainerSelectionChanged = true; + + if (selectable != null) { - 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; + } } /// @@ -281,120 +518,177 @@ namespace Perspex.Controls.Primitives /// The event. private void ContainerSelectionChanged(RoutedEventArgs e) { - var selectable = (ISelectable)e.Source; - - if (selectable != null) + if (!_ignoreContainerSelectionChanged) { - TrySetSelectionFromContainerEvent(e.Source, selectable.IsSelected); + var selectable = (ISelectable)e.Source; + + if (selectable != null) + { + UpdateSelectionFromEventSource(e.Source, selectable.IsSelected); + } } } /// - /// Called when the property changes. + /// Sets an item container's 'selected' class or . /// - /// The event args. - private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e) + /// The index of the item. + /// Whether the control is selected + /// The container. + 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, false); + MarkContainerSelected(container, selected); } - index = (int)e.NewValue; + return container; + } + + /// + /// Called when an index is added to the collection. + /// + /// The index in the SelectedIndexes collection. + /// The item indexes. + private void SelectedIndexesAdded(int listIndex, IEnumerable itemIndexes) + { + var indexes = (itemIndexes as IList) ?? itemIndexes.ToList(); + IControl container = null; - if (index == -1) + if (SelectedItems.Count != SelectedIndexes.Count) { - SelectedItem = null; + var items = indexes.Select(x => Items.Cast().ElementAt(x)); + SelectedItems.AddRange(items); } - else + + foreach (var itemIndex in indexes) { - SelectedItem = Items.Cast().ElementAt((int)e.NewValue); - var container = ItemContainerGenerator.ContainerFromIndex(index); - MarkContainerSelected(container, true); + container = MarkIndexSelected(itemIndex, true); + } - var inputElement = container as IInputElement; - if (inputElement != null && Presenter != null && Presenter.Panel != null) - { - KeyboardNavigation.SetTabOnceActiveElement( - (InputElement)Presenter.Panel, - inputElement); - } + if (SelectedIndexes.Count == 1) + { + RaisePropertyChanged(SelectedIndexProperty, -1, SelectedIndexes[0], BindingPriority.LocalValue); + } + + if (container != null && Presenter?.Panel != null) + { + KeyboardNavigation.SetTabOnceActiveElement((InputElement)Presenter.Panel, container); } } /// - /// Called when the property changes. + /// Called when an index is removed from the collection. /// - /// The event args. - private void SelectedItemChanged(PerspexPropertyChangedEventArgs e) + /// The index in the SelectedIndexes collection. + /// The item indexes. + private void SelectedIndexesRemoved(int listIndex, IEnumerable 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); + } } /// - /// Tries to get the container that was the source of an event. + /// Called when an item is added to the collection. /// - /// The control that raised the event. - /// The container or null if the event did not originate in a container. - private IControl GetContainerFromEvent(IInteractive eventSource) + /// The index in the SelectedItems collection. + /// The item. + private void SelectedItemsAdded(int index, object item) { - var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.LogicalParent == this); + if (SelectedIndexes.Count != SelectedItems.Count) + { + SelectedIndexes.Insert(index, IndexOf(Items, item)); + } - return item as IControl; + if (SelectedItems.Count == 1) + { + RaisePropertyChanged(SelectedItemProperty, null, item, BindingPriority.LocalValue); + } } /// - /// Called when the currently selected item is lost and the selection must be changed - /// depending on the property. + /// Called when an item is removed from the collection. /// - private void LostSelection() + /// The index in the SelectedItems collection. + /// The item. + private void SelectedItemsRemoved(int index, object item) { - var items = Items?.Cast(); + if (SelectedIndexes.Count != SelectedItems.Count) + { + SelectedIndexes.RemoveAt(index); + } + } - if (items != null && AutoSelect) + /// + /// Called when the collection is reset. + /// + private void SelectionReset() + { + if (SelectedIndexes.Count > 0) { - var index = Math.Min(SelectedIndex, items.Count() - 1); + SelectedIndexes.Clear(); + } - if (index > -1) - { - SelectedItem = items.ElementAt(index); - return; - } + if (SelectedItems.Count > 0) + { + SelectedItems.Clear(); } - SelectedIndex = -1; + foreach (var container in ItemContainerGenerator.Containers) + { + MarkContainerSelected(container, false); + } } /// - /// Tries to set the selection to a container that raised an event. + /// Validates items added to the collection. /// - /// The control that raised the event. - /// Whether the container should be selected or unselected. - private void TrySetSelectionFromContainerEvent(IInteractive eventSource, bool select) + /// The index to be added. + private void ValidateIndex(int index) { - var item = GetContainerFromEvent(eventSource); + if (index < 0 || index >= Items?.Cast().Count()) + { + throw new IndexOutOfRangeException(); + } + } - if (item != null) + /// + /// Called when the currently selected item is lost and the selection must be changed + /// depending on the property. + /// + private void LostSelection() + { + var items = Items?.Cast(); + + if (items != null && AlwaysSelected) { - var index = ItemContainerGenerator.IndexFromContainer(item); + var index = Math.Min(SelectedIndex, items.Count() - 1); - if (index != -1) + if (index > -1) { - if (select) - { - SelectedIndex = index; - } - else - { - LostSelection(); - } + SelectedItem = items.ElementAt(index); + return; } } + + SelectedIndex = -1; } } } diff --git a/src/Perspex.Controls/Primitives/TabStrip.cs b/src/Perspex.Controls/Primitives/TabStrip.cs index 5e1038517c..8231a5f121 100644 --- a/src/Perspex.Controls/Primitives/TabStrip.cs +++ b/src/Perspex.Controls/Primitives/TabStrip.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using System.Reactive.Linq; using Perspex.Controls.Generators; +using Perspex.Input; namespace Perspex.Controls.Primitives { @@ -15,7 +16,7 @@ namespace Perspex.Controls.Primitives static TabStrip() { - AutoSelectProperty.OverrideDefaultValue(true); + SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); } @@ -47,5 +48,27 @@ namespace Perspex.Controls.Primitives return result; } + + /// + protected override void OnGotFocus(GotFocusEventArgs e) + { + base.OnGotFocus(e); + + if (e.NavigationMethod == NavigationMethod.Directional) + { + UpdateSelectionFromEventSource(e.Source); + } + } + + /// + protected override void OnPointerPressed(PointerPressEventArgs e) + { + base.OnPointerPressed(e); + + if (e.MouseButton == MouseButton.Left) + { + UpdateSelectionFromEventSource(e.Source); + } + } } } diff --git a/src/Perspex.Controls/RowDefinitions.cs b/src/Perspex.Controls/RowDefinitions.cs index 7ebc6a8fb5..e8e418dafe 100644 --- a/src/Perspex.Controls/RowDefinitions.cs +++ b/src/Perspex.Controls/RowDefinitions.cs @@ -17,6 +17,7 @@ namespace Perspex.Controls /// public RowDefinitions() { + ResetBehavior = ResetBehavior.Remove; } /// @@ -24,6 +25,7 @@ namespace Perspex.Controls /// /// A string representation of the row definitions. public RowDefinitions(string s) + : this() { AddRange(GridLength.ParseLengths(s, CultureInfo.InvariantCulture).Select(x => new RowDefinition(x))); } diff --git a/src/Perspex.Controls/SelectionMode.cs b/src/Perspex.Controls/SelectionMode.cs new file mode 100644 index 0000000000..bbf3a55259 --- /dev/null +++ b/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 +{ + /// + /// Defines the selection mode for a control which can select multiple items. + /// + [Flags] + public enum SelectionMode + { + /// + /// One item can be selected. + /// + Single = 0x00, + + /// + /// Multiple items can be selected. + /// + Multiple = 0x01, + + /// + /// Item selection can be toggled by tapping/spacebar. + /// + Toggle = 0x02, + + /// + /// An item will always be selected as long as there are items to select. + /// + AlwaysSelected = 0x04, + } +} diff --git a/src/Perspex.Controls/TabControl.cs b/src/Perspex.Controls/TabControl.cs index 632137824d..ee87741980 100644 --- a/src/Perspex.Controls/TabControl.cs +++ b/src/Perspex.Controls/TabControl.cs @@ -33,7 +33,7 @@ namespace Perspex.Controls /// static TabControl() { - AutoSelectProperty.OverrideDefaultValue(true); + SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(false); SelectedIndexProperty.Changed.AddClassHandler(x => x.SelectedIndexChanged); } diff --git a/src/Perspex.Controls/Templates/DataTemplates.cs b/src/Perspex.Controls/Templates/DataTemplates.cs index cc0bc24792..be9bd20e99 100644 --- a/src/Perspex.Controls/Templates/DataTemplates.cs +++ b/src/Perspex.Controls/Templates/DataTemplates.cs @@ -10,5 +10,12 @@ namespace Perspex.Controls.Templates /// public class DataTemplates : PerspexList { + /// + /// Initializes a new instance of the class. + /// + public DataTemplates() + { + ResetBehavior = ResetBehavior.Remove; + } } } \ No newline at end of file diff --git a/src/Perspex.Controls/TreeView.cs b/src/Perspex.Controls/TreeView.cs index 369882fd18..c077d42aee 100644 --- a/src/Perspex.Controls/TreeView.cs +++ b/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. using System; -using System.Collections; -using System.Collections.Generic; using System.Linq; using Perspex.Controls.Generators; +using Perspex.Controls.Primitives; using Perspex.Input; -using Perspex.VisualTree; namespace Perspex.Controls { public class TreeView : ItemsControl { public static readonly PerspexProperty SelectedItemProperty = - PerspexProperty.Register("SelectedItem"); + SelectingItemsControl.SelectedItemProperty.AddOwner( + o => o.SelectedItem, + (o, v) => o.SelectedItem = v); + + private object _selectedItem; static TreeView() { @@ -33,8 +35,8 @@ namespace Perspex.Controls public object SelectedItem { - get { return GetValue(SelectedItemProperty); } - set { SetValue(SelectedItemProperty, value); } + get { return _selectedItem; } + set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); } } protected override IItemContainerGenerator CreateItemContainerGenerator() diff --git a/src/Perspex.Input/FocusManager.cs b/src/Perspex.Input/FocusManager.cs index 945e9ad782..67fef2588c 100644 --- a/src/Perspex.Input/FocusManager.cs +++ b/src/Perspex.Input/FocusManager.cs @@ -55,7 +55,11 @@ namespace Perspex.Input /// /// The control to focus. /// The method by which focus was changed. - public void Focus(IInputElement control, NavigationMethod method = NavigationMethod.Unspecified) + /// Any input modifiers active at the time of focus. + public void Focus( + IInputElement control, + NavigationMethod method = NavigationMethod.Unspecified, + InputModifiers modifiers = InputModifiers.None) { if (control != null) { @@ -65,7 +69,7 @@ namespace Perspex.Input if (scope != null) { Scope = scope; - SetFocusedElement(scope, control, method); + SetFocusedElement(scope, control, method, modifiers); } } else if (Current != null) @@ -90,6 +94,7 @@ namespace Perspex.Input /// The focus scope. /// The element to focus. May be null. /// The method by which focus was changed. + /// Any input modifiers active at the time of focus. /// /// If the specified scope is the current then the keyboard focus /// will change. @@ -97,7 +102,8 @@ namespace Perspex.Input public void SetFocusedElement( IFocusScope scope, IInputElement element, - NavigationMethod method = NavigationMethod.Unspecified) + NavigationMethod method = NavigationMethod.Unspecified, + InputModifiers modifiers = InputModifiers.None) { Contract.Requires(scope != null); @@ -105,7 +111,7 @@ namespace Perspex.Input if (Scope == scope) { - KeyboardDevice.Instance.SetFocusedElement(element, method); + KeyboardDevice.Instance.SetFocusedElement(element, method, modifiers); } } @@ -181,7 +187,7 @@ namespace Perspex.Input if (element != null) { - Focus(element, NavigationMethod.Pointer); + Focus(element, NavigationMethod.Pointer, ev.InputModifiers); } } } diff --git a/src/Perspex.Input/GotFocusEventArgs.cs b/src/Perspex.Input/GotFocusEventArgs.cs index 492ea75074..a29a9f966a 100644 --- a/src/Perspex.Input/GotFocusEventArgs.cs +++ b/src/Perspex.Input/GotFocusEventArgs.cs @@ -14,5 +14,10 @@ namespace Perspex.Input /// Gets or sets a value indicating how the change in focus occurred. /// public NavigationMethod NavigationMethod { get; set; } + + /// + /// Gets or sets any input modifiers active at the time of focus. + /// + public InputModifiers InputModifiers { get; set; } } } diff --git a/src/Perspex.Input/IFocusManager.cs b/src/Perspex.Input/IFocusManager.cs index c813c82338..7794faebdf 100644 --- a/src/Perspex.Input/IFocusManager.cs +++ b/src/Perspex.Input/IFocusManager.cs @@ -23,7 +23,11 @@ namespace Perspex.Input /// /// The control to focus. /// The method by which focus was changed. - void Focus(IInputElement control, NavigationMethod method = NavigationMethod.Unspecified); + /// Any input modifiers active at the time of focus. + void Focus( + IInputElement control, + NavigationMethod method = NavigationMethod.Unspecified, + InputModifiers modifiers = InputModifiers.None); /// /// Notifies the focus manager of a change in focus scope. diff --git a/src/Perspex.Input/IKeyboardDevice.cs b/src/Perspex.Input/IKeyboardDevice.cs index fdf35fba1f..e0ff4c5449 100644 --- a/src/Perspex.Input/IKeyboardDevice.cs +++ b/src/Perspex.Input/IKeyboardDevice.cs @@ -30,6 +30,9 @@ namespace Perspex.Input { IInputElement FocusedElement { get; } - void SetFocusedElement(IInputElement element, NavigationMethod method); + void SetFocusedElement( + IInputElement element, + NavigationMethod method, + InputModifiers modifiers); } } diff --git a/src/Perspex.Input/IKeyboardNavigationHandler.cs b/src/Perspex.Input/IKeyboardNavigationHandler.cs index fd79e941b6..074279d718 100644 --- a/src/Perspex.Input/IKeyboardNavigationHandler.cs +++ b/src/Perspex.Input/IKeyboardNavigationHandler.cs @@ -22,6 +22,10 @@ namespace Perspex.Input /// /// The current element. /// The direction to move. - void Move(IInputElement element, FocusNavigationDirection direction); + /// Any input modifiers active at the time of focus. + void Move( + IInputElement element, + FocusNavigationDirection direction, + InputModifiers modifiers = InputModifiers.None); } } \ No newline at end of file diff --git a/src/Perspex.Input/KeyboardDevice.cs b/src/Perspex.Input/KeyboardDevice.cs index 8c2154eb8c..3032a663bd 100644 --- a/src/Perspex.Input/KeyboardDevice.cs +++ b/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) { @@ -68,6 +71,7 @@ namespace Perspex.Input { RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = method, + InputModifiers = modifiers, }); } } diff --git a/src/Perspex.Input/KeyboardNavigationHandler.cs b/src/Perspex.Input/KeyboardNavigationHandler.cs index 9c4ac508fd..7ad5a6a9ab 100644 --- a/src/Perspex.Input/KeyboardNavigationHandler.cs +++ b/src/Perspex.Input/KeyboardNavigationHandler.cs @@ -67,7 +67,11 @@ namespace Perspex.Input /// /// The current element. /// The direction to move. - public void Move(IInputElement element, FocusNavigationDirection direction) + /// Any input modifiers active at the time of focus. + public void Move( + IInputElement element, + FocusNavigationDirection direction, + InputModifiers modifiers = InputModifiers.None) { Contract.Requires(element != null); @@ -78,7 +82,7 @@ namespace Perspex.Input var method = direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Previous ? 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) { - Move(current, direction.Value); + Move(current, direction.Value, e.Modifiers); e.Handled = true; } } diff --git a/src/Perspex.SceneGraph/Visual.cs b/src/Perspex.SceneGraph/Visual.cs index 84940468ea..a63b8b0aad 100644 --- a/src/Perspex.SceneGraph/Visual.cs +++ b/src/Perspex.SceneGraph/Visual.cs @@ -124,6 +124,7 @@ namespace Perspex }); _visualChildren = new PerspexList(); + _visualChildren.ResetBehavior = ResetBehavior.Remove; _visualChildren.CollectionChanged += VisualChildrenChanged; } diff --git a/src/Perspex.Themes.Default/ListBoxItemStyle.cs b/src/Perspex.Themes.Default/ListBoxItemStyle.cs index f73b338ca1..e1589dee26 100644 --- a/src/Perspex.Themes.Default/ListBoxItemStyle.cs +++ b/src/Perspex.Themes.Default/ListBoxItemStyle.cs @@ -30,14 +30,14 @@ namespace Perspex.Themes.Default new Setter(TemplatedControl.TemplateProperty, new ControlTemplate(Template)), }, }, - new Style(x => x.OfType().Class("selected").Template().Name("border")) + new Style(x => x.OfType().Class(":selected").Template().Name("border")) { Setters = new[] { new Setter(TemplatedControl.BackgroundProperty, new SolidColorBrush(0xfff0f0f0)), }, }, - new Style(x => x.OfType().Class("selected").Class(":focus").Template().Name("border")) + new Style(x => x.OfType().Class(":selected").Class(":focus").Template().Name("border")) { Setters = new[] { diff --git a/src/Perspex.Themes.Default/MenuItemStyle.cs b/src/Perspex.Themes.Default/MenuItemStyle.cs index 3ff1638dc6..e79c6b7179 100644 --- a/src/Perspex.Themes.Default/MenuItemStyle.cs +++ b/src/Perspex.Themes.Default/MenuItemStyle.cs @@ -47,7 +47,7 @@ namespace Perspex.Themes.Default new Setter(TemplatedControl.TemplateProperty, new ControlTemplate(TopLevelTemplate)), }, }, - new Style(x => x.OfType().Class("selected").Template().Name("root")) + new Style(x => x.OfType().Class(":selected").Template().Name("root")) { Setters = new[] { diff --git a/src/Perspex.Themes.Default/TabItemStyle.cs b/src/Perspex.Themes.Default/TabItemStyle.cs index 44dcd59d5b..f4163ea0db 100644 --- a/src/Perspex.Themes.Default/TabItemStyle.cs +++ b/src/Perspex.Themes.Default/TabItemStyle.cs @@ -32,7 +32,7 @@ namespace Perspex.Themes.Default new Setter(TemplatedControl.TemplateProperty, new ControlTemplate(Template)), }, }, - new Style(x => x.OfType().Class("selected")) + new Style(x => x.OfType().Class(":selected")) { Setters = new[] { diff --git a/src/Perspex.Themes.Default/TreeViewItemStyle.cs b/src/Perspex.Themes.Default/TreeViewItemStyle.cs index 96e0dbbe81..3485041a62 100644 --- a/src/Perspex.Themes.Default/TreeViewItemStyle.cs +++ b/src/Perspex.Themes.Default/TreeViewItemStyle.cs @@ -42,14 +42,14 @@ namespace Perspex.Themes.Default new Setter(TemplatedControl.PaddingProperty, new Thickness(2)), }, }, - new Style(x => x.OfType().Class("selected").Template().Name("header")) + new Style(x => x.OfType().Class(":selected").Template().Name("header")) { Setters = new[] { new Setter(TemplatedControl.BackgroundProperty, new SolidColorBrush(0xfff0f0f0)), }, }, - new Style(x => x.OfType().Class("selected").Class(":focus").Template().Name("header")) + new Style(x => x.OfType().Class(":selected").Class(":focus").Template().Name("header")) { Setters = new[] { diff --git a/src/Windows/Perspex.Win32/Input/WindowsKeyboardDevice.cs b/src/Windows/Perspex.Win32/Input/WindowsKeyboardDevice.cs index c26bfbd517..10598c4e00 100644 --- a/src/Windows/Perspex.Win32/Input/WindowsKeyboardDevice.cs +++ b/src/Windows/Perspex.Win32/Input/WindowsKeyboardDevice.cs @@ -49,7 +49,7 @@ namespace Perspex.Win32.Input public void WindowActivated(Window window) { - SetFocusedElement(window, NavigationMethod.Unspecified); + SetFocusedElement(window, NavigationMethod.Unspecified, InputModifiers.None); } public string StringFromVirtualKey(uint virtualKey) diff --git a/tests/Perspex.Base.UnitTests/Collections/PerspexListTests.cs b/tests/Perspex.Base.UnitTests/Collections/PerspexListTests.cs index 70509b9f0c..175395fd27 100644 --- a/tests/Perspex.Base.UnitTests/Collections/PerspexListTests.cs +++ b/tests/Perspex.Base.UnitTests/Collections/PerspexListTests.cs @@ -158,12 +158,32 @@ namespace Perspex.Base.UnitTests.Collections Assert.True(raised); } + [Fact] + public void Clearing_Items_Should_Raise_CollectionChanged_Reset() + { + var target = new PerspexList(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] public void Clearing_Items_Should_Raise_CollectionChanged_Remove() { var target = new PerspexList(new[] { 1, 2, 3 }); var raised = false; + target.ResetBehavior = ResetBehavior.Remove; target.CollectionChanged += (s, e) => { Assert.Equal(target, s); diff --git a/tests/Perspex.Controls.UnitTests/ListBoxTests.cs b/tests/Perspex.Controls.UnitTests/ListBoxTests.cs index bea8be733d..4fe5020e2d 100644 --- a/tests/Perspex.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Perspex.Controls.UnitTests/ListBoxTests.cs @@ -1,9 +1,7 @@ // Copyright (c) The Perspex Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; using System.Linq; -using Perspex.Controls; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; 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] public void DataContexts_Should_Be_Correctly_Set() { diff --git a/tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs new file mode 100644 index 0000000000..8dba79c008 --- /dev/null +++ b/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; } + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/Mixins/SelectableMixinTests.cs b/tests/Perspex.Controls.UnitTests/Mixins/SelectableMixinTests.cs index 548992fe38..68654a562c 100644 --- a/tests/Perspex.Controls.UnitTests/Mixins/SelectableMixinTests.cs +++ b/tests/Perspex.Controls.UnitTests/Mixins/SelectableMixinTests.cs @@ -24,7 +24,7 @@ namespace Perspex.Controls.UnitTests.Mixins target.IsSelected = true; - Assert.Equal(new[] { "selected" }, target.Classes); + Assert.Equal(new[] { ":selected" }, target.Classes); } [Fact] diff --git a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 43854919c4..ac644d8aba 100644 --- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -84,6 +84,8 @@ + + diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index f0ee79bd06..421a858950 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -330,76 +330,6 @@ namespace Perspex.Controls.UnitTests.Primitives 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] public void Raising_IsSelectedChanged_On_Item_Should_Update_Selection() { @@ -490,6 +420,31 @@ namespace Perspex.Controls.UnitTests.Primitives 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() { return new ControlTemplate(control => diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index dd34f8eb83..490fadc4ba 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -14,9 +14,8 @@ namespace Perspex.Controls.UnitTests.Primitives [Fact] public void First_Item_Should_Be_Selected() { - var target = new SelectingItemsControl + var target = new TestSelector { - AutoSelect = true, Items = new[] { "foo", "bar" }, Template = Template(), }; @@ -31,9 +30,8 @@ namespace Perspex.Controls.UnitTests.Primitives public void First_Item_Should_Be_Selected_When_Added() { var items = new PerspexList(); - var target = new SelectingItemsControl + var target = new TestSelector { - AutoSelect = true, Items = items, Template = Template(), }; @@ -50,9 +48,8 @@ namespace Perspex.Controls.UnitTests.Primitives { var items = new PerspexList(new[] { "foo", "bar", "baz", "qux" }); - var target = new SelectingItemsControl + var target = new TestSelector { - AutoSelect = true, Items = items, Template = Template(), }; @@ -70,9 +67,8 @@ namespace Perspex.Controls.UnitTests.Primitives { var items = new PerspexList(new[] { "foo", "bar" }); - var target = new SelectingItemsControl + var target = new TestSelector { - AutoSelect = true, Items = items, Template = Template(), }; @@ -97,6 +93,14 @@ namespace Perspex.Controls.UnitTests.Primitives }); } + private class TestSelector : SelectingItemsControl + { + static TestSelector() + { + SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); + } + } + private class Item : Control, ISelectable { public bool IsSelected { get; set; } diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs new file mode 100644 index 0000000000..c61cb3282e --- /dev/null +++ b/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 SelectedIndexes + { + get { return base.SelectedIndexes; } + } + + public new IPerspexList 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(control => + new ItemsPresenter + { + Name = "itemsPresenter", + [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], + [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], + }); + } + } +}