From d41260c6b7a0f88a33ce55d3d778ba634d8d2e55 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 17:15:57 +0200 Subject: [PATCH 01/14] Use same property for SelectedItemProperty On SelectingItemsControl and TreeView. --- src/Perspex.Controls/TreeView.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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() From 7b82df436949e84831922116adbb5197ad4ffb4c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 17:42:24 +0200 Subject: [PATCH 02/14] Replaced AutoSelect with SelectionMode. Added protected multiple selection properties to SelectingItemsControl. --- samples/TestApplication/Program.cs | 2 - src/Perspex.Controls/Deck.cs | 2 +- src/Perspex.Controls/Perspex.Controls.csproj | 1 + .../Primitives/SelectingItemsControl.cs | 65 ++++++++++++------- src/Perspex.Controls/Primitives/TabStrip.cs | 2 +- src/Perspex.Controls/SelectionMode.cs | 34 ++++++++++ src/Perspex.Controls/TabControl.cs | 2 +- .../SelectingItemsControlTests_AutoSelect.cs | 20 +++--- 8 files changed, 91 insertions(+), 37 deletions(-) create mode 100644 src/Perspex.Controls/SelectionMode.cs 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/src/Perspex.Controls/Deck.cs b/src/Perspex.Controls/Deck.cs index dc4ba99301..56151f5829 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.SingleAlways); ItemsPanelProperty.OverrideDefaultValue(PanelTemplate); } 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..66aa312dd3 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; @@ -16,17 +18,8 @@ namespace Perspex.Controls.Primitives /// /// An that maintains a selection. /// - /// - /// TODO: Support multiple selection. - /// public class SelectingItemsControl : ItemsControl { - /// - /// Defines the property. - /// - public static readonly PerspexProperty AutoSelectProperty = - PerspexProperty.Register("AutoSelect"); - /// /// Defines the property. /// @@ -45,6 +38,21 @@ namespace Perspex.Controls.Primitives o => o.SelectedItem, (o, v) => o.SelectedItem = v); + /// + /// 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 @@ -55,6 +63,7 @@ namespace Perspex.Controls.Primitives private int _selectedIndex = -1; private object _selectedItem; + private IList _selectedItems; /// /// Initializes static members of the class. @@ -72,16 +81,7 @@ 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); } + _selectedItems = new PerspexList(); } /// @@ -118,6 +118,23 @@ namespace Perspex.Controls.Primitives } } + /// + /// Gets the selected items. + /// + protected IList SelectedItems + { + get { return _selectedItems; } + } + + /// + /// Gets or sets the selection mode. + /// + protected SelectionMode SelectionMode + { + get { return GetValue(SelectionModeProperty); } + set { SetValue(SelectionModeProperty, value); } + } + /// protected override void ItemsChanged(PerspexPropertyChangedEventArgs e) { @@ -127,7 +144,7 @@ namespace Perspex.Controls.Primitives { SelectedIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem); } - else if (AutoSelect && Items != null & Items.Cast().Any()) + else if (SelectionMode == SelectionMode.SingleAlways && Items != null & Items.Cast().Any()) { SelectedIndex = 0; } @@ -141,7 +158,7 @@ namespace Perspex.Controls.Primitives switch (e.Action) { case NotifyCollectionChangedAction.Add: - if (AutoSelect && SelectedIndex == -1) + if (SelectionMode == SelectionMode.SingleAlways && SelectedIndex == -1) { SelectedIndex = 0; } @@ -155,7 +172,7 @@ namespace Perspex.Controls.Primitives if (selectedIndex >= e.OldStartingIndex && selectedIndex < e.OldStartingIndex + e.OldItems.Count) { - if (!AutoSelect) + if (SelectionMode != SelectionMode.SingleAlways) { SelectedIndex = -1; } @@ -350,13 +367,13 @@ namespace Perspex.Controls.Primitives /// /// Called when the currently selected item is lost and the selection must be changed - /// depending on the property. + /// depending on the property. /// private void LostSelection() { var items = Items?.Cast(); - if (items != null && AutoSelect) + if (items != null && SelectionMode == SelectionMode.SingleAlways) { var index = Math.Min(SelectedIndex, items.Count() - 1); diff --git a/src/Perspex.Controls/Primitives/TabStrip.cs b/src/Perspex.Controls/Primitives/TabStrip.cs index 5e1038517c..59139d7e2b 100644 --- a/src/Perspex.Controls/Primitives/TabStrip.cs +++ b/src/Perspex.Controls/Primitives/TabStrip.cs @@ -15,7 +15,7 @@ namespace Perspex.Controls.Primitives static TabStrip() { - AutoSelectProperty.OverrideDefaultValue(true); + SelectionModeProperty.OverrideDefaultValue(SelectionMode.SingleAlways); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); } diff --git a/src/Perspex.Controls/SelectionMode.cs b/src/Perspex.Controls/SelectionMode.cs new file mode 100644 index 0000000000..9895ec195f --- /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. + +namespace Perspex.Controls +{ + /// + /// Defines the selection mode for a control which can select multiple items. + /// + public enum SelectionMode + { + /// + /// One item can be selected at a time. + /// + Single, + + /// + /// One item can be selected at a time, and there will always be a selected item as long + /// as there are items to select. + /// + SingleAlways, + + /// + /// Multiple items can be selected and their selection state is toggled by presses or by + /// pressing the spacebar. + /// + MultipleToggle, + + /// + /// A range of items can be selected by holding the shift key, and individual items can be + /// selected by holding the ctrl key. + /// + MultipleRange, + } +} diff --git a/src/Perspex.Controls/TabControl.cs b/src/Perspex.Controls/TabControl.cs index 632137824d..c4ddd6935d 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.SingleAlways); FocusableProperty.OverrideDefaultValue(false); SelectedIndexProperty.Changed.AddClassHandler(x => x.SelectedIndexChanged); } diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index dd34f8eb83..bae19a78ee 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.SingleAlways); + } + } + private class Item : Control, ISelectable { public bool IsSelected { get; set; } From e2f7ff8a56f927dabdad480c8cf926036c167197 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 18:06:19 +0200 Subject: [PATCH 03/14] Implement SelectedItem in terms of SelectedItems. --- .../Primitives/SelectingItemsControl.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 66aa312dd3..859ecadad3 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -62,8 +62,7 @@ namespace Perspex.Controls.Primitives RoutedEvent.Register("IsSelectedChanged", RoutingStrategies.Bubble); private int _selectedIndex = -1; - private object _selectedItem; - private IList _selectedItems; + private IList _selectedItems = new PerspexList(); /// /// Initializes static members of the class. @@ -81,7 +80,6 @@ namespace Perspex.Controls.Primitives public SelectingItemsControl() { ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized); - _selectedItems = new PerspexList(); } /// @@ -108,13 +106,25 @@ 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); + } } } From e93d9d463ca9e6b2efd0cde39a49e0cbadebb1ba Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 18:12:23 +0200 Subject: [PATCH 04/14] Implement SelectedIndex in terms of SelectedIndexes. --- .../Primitives/SelectingItemsControl.cs | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 859ecadad3..b1cb52b305 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -38,6 +38,14 @@ 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. /// @@ -61,8 +69,8 @@ namespace Perspex.Controls.Primitives public static readonly RoutedEvent IsSelectedChangedEvent = RoutedEvent.Register("IsSelectedChanged", RoutingStrategies.Bubble); - private int _selectedIndex = -1; - private IList _selectedItems = new PerspexList(); + private PerspexList _selectedIndexes = new PerspexList(); + private PerspexList _selectedItems = new PerspexList(); /// /// Initializes static members of the class. @@ -89,13 +97,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); + } } } @@ -128,6 +148,14 @@ namespace Perspex.Controls.Primitives } } + /// + /// Gets the selected indexes. + /// + protected IList SelectedIndexes + { + get { return _selectedIndexes; } + } + /// /// Gets the selected items. /// From 271f47fb5b16752bf2183dd8b94e02aa5d9538b2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 18:27:03 +0200 Subject: [PATCH 05/14] Make SelectionMode flags. So you can combine behaviours. --- src/Perspex.Controls/Deck.cs | 2 +- .../Primitives/SelectingItemsControl.cs | 13 +++++++---- src/Perspex.Controls/Primitives/TabStrip.cs | 2 +- src/Perspex.Controls/SelectionMode.cs | 22 +++++++++---------- src/Perspex.Controls/TabControl.cs | 2 +- .../SelectingItemsControlTests_AutoSelect.cs | 2 +- 6 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/Perspex.Controls/Deck.cs b/src/Perspex.Controls/Deck.cs index 56151f5829..1cdfdad239 100644 --- a/src/Perspex.Controls/Deck.cs +++ b/src/Perspex.Controls/Deck.cs @@ -31,7 +31,7 @@ namespace Perspex.Controls /// static Deck() { - SelectionModeProperty.OverrideDefaultValue(SelectionMode.SingleAlways); + SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(PanelTemplate); } diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index b1cb52b305..243d1e29e4 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -173,6 +173,11 @@ namespace Perspex.Controls.Primitives set { SetValue(SelectionModeProperty, value); } } + /// + /// Gets a value indicating whether is set. + /// + protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0; + /// protected override void ItemsChanged(PerspexPropertyChangedEventArgs e) { @@ -182,7 +187,7 @@ namespace Perspex.Controls.Primitives { SelectedIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem); } - else if (SelectionMode == SelectionMode.SingleAlways && Items != null & Items.Cast().Any()) + else if (AlwaysSelected && Items != null & Items.Cast().Any()) { SelectedIndex = 0; } @@ -196,7 +201,7 @@ namespace Perspex.Controls.Primitives switch (e.Action) { case NotifyCollectionChangedAction.Add: - if (SelectionMode == SelectionMode.SingleAlways && SelectedIndex == -1) + if (AlwaysSelected && SelectedIndex == -1) { SelectedIndex = 0; } @@ -210,7 +215,7 @@ namespace Perspex.Controls.Primitives if (selectedIndex >= e.OldStartingIndex && selectedIndex < e.OldStartingIndex + e.OldItems.Count) { - if (SelectionMode != SelectionMode.SingleAlways) + if (!AlwaysSelected) { SelectedIndex = -1; } @@ -411,7 +416,7 @@ namespace Perspex.Controls.Primitives { var items = Items?.Cast(); - if (items != null && SelectionMode == SelectionMode.SingleAlways) + if (items != null && AlwaysSelected) { var index = Math.Min(SelectedIndex, items.Count() - 1); diff --git a/src/Perspex.Controls/Primitives/TabStrip.cs b/src/Perspex.Controls/Primitives/TabStrip.cs index 59139d7e2b..648103ed51 100644 --- a/src/Perspex.Controls/Primitives/TabStrip.cs +++ b/src/Perspex.Controls/Primitives/TabStrip.cs @@ -15,7 +15,7 @@ namespace Perspex.Controls.Primitives static TabStrip() { - SelectionModeProperty.OverrideDefaultValue(SelectionMode.SingleAlways); + SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); } diff --git a/src/Perspex.Controls/SelectionMode.cs b/src/Perspex.Controls/SelectionMode.cs index 9895ec195f..bbf3a55259 100644 --- a/src/Perspex.Controls/SelectionMode.cs +++ b/src/Perspex.Controls/SelectionMode.cs @@ -1,34 +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 at a time. + /// One item can be selected. /// - Single, + Single = 0x00, /// - /// One item can be selected at a time, and there will always be a selected item as long - /// as there are items to select. + /// Multiple items can be selected. /// - SingleAlways, + Multiple = 0x01, /// - /// Multiple items can be selected and their selection state is toggled by presses or by - /// pressing the spacebar. + /// Item selection can be toggled by tapping/spacebar. /// - MultipleToggle, + Toggle = 0x02, /// - /// A range of items can be selected by holding the shift key, and individual items can be - /// selected by holding the ctrl key. + /// An item will always be selected as long as there are items to select. /// - MultipleRange, + AlwaysSelected = 0x04, } } diff --git a/src/Perspex.Controls/TabControl.cs b/src/Perspex.Controls/TabControl.cs index c4ddd6935d..ee87741980 100644 --- a/src/Perspex.Controls/TabControl.cs +++ b/src/Perspex.Controls/TabControl.cs @@ -33,7 +33,7 @@ namespace Perspex.Controls /// static TabControl() { - SelectionModeProperty.OverrideDefaultValue(SelectionMode.SingleAlways); + SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(false); SelectedIndexProperty.Changed.AddClassHandler(x => x.SelectedIndexChanged); } diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index bae19a78ee..490fadc4ba 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -97,7 +97,7 @@ namespace Perspex.Controls.UnitTests.Primitives { static TestSelector() { - SelectionModeProperty.OverrideDefaultValue(SelectionMode.SingleAlways); + SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); } } From 7a287782fb1cf1a4e3973a14d7b937744117b99d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 4 Oct 2015 11:18:43 +0200 Subject: [PATCH 06/14] Renamed "selected" style class to ":selected" --- samples/TestApplication/GalleryStyle.cs | 2 +- src/Perspex.Controls/Mixins/SelectableMixin.cs | 4 ++-- src/Perspex.Controls/Primitives/SelectingItemsControl.cs | 4 ++-- src/Perspex.Themes.Default/ListBoxItemStyle.cs | 4 ++-- src/Perspex.Themes.Default/MenuItemStyle.cs | 2 +- src/Perspex.Themes.Default/TabItemStyle.cs | 2 +- src/Perspex.Themes.Default/TreeViewItemStyle.cs | 4 ++-- .../Perspex.Controls.UnitTests/Mixins/SelectableMixinTests.cs | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) 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/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/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 243d1e29e4..0eeee2dd90 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -305,11 +305,11 @@ namespace Perspex.Controls.Primitives { if (selected) { - styleable.Classes.Add("selected"); + styleable.Classes.Add(":selected"); } else { - styleable.Classes.Remove("selected"); + styleable.Classes.Remove(":selected"); } } } 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/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] From 21a52a46594cdcadec7775e8a812dc32dff497f1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 8 Oct 2015 17:57:07 +0200 Subject: [PATCH 07/14] Fix IPerspexList.Count. --- src/Perspex.Base/Collections/IPerspexList.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Perspex.Base/Collections/IPerspexList.cs b/src/Perspex.Base/Collections/IPerspexList.cs index 90278a6044..c680affdc9 100644 --- a/src/Perspex.Base/Collections/IPerspexList.cs +++ b/src/Perspex.Base/Collections/IPerspexList.cs @@ -11,6 +11,11 @@ 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; } + /// /// Adds multiple items to the collection. /// From d87bcde7be76ca1ab8d91258dfc7744ef1dbeced Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 8 Oct 2015 19:30:56 +0200 Subject: [PATCH 08/14] Added ResetBehavior to PerspexList. As sometimes you want to know something is a reset rather than a remove. --- src/Perspex.Animation/PropertyTransitions.cs | 7 ++ src/Perspex.Base/Collections/PerspexList.cs | 96 ++++++++++++++++++- src/Perspex.Controls/ColumnDefinitions.cs | 2 + src/Perspex.Controls/Control.cs | 4 +- src/Perspex.Controls/Controls.cs | 2 + src/Perspex.Controls/RowDefinitions.cs | 2 + .../Templates/DataTemplates.cs | 7 ++ src/Perspex.SceneGraph/Visual.cs | 1 + .../Collections/PerspexListTests.cs | 20 ++++ 9 files changed, 136 insertions(+), 5 deletions(-) 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/PerspexList.cs b/src/Perspex.Base/Collections/PerspexList.cs index 78c2de0b8d..2e8746b071 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,6 +187,14 @@ namespace Perspex.Collections { Contract.Requires(items != null); + if (Validate != null) + { + foreach (var item in items) + { + Validate(item); + } + } + int index = _inner.Count; _inner.AddRange(items); NotifyAdd((items as IList) ?? items.ToList(), index); @@ -153,7 +207,7 @@ namespace Perspex.Collections { var old = _inner; _inner = new List(); - NotifyRemove(old, 0); + NotifyReset(old); } /// @@ -204,6 +258,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,6 +272,14 @@ namespace Perspex.Collections { Contract.Requires(items != null); + if (Validate != null) + { + foreach (var item in items) + { + Validate(item); + } + } + _inner.InsertRange(index, items); NotifyAdd((items as IList) ?? items.ToList(), index); } @@ -367,5 +430,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.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/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/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.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/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); From b2d40e77c3460cafda858d7583215c1a3c68ef71 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 8 Oct 2015 20:31:37 +0200 Subject: [PATCH 09/14] Started implementing multiple selection. --- .../Views/MainWindow.paml | 2 +- .../Collections/PerspexListExtensions.cs | 82 ++++- .../Generators/IItemContainerGenerator.cs | 5 + .../Generators/ItemContainerGenerator.cs | 5 + .../Generators/TreeItemContainerGenerator.cs | 5 + src/Perspex.Controls/ListBox.cs | 36 ++ .../Primitives/SelectingItemsControl.cs | 332 +++++++++++++----- src/Perspex.Controls/Primitives/TabStrip.cs | 23 ++ .../ListBoxTests.cs | 19 - .../ListBoxTests_Single.cs | 188 ++++++++++ .../Perspex.Controls.UnitTests.csproj | 2 + .../Primitives/SelectingItemsControlTests.cs | 95 ++--- .../SelectingItemsControlTests_Multiple.cs | 246 +++++++++++++ 13 files changed, 842 insertions(+), 198 deletions(-) create mode 100644 tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs create mode 100644 tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index 3b6433d5b4..4774398f4f 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -49,7 +49,7 @@ - + diff --git a/src/Perspex.Base/Collections/PerspexListExtensions.cs b/src/Perspex.Base/Collections/PerspexListExtensions.cs index 27973ecabe..d1af24f31d 100644 --- a/src/Perspex.Base/Collections/PerspexListExtensions.cs +++ b/src/Perspex.Base/Collections/PerspexListExtensions.cs @@ -22,55 +22,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; @@ -116,7 +171,8 @@ namespace Perspex.Collections inpc.PropertyChanged -= handler; tracked.Remove(inpc); } - }); + }, + null); return Disposable.Create(() => { 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..fb523d6ed7 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,45 @@ 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); + } + } + + /// + protected override void OnPointerPressed(PointerPressEventArgs e) + { + base.OnPointerPressed(e); + + if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) + { + UpdateSelectionFromEventSource(e.Source, true); + } + } } } diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 0eeee2dd90..ad95c38206 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -3,7 +3,6 @@ using System; using System.Collections; -using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using Perspex.Collections; @@ -18,6 +17,22 @@ namespace Perspex.Controls.Primitives /// /// An that maintains a selection. /// + /// + /// + /// provides a base class for s + /// that maintain a selection (single or multiple). By default only its + /// and properties are visible; the + /// 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 { /// @@ -41,16 +56,16 @@ namespace Perspex.Controls.Primitives /// /// Defines the property. /// - protected static readonly PerspexProperty> SelectedIndexesProperty = - PerspexProperty.RegisterDirect>( + protected static readonly PerspexProperty> SelectedIndexesProperty = + PerspexProperty.RegisterDirect>( nameof(SelectedIndexes), o => o.SelectedIndexes); /// /// Defines the property. /// - protected static readonly PerspexProperty> SelectedItemsProperty = - PerspexProperty.RegisterDirect>( + protected static readonly PerspexProperty> SelectedItemsProperty = + PerspexProperty.RegisterDirect>( nameof(SelectedItems), o => o.SelectedItems); @@ -71,6 +86,7 @@ namespace Perspex.Controls.Primitives private PerspexList _selectedIndexes = new PerspexList(); private PerspexList _selectedItems = new PerspexList(); + private bool _ignoreContainerSelectionChanged; /// /// Initializes static members of the class. @@ -78,8 +94,6 @@ namespace Perspex.Controls.Primitives static SelectingItemsControl() { IsSelectedChangedEvent.AddClassHandler(x => x.ContainerSelectionChanged); - SelectedIndexProperty.Changed.AddClassHandler(x => x.SelectedIndexChanged); - SelectedItemProperty.Changed.AddClassHandler(x => x.SelectedItemChanged); } /// @@ -88,6 +102,9 @@ namespace Perspex.Controls.Primitives public SelectingItemsControl() { ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized); + _selectedIndexes.Validate = ValidateIndex; + _selectedIndexes.ForEachItem(SelectedIndexAdded, SelectedIndexRemoved, SelectionReset); + _selectedItems.ForEachItem(SelectedItemAdded, SelectedItemRemoved, SelectionReset); } /// @@ -151,7 +168,7 @@ namespace Perspex.Controls.Primitives /// /// Gets the selected indexes. /// - protected IList SelectedIndexes + protected IPerspexList SelectedIndexes { get { return _selectedIndexes; } } @@ -159,7 +176,7 @@ namespace Perspex.Controls.Primitives /// /// Gets the selected items. /// - protected IList SelectedItems + protected IPerspexList SelectedItems { get { return _selectedItems; } } @@ -178,6 +195,20 @@ namespace Perspex.Controls.Primitives /// 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) { @@ -233,23 +264,80 @@ namespace Perspex.Controls.Primitives } } - /// - protected override void OnGotFocus(GotFocusEventArgs e) + /// + /// Updates the selection for an item. + /// + /// The index of the item. + /// Whether the item should be selected or unselected. + protected void UpdateSelection(int index, bool select) + { + if (index != -1) + { + if (select) + { + var toggle = (SelectionMode & SelectionMode.Toggle) != 0; + + if (!toggle) + { + SelectedIndex = index; + } + else + { + var i = SelectedIndexes.IndexOf(index); + + if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1)) + { + SelectedIndexes.RemoveAt(i); + } + else + { + SelectedIndexes.Add(index); + } + } + } + else + { + LostSelection(); + } + } + } + + /// + /// Updates the selection for a container. + /// + /// The container. + /// Whether the container should be selected or unselected. + protected void UpdateSelection(IControl container, bool select) { - base.OnGotFocus(e); + var index = ItemContainerGenerator.IndexFromContainer(container); - if (e.NavigationMethod == NavigationMethod.Pointer || - e.NavigationMethod == NavigationMethod.Directional) + if (index != -1) { - TrySetSelectionFromContainerEvent(e.Source, true); + UpdateSelection(index, select); } } - /// - 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. + /// + /// True if the event originated from a container that belongs to the control; otherwise + /// false. + /// + protected bool UpdateSelectionFromEventSource(IInteractive eventSource, bool select) { - base.OnPointerPressed(e); - e.Handled = true; + var item = GetContainerFromEventSource(eventSource); + + if (item != null) + { + UpdateSelection(item, select); + return true; + } + + return false; } /// @@ -292,26 +380,35 @@ namespace Perspex.Controls.Primitives /// /// The container. /// Whether the control is selected - private static void MarkContainerSelected(IControl container, bool selected) + private void MarkContainerSelected(IControl container, bool selected) { - var selectable = container as ISelectable; - var styleable = container as IStyleable; - - if (selectable != null) - { - selectable.IsSelected = selected; - } - else if (styleable != null) + try { - if (selected) + var selectable = container as ISelectable; + var styleable = container as IStyleable; + + _ignoreContainerSelectionChanged = true; + + if (selectable != null) { - styleable.Classes.Add(":selected"); + selectable.IsSelected = selected; } - else + else if (styleable != null) { - styleable.Classes.Remove(":selected"); + if (selected) + { + styleable.Classes.Add(":selected"); + } + else + { + styleable.Classes.Remove(":selected"); + } } } + finally + { + _ignoreContainerSelectionChanged = false; + } } /// @@ -341,71 +438,143 @@ namespace Perspex.Controls.Primitives /// The event. private void ContainerSelectionChanged(RoutedEventArgs e) { - var selectable = (ISelectable)e.Source; + if (!_ignoreContainerSelectionChanged) + { + var selectable = (ISelectable)e.Source; - if (selectable != null) + if (selectable != null) + { + UpdateSelectionFromEventSource(e.Source, selectable.IsSelected); + } + } + } + + /// + /// Sets an item container's 'selected' class or . + /// + /// The index of the item. + /// Whether the control is selected + /// The container. + private IControl MarkIndexSelected(int index, bool selected) + { + var container = ItemContainerGenerator.ContainerFromIndex(index); + + if (container != null) { - TrySetSelectionFromContainerEvent(e.Source, selectable.IsSelected); + MarkContainerSelected(container, selected); } + + return container; } /// - /// Called when the property changes. + /// Called when an index is added to the collection. /// - /// The event args. - private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e) + /// The index in the SelectedIndexes collection. + /// The item index. + private void SelectedIndexAdded(int listIndex, int itemIndex) { - var index = (int)e.OldValue; + if (SelectedIndexes.Count == 1) + { + RaisePropertyChanged(SelectedIndexProperty, -1, itemIndex, BindingPriority.LocalValue); + } - if (index != -1) + if (SelectedItems.Count != SelectedIndexes.Count) { - var container = ItemContainerGenerator.ContainerFromIndex(index); - MarkContainerSelected(container, false); + var item = Items.Cast().ElementAt(itemIndex); + SelectedItems.Insert(listIndex, item); } - index = (int)e.NewValue; + var container = MarkIndexSelected(itemIndex, true); - if (index == -1) + if (container != null && Presenter?.Panel != null) { - SelectedItem = null; + KeyboardNavigation.SetTabOnceActiveElement((InputElement)Presenter.Panel, container); } - else + } + + /// + /// Called when an index is removed from the collection. + /// + /// The index in the SelectedIndexes collection. + /// The item index. + private void SelectedIndexRemoved(int listIndex, int itemIndex) + { + if (SelectedIndexes.Count == 0) { - SelectedItem = Items.Cast().ElementAt((int)e.NewValue); - var container = ItemContainerGenerator.ContainerFromIndex(index); - MarkContainerSelected(container, true); + RaisePropertyChanged(SelectedIndexProperty, itemIndex, -1, BindingPriority.LocalValue); + } - var inputElement = container as IInputElement; - if (inputElement != null && Presenter != null && Presenter.Panel != null) - { - KeyboardNavigation.SetTabOnceActiveElement( - (InputElement)Presenter.Panel, - inputElement); - } + if (SelectedIndexes.Count != SelectedItems.Count) + { + SelectedItems.RemoveAt(listIndex); } + + MarkIndexSelected(itemIndex, false); } /// - /// Called when the property changes. + /// Called when an item is added to the collection. /// - /// The event args. - private void SelectedItemChanged(PerspexPropertyChangedEventArgs e) + /// The index in the SelectedItems collection. + /// The item. + private void SelectedItemAdded(int index, object item) { - SelectedIndex = IndexOf(Items, e.NewValue); + if (SelectedItems.Count == 1) + { + RaisePropertyChanged(SelectedItemProperty, null, item, BindingPriority.LocalValue); + } + + if (SelectedIndexes.Count != SelectedItems.Count) + { + SelectedIndexes.Insert(index, IndexOf(Items, item)); + } } /// - /// Tries to get the container that was the source of an event. + /// Called when an item is removed from 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 SelectedItemRemoved(int index, object item) { - var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.LogicalParent == this); + if (SelectedIndexes.Count != SelectedItems.Count) + { + SelectedIndexes.RemoveAt(index); + } + } - return item as IControl; + /// + /// Called when the collection is reset. + /// + private void SelectionReset() + { + if (SelectedIndexes.Count > 0) + { + SelectedIndexes.Clear(); + } + + if (SelectedItems.Count > 0) + { + SelectedItems.Clear(); + } + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkContainerSelected(container, false); + } + } + + /// + /// Validates items added to the collection. + /// + /// The index to be added. + private void ValidateIndex(int index) + { + if (index < 0 || index >= Items?.Cast().Count()) + { + throw new IndexOutOfRangeException(); + } } /// @@ -429,32 +598,5 @@ namespace Perspex.Controls.Primitives SelectedIndex = -1; } - - /// - /// Tries to set the selection to a container that raised an event. - /// - /// The control that raised the event. - /// Whether the container should be selected or unselected. - private void TrySetSelectionFromContainerEvent(IInteractive eventSource, bool select) - { - var item = GetContainerFromEvent(eventSource); - - if (item != null) - { - var index = ItemContainerGenerator.IndexFromContainer(item); - - if (index != -1) - { - if (select) - { - SelectedIndex = index; - } - else - { - LostSelection(); - } - } - } - } } } diff --git a/src/Perspex.Controls/Primitives/TabStrip.cs b/src/Perspex.Controls/Primitives/TabStrip.cs index 648103ed51..2f2f1b44b5 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 { @@ -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, true); + } + } + + /// + protected override void OnPointerPressed(PointerPressEventArgs e) + { + base.OnPointerPressed(e); + + if (e.MouseButton == MouseButton.Left) + { + UpdateSelectionFromEventSource(e.Source, true); + } + } } } 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..968c2a7b03 --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs @@ -0,0 +1,188 @@ +// 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.Single | 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 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/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_Multiple.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs new file mode 100644 index 0000000000..38af6d4dd8 --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -0,0 +1,246 @@ +// 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); + } + + private class TestSelector : SelectingItemsControl + { + public new IPerspexList SelectedIndexes + { + get { return base.SelectedIndexes; } + } + + public new IPerspexList SelectedItems + { + get { return base.SelectedItems; } + } + } + + private ControlTemplate Template() + { + return new ControlTemplate(control => + new ItemsPresenter + { + Name = "itemsPresenter", + [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], + [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], + }); + } + } +} From 49e2e3ec869958daf9acac2ab1f03ca557b4b461 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 8 Oct 2015 20:58:06 +0200 Subject: [PATCH 10/14] Fixed problem in toggle mode with single selection. --- .../Primitives/SelectingItemsControl.cs | 17 +++++-- .../ListBoxTests_Single.cs | 46 ++++++++++++++++++- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index ad95c38206..9abd72635d 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -265,7 +265,7 @@ namespace Perspex.Controls.Primitives } /// - /// Updates the selection for an item. + /// Updates the selection for an item based on user interaction. /// /// The index of the item. /// Whether the item should be selected or unselected. @@ -275,7 +275,9 @@ namespace Perspex.Controls.Primitives { if (select) { - var toggle = (SelectionMode & SelectionMode.Toggle) != 0; + var mode = SelectionMode; + var toggle = (mode & SelectionMode.Toggle) != 0; + var multi = (mode & SelectionMode.Multiple) != 0; if (!toggle) { @@ -291,7 +293,14 @@ namespace Perspex.Controls.Primitives } else { - SelectedIndexes.Add(index); + if (multi) + { + SelectedIndexes.Add(index); + } + else + { + SelectedIndex = index; + } } } } @@ -303,7 +312,7 @@ namespace Perspex.Controls.Primitives } /// - /// Updates the selection for a container. + /// Updates the selection for a container based on user interaction. /// /// The container. /// Whether the container should be selected or unselected. diff --git a/tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs index 968c2a7b03..8dba79c008 100644 --- a/tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Perspex.Controls.UnitTests/ListBoxTests_Single.cs @@ -122,7 +122,7 @@ namespace Perspex.Controls.UnitTests { Template = new ControlTemplate(CreateListBoxTemplate), Items = new[] { "Foo", "Bar", "Baz " }, - SelectionMode = SelectionMode.Single | SelectionMode.Toggle, + SelectionMode = SelectionMode.Toggle, }; target.ApplyTemplate(); @@ -137,6 +137,50 @@ namespace Perspex.Controls.UnitTests 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() { From 7d73960ef676d2c2942d65e288fd12d97e7bd46b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 8 Oct 2015 23:29:18 +0200 Subject: [PATCH 11/14] Make selecting ranges work. --- .../Views/MainWindow.paml | 2 +- src/Perspex.Base/Collections/IPerspexList.cs | 8 ++ src/Perspex.Base/Collections/PerspexList.cs | 47 ++++--- .../Collections/PerspexListExtensions.cs | 61 +++++++++ src/Perspex.Controls/ListBox.cs | 8 +- .../Primitives/SelectingItemsControl.cs | 117 +++++++++++++----- src/Perspex.Controls/Primitives/TabStrip.cs | 4 +- .../SelectingItemsControlTests_Multiple.cs | 63 ++++++++++ 8 files changed, 257 insertions(+), 53 deletions(-) diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index 4774398f4f..3dfac47f36 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -49,7 +49,7 @@ - + diff --git a/src/Perspex.Base/Collections/IPerspexList.cs b/src/Perspex.Base/Collections/IPerspexList.cs index c680affdc9..804ae73e07 100644 --- a/src/Perspex.Base/Collections/IPerspexList.cs +++ b/src/Perspex.Base/Collections/IPerspexList.cs @@ -16,6 +16,14 @@ namespace Perspex.Collections /// 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. /// diff --git a/src/Perspex.Base/Collections/PerspexList.cs b/src/Perspex.Base/Collections/PerspexList.cs index 2e8746b071..c0a35feac2 100644 --- a/src/Perspex.Base/Collections/PerspexList.cs +++ b/src/Perspex.Base/Collections/PerspexList.cs @@ -187,17 +187,22 @@ namespace Perspex.Collections { Contract.Requires(items != null); - if (Validate != null) + var list = (items as IList) ?? items.ToList(); + + if (list.Count > 0) { - foreach (var item in items) + if (Validate != null) { - Validate(item); + foreach (var item in list) + { + Validate((T)item); + } } - } - int index = _inner.Count; - _inner.AddRange(items); - NotifyAdd((items as IList) ?? items.ToList(), index); + int index = _inner.Count; + _inner.AddRange(items); + NotifyAdd(list, index); + } } /// @@ -205,9 +210,12 @@ namespace Perspex.Collections /// public void Clear() { - var old = _inner; - _inner = new List(); - NotifyReset(old); + if (this.Count > 0) + { + var old = _inner; + _inner = new List(); + NotifyReset(old); + } } /// @@ -272,16 +280,21 @@ namespace Perspex.Collections { Contract.Requires(items != null); - if (Validate != null) + var list = (items as IList) ?? items.ToList(); + + if (list.Count > 0) { - foreach (var item in items) + if (Validate != null) { - Validate(item); + foreach (var item in list) + { + Validate((T)item); + } } - } - _inner.InsertRange(index, items); - NotifyAdd((items as IList) ?? items.ToList(), index); + _inner.InsertRange(index, items); + NotifyAdd((items as IList) ?? items.ToList(), index); + } } /// @@ -311,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) { diff --git a/src/Perspex.Base/Collections/PerspexListExtensions.cs b/src/Perspex.Base/Collections/PerspexListExtensions.cs index d1af24f31d..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 @@ -133,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. /// diff --git a/src/Perspex.Controls/ListBox.cs b/src/Perspex.Controls/ListBox.cs index fb523d6ed7..044de39828 100644 --- a/src/Perspex.Controls/ListBox.cs +++ b/src/Perspex.Controls/ListBox.cs @@ -38,7 +38,7 @@ namespace Perspex.Controls if (e.NavigationMethod == NavigationMethod.Directional) { - UpdateSelectionFromEventSource(e.Source, true); + UpdateSelectionFromEventSource(e.Source); } } @@ -49,7 +49,11 @@ namespace Perspex.Controls if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) { - UpdateSelectionFromEventSource(e.Source, true); + UpdateSelectionFromEventSource( + e.Source, + true, + (e.InputModifiers & InputModifiers.Shift) != 0, + (e.InputModifiers & InputModifiers.Control) != 0); } } } diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 9abd72635d..3ffdfb780b 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -3,6 +3,7 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using Perspex.Collections; @@ -103,8 +104,8 @@ namespace Perspex.Controls.Primitives { ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized); _selectedIndexes.Validate = ValidateIndex; - _selectedIndexes.ForEachItem(SelectedIndexAdded, SelectedIndexRemoved, SelectionReset); - _selectedItems.ForEachItem(SelectedItemAdded, SelectedItemRemoved, SelectionReset); + _selectedIndexes.ForEachItem(SelectedIndexesAdded, SelectedIndexesRemoved, SelectionReset); + _selectedItems.ForEachItem(SelectedItemsAdded, SelectedItemsRemoved, SelectionReset); } /// @@ -269,20 +270,35 @@ namespace Perspex.Controls.Primitives /// /// The index of the item. /// Whether the item should be selected or unselected. - protected void UpdateSelection(int index, bool select) + /// Whether the range modifier is enabled (i.e. shift key). + /// Whether the toggle modifier is enabled (i.e. ctrl key). + protected void UpdateSelection( + int index, + bool select = true, + bool rangeModifier = false, + bool toggleModifier = false) { if (index != -1) { if (select) { var mode = SelectionMode; - var toggle = (mode & SelectionMode.Toggle) != 0; + var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0; var multi = (mode & SelectionMode.Multiple) != 0; + var range = multi && SelectedIndexes.Count > 0 ? rangeModifier : false; - if (!toggle) + if (!toggle && !range) { SelectedIndex = index; } + else if (multi && range) + { + var first = SelectedIndexes[0]; + + // TODO: Don't deselect items in new selection. + SelectedIndexes.Clear(); + SelectedIndexes.AddRange(Range(first, index)); + } else { var i = SelectedIndexes.IndexOf(index); @@ -311,18 +327,36 @@ namespace Perspex.Controls.Primitives } } + private IEnumerable Range(int first, int last) + { + int step = first > last ? -1 : 1; + + for (int i = first; i != last; i += step) + { + yield return i; + } + + yield return last; + } + /// /// Updates the selection for a container based on user interaction. /// /// The container. /// Whether the container should be selected or unselected. - protected void UpdateSelection(IControl container, bool select) + /// Whether the range modifier is enabled (i.e. shift key). + /// Whether the toggle modifier is enabled (i.e. ctrl key). + protected void UpdateSelection( + IControl container, + bool select = true, + bool rangeModifier = false, + bool toggleModifier = false) { var index = ItemContainerGenerator.IndexFromContainer(container); if (index != -1) { - UpdateSelection(index, select); + UpdateSelection(index, select, rangeModifier, toggleModifier); } } @@ -332,17 +366,23 @@ namespace Perspex.Controls.Primitives /// /// 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) + protected bool UpdateSelectionFromEventSource( + IInteractive eventSource, + bool select = true, + bool rangeModifier = false, + bool toggleModifier = false) { var item = GetContainerFromEventSource(eventSource); if (item != null) { - UpdateSelection(item, select); + UpdateSelection(item, select, rangeModifier, toggleModifier); return true; } @@ -480,21 +520,27 @@ namespace Perspex.Controls.Primitives /// Called when an index is added to the collection. /// /// The index in the SelectedIndexes collection. - /// The item index. - private void SelectedIndexAdded(int listIndex, int itemIndex) + /// The item indexes. + private void SelectedIndexesAdded(int listIndex, IEnumerable itemIndexes) { - if (SelectedIndexes.Count == 1) + var indexes = (itemIndexes as IList) ?? itemIndexes.ToList(); + IControl container = null; + + if (SelectedItems.Count != SelectedIndexes.Count) { - RaisePropertyChanged(SelectedIndexProperty, -1, itemIndex, BindingPriority.LocalValue); + var items = indexes.Select(x => Items.Cast().ElementAt(x)); + SelectedItems.AddRange(items); } - if (SelectedItems.Count != SelectedIndexes.Count) + foreach (var itemIndex in indexes) { - var item = Items.Cast().ElementAt(itemIndex); - SelectedItems.Insert(listIndex, item); + container = MarkIndexSelected(itemIndex, true); } - var container = MarkIndexSelected(itemIndex, true); + if (SelectedIndexes.Count == 1) + { + RaisePropertyChanged(SelectedIndexProperty, -1, SelectedIndexes[0], BindingPriority.LocalValue); + } if (container != null && Presenter?.Panel != null) { @@ -506,20 +552,29 @@ namespace Perspex.Controls.Primitives /// Called when an index is removed from the collection. /// /// The index in the SelectedIndexes collection. - /// The item index. - private void SelectedIndexRemoved(int listIndex, int itemIndex) + /// The item indexes. + private void SelectedIndexesRemoved(int listIndex, IEnumerable itemIndexes) { - if (SelectedIndexes.Count == 0) + var sync = SelectedIndexes.Count != SelectedItems.Count; + + foreach (var itemIndex in itemIndexes) { - RaisePropertyChanged(SelectedIndexProperty, itemIndex, -1, BindingPriority.LocalValue); + if (sync) + { + SelectedItems.RemoveAt(listIndex++); + } + + MarkIndexSelected(itemIndex, false); } - if (SelectedIndexes.Count != SelectedItems.Count) + if (SelectedIndexes.Count == 0) { - SelectedItems.RemoveAt(listIndex); + RaisePropertyChanged( + SelectedIndexProperty, + itemIndexes.First(), + -1, + BindingPriority.LocalValue); } - - MarkIndexSelected(itemIndex, false); } /// @@ -527,16 +582,16 @@ namespace Perspex.Controls.Primitives /// /// The index in the SelectedItems collection. /// The item. - private void SelectedItemAdded(int index, object item) + private void SelectedItemsAdded(int index, object item) { - if (SelectedItems.Count == 1) + if (SelectedIndexes.Count != SelectedItems.Count) { - RaisePropertyChanged(SelectedItemProperty, null, item, BindingPriority.LocalValue); + SelectedIndexes.Insert(index, IndexOf(Items, item)); } - if (SelectedIndexes.Count != SelectedItems.Count) + if (SelectedItems.Count == 1) { - SelectedIndexes.Insert(index, IndexOf(Items, item)); + RaisePropertyChanged(SelectedItemProperty, null, item, BindingPriority.LocalValue); } } @@ -545,7 +600,7 @@ namespace Perspex.Controls.Primitives /// /// The index in the SelectedItems collection. /// The item. - private void SelectedItemRemoved(int index, object item) + private void SelectedItemsRemoved(int index, object item) { if (SelectedIndexes.Count != SelectedItems.Count) { diff --git a/src/Perspex.Controls/Primitives/TabStrip.cs b/src/Perspex.Controls/Primitives/TabStrip.cs index 2f2f1b44b5..8231a5f121 100644 --- a/src/Perspex.Controls/Primitives/TabStrip.cs +++ b/src/Perspex.Controls/Primitives/TabStrip.cs @@ -56,7 +56,7 @@ namespace Perspex.Controls.Primitives if (e.NavigationMethod == NavigationMethod.Directional) { - UpdateSelectionFromEventSource(e.Source, true); + UpdateSelectionFromEventSource(e.Source); } } @@ -67,7 +67,7 @@ namespace Perspex.Controls.Primitives if (e.MouseButton == MouseButton.Left) { - UpdateSelectionFromEventSource(e.Source, true); + UpdateSelectionFromEventSource(e.Source); } } } diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 38af6d4dd8..53938d4620 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -219,6 +219,58 @@ namespace Perspex.Controls.UnitTests.Primitives Assert.False(((ListBoxItem)target.Presenter.Panel.Children[1]).IsSelected); } + [Fact] + public void Range_Select_Should_Select_Range() + { + var target = new TestSelector + { + Items = new[] + { + "foo", + "bar", + "baz", + "qux", + "qiz", + "lol", + }, + SelectionMode = SelectionMode.Multiple, + Template = Template(), + }; + + target.ApplyTemplate(); + target.SelectedIndex = 1; + target.SelectRange(3); + + Assert.Equal(new[] { 1, 2, 3 }, target.SelectedIndexes); + Assert.Equal(new[] { "bar", "baz", "qux" }, target.SelectedItems); + } + + [Fact] + public void Range_Select_Backwards_Should_Select_Range() + { + var target = new TestSelector + { + Items = new[] + { + "foo", + "bar", + "baz", + "qux", + "qiz", + "lol", + }, + SelectionMode = SelectionMode.Multiple, + Template = Template(), + }; + + target.ApplyTemplate(); + target.SelectedIndex = 3; + target.SelectRange(1); + + Assert.Equal(new[] { 3, 2, 1 }, target.SelectedIndexes); + Assert.Equal(new[] { "qux", "baz", "bar" }, target.SelectedItems); + } + private class TestSelector : SelectingItemsControl { public new IPerspexList SelectedIndexes @@ -230,6 +282,17 @@ namespace Perspex.Controls.UnitTests.Primitives { get { return base.SelectedItems; } } + + public new SelectionMode SelectionMode + { + get { return base.SelectionMode; } + set { base.SelectionMode = value; } + } + + public void SelectRange(int index) + { + UpdateSelection(index, true, true); + } } private ControlTemplate Template() From 05aed57daac2db2224bc15cbc202b587cbcf0626 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 9 Oct 2015 00:19:57 +0200 Subject: [PATCH 12/14] Fix selecting backwards ranges. --- src/Perspex.Base/Collections/IPerspexList.cs | 8 ++- src/Perspex.Base/Collections/PerspexList.cs | 15 ++++ .../Primitives/SelectingItemsControl.cs | 72 +++++++++++++------ 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/src/Perspex.Base/Collections/IPerspexList.cs b/src/Perspex.Base/Collections/IPerspexList.cs index 804ae73e07..2c8debb684 100644 --- a/src/Perspex.Base/Collections/IPerspexList.cs +++ b/src/Perspex.Base/Collections/IPerspexList.cs @@ -16,7 +16,6 @@ namespace Perspex.Collections /// new int Count { get; } - /// /// Gets or sets the element at the specified index. /// @@ -42,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 c0a35feac2..9c200aedaf 100644 --- a/src/Perspex.Base/Collections/PerspexList.cs +++ b/src/Perspex.Base/Collections/PerspexList.cs @@ -344,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) { diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 3ffdfb780b..db6838104c 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -293,11 +293,7 @@ namespace Perspex.Controls.Primitives } else if (multi && range) { - var first = SelectedIndexes[0]; - - // TODO: Don't deselect items in new selection. - SelectedIndexes.Clear(); - SelectedIndexes.AddRange(Range(first, index)); + SynchronizeIndexes(SelectedIndexes, SelectedIndexes[0], index); } else { @@ -327,18 +323,6 @@ namespace Perspex.Controls.Primitives } } - private IEnumerable Range(int first, int last) - { - int step = first > last ? -1 : 1; - - for (int i = first; i != last; i += step) - { - yield return i; - } - - yield return last; - } - /// /// Updates the selection for a container based on user interaction. /// @@ -424,6 +408,53 @@ namespace Perspex.Controls.Primitives return -1; } + /// + /// Generates a range of integers between the first and last inclusive. + /// + /// The first integer. + /// The last integer. + /// The range. + private static IEnumerable Range(int first, int last) + { + int step = first > last ? -1 : 1; + + for (int i = first; i != last; i += step) + { + yield return i; + } + + yield return last; + } + + /// + /// 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 (i < indexes.Count - 1) + { + indexes.RemoveRange(i, indexes.Count - i); + } + + indexes.AddRange(Range(next, last)); + } + } + /// /// Sets a container's 'selected' class or . /// @@ -557,13 +588,10 @@ namespace Perspex.Controls.Primitives { var sync = SelectedIndexes.Count != SelectedItems.Count; + SelectedItems.RemoveRange(listIndex, itemIndexes.Count()); + foreach (var itemIndex in itemIndexes) { - if (sync) - { - SelectedItems.RemoveAt(listIndex++); - } - MarkIndexSelected(itemIndex, false); } From 1ed1d0efb90fbdef44c502dff2e5ee2537eaee58 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 9 Oct 2015 00:27:54 +0200 Subject: [PATCH 13/14] Added additional test. --- .../SelectingItemsControlTests_Multiple.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 53938d4620..c61cb3282e 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -271,6 +271,33 @@ namespace Perspex.Controls.UnitTests.Primitives 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 From ddb5ed229aa78922c5db90f5f757e4d0b1c9986d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 9 Oct 2015 00:43:52 +0200 Subject: [PATCH 14/14] Make keyboard range selection work. --- src/Perspex.Controls/ListBox.cs | 5 ++++- src/Perspex.Input/FocusManager.cs | 16 +++++++++++----- src/Perspex.Input/GotFocusEventArgs.cs | 5 +++++ src/Perspex.Input/IFocusManager.cs | 6 +++++- src/Perspex.Input/IKeyboardDevice.cs | 5 ++++- src/Perspex.Input/IKeyboardNavigationHandler.cs | 6 +++++- src/Perspex.Input/KeyboardDevice.cs | 6 +++++- src/Perspex.Input/KeyboardNavigationHandler.cs | 10 +++++++--- .../Perspex.Win32/Input/WindowsKeyboardDevice.cs | 2 +- 9 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/Perspex.Controls/ListBox.cs b/src/Perspex.Controls/ListBox.cs index 044de39828..773a5ceb6e 100644 --- a/src/Perspex.Controls/ListBox.cs +++ b/src/Perspex.Controls/ListBox.cs @@ -38,7 +38,10 @@ namespace Perspex.Controls if (e.NavigationMethod == NavigationMethod.Directional) { - UpdateSelectionFromEventSource(e.Source); + UpdateSelectionFromEventSource( + e.Source, + true, + (e.InputModifiers & InputModifiers.Shift) != 0); } } 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/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)