diff --git a/Perspex.Controls/Deck.cs b/Perspex.Controls/Deck.cs index 67d5a83c3d..286eff7736 100644 --- a/Perspex.Controls/Deck.cs +++ b/Perspex.Controls/Deck.cs @@ -6,57 +6,59 @@ namespace Perspex.Controls { - using System.Collections; using Perspex.Animation; - using Perspex.Controls.Generators; using Perspex.Controls.Primitives; using Perspex.Controls.Utils; using Perspex.Input; /// - /// A selecting items control that displays a single item that fills the control. + /// An items control that displays its items as pages that fill the control. /// public class Deck : SelectingItemsControl { + /// + /// Defines the property. + /// public static readonly PerspexProperty TransitionProperty = PerspexProperty.Register("Transition"); - private static readonly ItemsPanelTemplate PanelTemplate = + /// + /// The default value of for . + /// + private static readonly ItemsPanelTemplate PanelTemplate = new ItemsPanelTemplate(() => new Panel()); + /// + /// Initializes static members of the class. + /// static Deck() { - ItemsPanelProperty.OverrideDefaultValue(typeof(Deck), PanelTemplate); + AutoSelectProperty.OverrideDefaultValue(true); + ItemsPanelProperty.OverrideDefaultValue(PanelTemplate); } + /// + /// Gets or sets the transition to use when moving between pages. + /// public IPageTransition Transition { get { return this.GetValue(TransitionProperty); } set { this.SetValue(TransitionProperty, value); } } - protected override void ItemsChanged(PerspexPropertyChangedEventArgs e) - { - base.ItemsChanged(e); - - var items = this.Items; - - if (items != null && items.Count() > 0) - { - this.SelectedIndex = 0; - } - } - + /// protected override void OnKeyDown(KeyEventArgs e) { // Ignore key presses. } + /// protected override void OnPointerPressed(PointerPressEventArgs e) { // Ignore pointer presses. } + /// protected override void OnTemplateApplied() { base.OnTemplateApplied(); diff --git a/Perspex.Controls/Generators/ItemContainerGenerator.cs b/Perspex.Controls/Generators/ItemContainerGenerator.cs index 57270966ad..af00571602 100644 --- a/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/Perspex.Controls/Generators/ItemContainerGenerator.cs @@ -92,7 +92,7 @@ namespace Perspex.Controls.Generators if (container != null) { result.Add(container); - this.containers[i] = null; + this.containers.Remove(i); } } diff --git a/Perspex.Controls/Presenters/DeckPresenter.cs b/Perspex.Controls/Presenters/DeckPresenter.cs index f39d0b38e3..899cf86140 100644 --- a/Perspex.Controls/Presenters/DeckPresenter.cs +++ b/Perspex.Controls/Presenters/DeckPresenter.cs @@ -6,74 +6,132 @@ namespace Perspex.Controls.Presenters { + using System; + using System.Collections; + using System.Linq; + using System.Reactive.Linq; + using System.Threading.Tasks; using Perspex.Animation; using Perspex.Controls.Generators; using Perspex.Controls.Primitives; using Perspex.Controls.Utils; - using Perspex.Input; using Perspex.Styling; - using System; - using System.Collections; - using System.Linq; - using System.Reactive.Linq; + /// + /// Displays pages inside an . + /// public class DeckPresenter : Control, IItemsPresenter, ITemplatedControl { + /// + /// Defines the property. + /// public static readonly PerspexProperty ItemsProperty = ItemsControl.ItemsProperty.AddOwner(); + /// + /// Defines the property. + /// public static readonly PerspexProperty ItemsPanelProperty = ItemsControl.ItemsPanelProperty.AddOwner(); - public static readonly PerspexProperty SelectedItemProperty = - SelectingItemsControl.SelectedItemProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly PerspexProperty SelectedIndexProperty = + SelectingItemsControl.SelectedIndexProperty.AddOwner(); + /// + /// Defines the property. + /// public static readonly PerspexProperty TransitionProperty = Deck.TransitionProperty.AddOwner(); private bool createdPanel; - public DeckPresenter() + private IItemContainerGenerator generator; + + /// + /// Initializes static members of the class. + /// + static DeckPresenter() { - this.GetObservableWithHistory(SelectedItemProperty).Subscribe(this.SelectedItemChanged); + SelectedIndexProperty.Changed.AddClassHandler(x => x.SelectedIndexChanged); } + /// + /// Gets the used to generate item container + /// controls. + /// public IItemContainerGenerator ItemContainerGenerator { - get; - private set; + get + { + if (this.generator == null) + { + var i = this.TemplatedParent as ItemsControl; + this.generator = i?.ItemContainerGenerator ?? new ItemContainerGenerator(this); + } + + return this.generator; + } + + set + { + if (this.generator != null) + { + throw new InvalidOperationException("ItemContainerGenerator is already set."); + } + + this.generator = value; + } } + /// + /// Gets or sets the items to display. + /// public IEnumerable Items { get { return this.GetValue(ItemsProperty); } set { this.SetValue(ItemsProperty, value); } } + /// + /// Gets or sets the panel used to display the pages. + /// public ItemsPanelTemplate ItemsPanel { get { return this.GetValue(ItemsPanelProperty); } set { this.SetValue(ItemsPanelProperty, value); } } - public object SelectedItem + /// + /// Gets or sets the index of the selected page. + /// + public int SelectedIndex { - get { return this.GetValue(SelectedItemProperty); } - set { this.SetValue(SelectedItemProperty, value); } + get { return this.GetValue(SelectedIndexProperty); } + set { this.SetValue(SelectedIndexProperty, value); } } + /// + /// Gets the panel used to display the pages. + /// public Panel Panel { get; private set; } + /// + /// Gets or sets a transition to use when switching pages. + /// public IPageTransition Transition { get { return this.GetValue(TransitionProperty); } set { this.SetValue(TransitionProperty, value); } } + /// public override sealed void ApplyTemplate() { if (!this.createdPanel) @@ -82,81 +140,73 @@ namespace Perspex.Controls.Presenters } } - protected override Size MeasureOverride(Size availableSize) - { - this.Panel.Measure(availableSize); - return this.Panel.DesiredSize; - } - - protected override Size ArrangeOverride(Size finalSize) - { - this.Panel.Arrange(new Rect(finalSize)); - return finalSize; - } - + /// + /// Creates the . + /// private void CreatePanel() { this.ClearVisualChildren(); - this.Panel = this.ItemsPanel.Build(); - this.Panel.TemplatedParent = this; - ((IItemsPanel)this.Panel).ChildLogicalParent = this.TemplatedParent as ILogical; - this.AddVisualChild(this.Panel); - this.createdPanel = true; - if (this.SelectedItem != null) + if (this.ItemsPanel != null) { - this.SelectedItemChanged(Tuple.Create(null, this.SelectedItem)); + this.Panel = this.ItemsPanel.Build(); + this.Panel.TemplatedParent = this; + ((IItemsPanel)this.Panel).ChildLogicalParent = this.TemplatedParent as ILogical; + this.AddVisualChild(this.Panel); + this.createdPanel = true; + var task = this.MoveToPage(-1, this.SelectedIndex); } } - private IItemContainerGenerator GetGenerator() + /// + /// Moves to the selected page, animating if a is set. + /// + /// The index of the old page. + /// The index of the new page. + /// A task tracking the animation. + private async Task MoveToPage(int fromIndex, int toIndex) { - if (this.ItemContainerGenerator == null) + var generator = this.ItemContainerGenerator; + IControl from = null; + IControl to = null; + + if (fromIndex != -1) { - ItemsControl i = this.TemplatedParent as ItemsControl; - this.ItemContainerGenerator = i?.ItemContainerGenerator ?? new ItemContainerGenerator(this); + from = generator.ContainerFromIndex(fromIndex); } - return this.ItemContainerGenerator; - } - - private async void SelectedItemChanged(Tuple value) - { - if (this.createdPanel) + if (toIndex != -1) { - var generator = this.GetGenerator(); - IControl from = null; - IControl to = null; - int fromIndex = -1; - int toIndex = -1; + var item = this.Items.Cast().ElementAt(toIndex); + to = generator.CreateContainers(toIndex, new[] { item }, null).FirstOrDefault(); - if (value.Item1 != null) + if (to != null) { - fromIndex = this.Items.IndexOf(value.Item1); - from = generator.ContainerFromIndex(fromIndex); + this.Panel.Children.Add(to); } + } - if (value.Item2 != null) - { - toIndex = this.Items.IndexOf(value.Item2); - to = generator.CreateContainers(toIndex, new[] { value.Item2 }, null).FirstOrDefault(); - - if (to != null) - { - this.Panel.Children.Add(to); - } - } + if (this.Transition != null) + { + await this.Transition.Start((Visual)from, (Visual)to, fromIndex < toIndex); + } - if (this.Transition != null) - { - await this.Transition.Start((Visual)from, (Visual)to, fromIndex < toIndex); - } + if (from != null) + { + this.Panel.Children.Remove(from); + generator.RemoveContainers(fromIndex, 1); + } + } - if (from != null) - { - this.Panel.Children.Remove(from); - generator.RemoveContainers(fromIndex, 1); - } + /// + /// Called when the property changes. + /// + /// The event args. + private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e) + { + if (this.Panel != null) + { + var task = this.MoveToPage((int)e.OldValue, (int)e.NewValue); } } } diff --git a/Perspex.Controls/Primitives/SelectingItemsControl.cs b/Perspex.Controls/Primitives/SelectingItemsControl.cs index 2cfad89bf1..e4e40ace73 100644 --- a/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -105,7 +105,15 @@ namespace Perspex.Controls.Primitives protected override void ItemsChanged(PerspexPropertyChangedEventArgs e) { base.ItemsChanged(e); - this.SelectedIndex = IndexOf((IEnumerable)e.NewValue, this.SelectedItem); + + if (this.SelectedIndex != -1) + { + this.SelectedIndex = IndexOf((IEnumerable)e.NewValue, this.SelectedItem); + } + else if (this.AutoSelect && this.Items != null & this.Items.Cast().Any()) + { + this.SelectedIndex = 0; + } } /// @@ -115,6 +123,13 @@ namespace Perspex.Controls.Primitives switch (e.Action) { + case NotifyCollectionChangedAction.Add: + if (this.AutoSelect && this.SelectedIndex == -1) + { + this.SelectedIndex = 0; + } + break; + case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Replace: var selectedIndex = this.SelectedIndex; @@ -252,10 +267,6 @@ namespace Perspex.Controls.Primitives var container = containers.Items[selectedIndex - containers.StartingIndex]; MarkContainerSelected(container, true); } - else if (selectedIndex == -1 && this.AutoSelect) - { - this.SelectedIndex = 0; - } } /// diff --git a/Perspex.Controls/TabControl.cs b/Perspex.Controls/TabControl.cs index 8120981f6a..4500640a88 100644 --- a/Perspex.Controls/TabControl.cs +++ b/Perspex.Controls/TabControl.cs @@ -6,82 +6,118 @@ namespace Perspex.Controls { - using System; - using System.Reactive.Linq; using Perspex.Animation; using Perspex.Collections; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.Input; + /// + /// A tab control that displays a tab strip along with the content of the selected tab. + /// public class TabControl : SelectingItemsControl, ILogical { + /// + /// Defines the property. + /// public static readonly PerspexProperty SelectedContentProperty = PerspexProperty.Register("SelectedContent"); + /// + /// Defines the property. + /// public static readonly PerspexProperty SelectedTabProperty = PerspexProperty.Register("SelectedTab"); + /// + /// Defines the property. + /// public static readonly PerspexProperty TransitionProperty = Deck.TransitionProperty.AddOwner(); - private PerspexReadOnlyListView logicalChildren = + private PerspexReadOnlyListView logicalChildren = new PerspexReadOnlyListView(); + /// + /// Initializes static members of the class. + /// static TabControl() { + AutoSelectProperty.OverrideDefaultValue(true); FocusableProperty.OverrideDefaultValue(false); + SelectedIndexProperty.Changed.AddClassHandler(x => x.SelectedIndexChanged); } + /// + /// Initializes a new instance of the class. + /// public TabControl() { - this.BindTwoWay(SelectedTabProperty, this, SelectingItemsControl.SelectedItemProperty); - - this.GetObservable(SelectedItemProperty).Subscribe(x => - { - ContentControl c = x as ContentControl; - object content = (c != null) ? c.Content : c; - this.SetValue(SelectedTabProperty, x); - this.SetValue(SelectedContentProperty, content); - }); } + /// + /// Gets the content of the selected tab. + /// public object SelectedContent { get { return this.GetValue(SelectedContentProperty); } - set { this.SetValue(SelectedContentProperty, value); } + private set { this.SetValue(SelectedContentProperty, value); } } + /// + /// Gets the as a . + /// public TabItem SelectedTab { get { return this.GetValue(SelectedTabProperty); } - set { this.SetValue(SelectedTabProperty, value); } + private set { this.SetValue(SelectedTabProperty, value); } } + /// + /// Gets or sets the transition to use when switching tabs. + /// public IPageTransition Transition { get { return this.GetValue(TransitionProperty); } set { this.SetValue(TransitionProperty, value); } } + /// + /// Gets the logical children of the control. + /// IPerspexReadOnlyList ILogical.LogicalChildren { get { return this.logicalChildren; } } + /// protected override void OnKeyDown(KeyEventArgs e) { // Don't handle keypresses. } - Deck deck; - + /// protected override void OnTemplateApplied() { base.OnTemplateApplied(); - this.deck = this.GetTemplateChild("deck"); + var deck = this.GetTemplateChild("deck"); this.logicalChildren.Source = ((ILogical)deck).LogicalChildren; } + + /// + /// Called when the property changes. + /// + /// The event args. + private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e) + { + if ((int)e.NewValue != -1) + { + var item = this.SelectedItem as IContentControl; + var content = item?.Content ?? item; + this.SelectedTab = item as TabItem; + this.SelectedContent = content; + } + } } } diff --git a/Perspex.Themes.Default/DeckStyle.cs b/Perspex.Themes.Default/DeckStyle.cs index 815c1eaf93..4ccefcc77c 100644 --- a/Perspex.Themes.Default/DeckStyle.cs +++ b/Perspex.Themes.Default/DeckStyle.cs @@ -35,7 +35,7 @@ namespace Perspex.Themes.Default Name = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~Deck.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~Deck.ItemsPanelProperty], - [~DeckPresenter.SelectedItemProperty] = control[~Deck.SelectedItemProperty], + [~DeckPresenter.SelectedIndexProperty] = control[~Deck.SelectedIndexProperty], [~DeckPresenter.TransitionProperty] = control[~Deck.TransitionProperty], }; } diff --git a/Tests/Perspex.Controls.UnitTests/DeckTests.cs b/Tests/Perspex.Controls.UnitTests/DeckTests.cs index 20d30f68a2..8320dc264b 100644 --- a/Tests/Perspex.Controls.UnitTests/DeckTests.cs +++ b/Tests/Perspex.Controls.UnitTests/DeckTests.cs @@ -29,6 +29,7 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); + Assert.Equal(0, target.SelectedIndex); Assert.Equal("Foo", target.SelectedItem); } @@ -61,7 +62,7 @@ namespace Perspex.Controls.UnitTests Name = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~Deck.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~Deck.ItemsPanelProperty], - [~DeckPresenter.SelectedItemProperty] = control[~Deck.SelectedItemProperty], + [~DeckPresenter.SelectedIndexProperty] = control[~Deck.SelectedIndexProperty], [~DeckPresenter.TransitionProperty] = control[~Deck.TransitionProperty], }; } diff --git a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index d3cca8ea9e..9735b311a5 100644 --- a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -95,6 +95,7 @@ + diff --git a/Tests/Perspex.Controls.UnitTests/Presenters/DeckPresenterTests.cs b/Tests/Perspex.Controls.UnitTests/Presenters/DeckPresenterTests.cs new file mode 100644 index 0000000000..7cd807006a --- /dev/null +++ b/Tests/Perspex.Controls.UnitTests/Presenters/DeckPresenterTests.cs @@ -0,0 +1,83 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls.UnitTests.Presenters +{ + using Perspex.Controls.Generators; + using Perspex.Controls.Presenters; + using Xunit; + + public class DeckPresenterTests + { + [Fact] + public void ApplyTemplate_Should_Create_Panel() + { + var target = new DeckPresenter + { + ItemsPanel = new ItemsPanelTemplate(() => new Panel()), + }; + + target.ApplyTemplate(); + + Assert.IsType(target.Panel); + } + + [Fact] + public void ItemContainerGenerator_Should_Be_Picked_Up_From_TemplatedControl() + { + var parent = new TestItemsControl(); + var target = new DeckPresenter + { + TemplatedParent = parent, + }; + + Assert.IsType>(target.ItemContainerGenerator); + } + + [Fact] + public void Setting_SelectedIndex_Should_Show_Page() + { + var target = new DeckPresenter + { + Items = new[] { "foo", "bar" }, + SelectedIndex = 0, + }; + + target.ApplyTemplate(); + + Assert.IsType(target.Panel.Children[0]); + Assert.Equal("foo", ((TextBlock)target.Panel.Children[0]).Text); + } + + [Fact] + public void Changing_SelectedIndex_Should_Show_Page() + { + var target = new DeckPresenter + { + Items = new[] { "foo", "bar" }, + SelectedIndex = 0, + }; + + target.ApplyTemplate(); + target.SelectedIndex = 1; + + Assert.IsType(target.Panel.Children[0]); + Assert.Equal("bar", ((TextBlock)target.Panel.Children[0]).Text); + } + + private class TestItem : ContentControl + { + } + + private class TestItemsControl : ItemsControl + { + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new TypedItemContainerGenerator(this); + } + } + } +} diff --git a/Tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/Tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index d6316f051f..1b71d8c000 100644 --- a/Tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/Tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -50,6 +50,18 @@ namespace Perspex.Controls.UnitTests.Presenters Assert.IsType(target.Panel.Children[1]); } + [Fact] + public void ItemContainerGenerator_Should_Be_Picked_Up_From_TemplatedControl() + { + var parent = new TestItemsControl(); + var target = new ItemsPresenter + { + TemplatedParent = parent, + }; + + Assert.IsType>(target.ItemContainerGenerator); + } + [Fact] public void Should_Remove_Containers() { @@ -174,5 +186,17 @@ namespace Perspex.Controls.UnitTests.Presenters Assert.Equal(target.Panel, child); } + + private class TestItem : ContentControl + { + } + + private class TestItemsControl : ItemsControl + { + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new TypedItemContainerGenerator(this); + } + } } } diff --git a/Tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/Tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index a394f40197..c39b5162bf 100644 --- a/Tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/Tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -31,6 +31,24 @@ namespace Perspex.Controls.UnitTests.Primitives Assert.Equal("foo", target.SelectedItem); } + [Fact] + public void First_Item_Should_Be_Selected_When_Added() + { + var items = new PerspexList(); + var target = new SelectingItemsControl + { + AutoSelect = true, + Items = items, + Template = this.Template(), + }; + + target.ApplyTemplate(); + items.Add("foo"); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("foo", target.SelectedItem); + } + [Fact] public void Item_Should_Be_Selected_When_Selection_Removed() { diff --git a/Tests/Perspex.Controls.UnitTests/TabControlTests.cs b/Tests/Perspex.Controls.UnitTests/TabControlTests.cs index e6939990a2..96014ba7da 100644 --- a/Tests/Perspex.Controls.UnitTests/TabControlTests.cs +++ b/Tests/Perspex.Controls.UnitTests/TabControlTests.cs @@ -45,7 +45,7 @@ namespace Perspex.Controls.UnitTests } [Fact] - public void Setting_SelectedItem_Should_Set_SelectedTab() + public void SelectedContent_Should_Initially_Be_First_Tab_Content() { var target = new TabControl { @@ -54,23 +54,24 @@ namespace Perspex.Controls.UnitTests { new TabItem { - Name = "first" + Name = "first", + Content = "foo", }, new TabItem { - Name = "second" + Name = "second", + Content = "bar", }, } }; target.ApplyTemplate(); - target.SelectedItem = target.Items.Cast().ElementAt(1); - Assert.Same(target.SelectedTab, target.SelectedItem); + Assert.Equal("foo", target.SelectedContent); } [Fact] - public void Setting_SelectedTab_Should_Set_SelectedItem() + public void Setting_SelectedItem_Should_Set_SelectedTab() { var target = new TabControl { @@ -79,19 +80,22 @@ namespace Perspex.Controls.UnitTests { new TabItem { - Name = "first" + Name = "first", + Content = "foo", }, new TabItem { - Name = "second" + Name = "second", + Content = "bar", }, } }; target.ApplyTemplate(); - target.SelectedTab = target.Items.Cast().ElementAt(1); + target.SelectedItem = target.Items.Cast().ElementAt(1); - Assert.Same(target.SelectedItem, target.SelectedTab); + Assert.Same(target.SelectedTab, target.SelectedItem); + Assert.Equal("bar", target.SelectedContent); } [Fact] @@ -145,7 +149,7 @@ namespace Perspex.Controls.UnitTests } } }; - } + } private Control CreateTabStripTemplate(TabStrip parent) { @@ -163,7 +167,7 @@ namespace Perspex.Controls.UnitTests Name = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~Deck.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~Deck.ItemsPanelProperty], - [~DeckPresenter.SelectedItemProperty] = control[~Deck.SelectedItemProperty], + [~DeckPresenter.SelectedIndexProperty] = control[~Deck.SelectedIndexProperty], [~DeckPresenter.TransitionProperty] = control[~Deck.TransitionProperty], }; }