diff --git a/src/Perspex.Controls/Control.cs b/src/Perspex.Controls/Control.cs index e41be6ccc3..62150c76b9 100644 --- a/src/Perspex.Controls/Control.cs +++ b/src/Perspex.Controls/Control.cs @@ -406,7 +406,11 @@ namespace Perspex.Controls base.OnAttachedToVisualTree(root); IStyler styler = PerspexLocator.Current.GetService(); - styler.ApplyStyles(this); + + if (styler != null) + { + styler.ApplyStyles(this); + } } /// diff --git a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs index 2009131bc5..ff1d57994a 100644 --- a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs @@ -43,9 +43,9 @@ namespace Perspex.Controls.Generators /// /// The index of the first item of the data in the containing collection. /// - /// The items. - /// The removed controls. - IList RemoveContainers(int startingIndex, IEnumerable items); + /// The the number of items to remove. + /// The removed containers. + IList RemoveContainers(int startingIndex, int count); /// /// Clears the created containers from the index and returns the removed controls. diff --git a/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs index 08cf643d4c..37f097618b 100644 --- a/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs @@ -15,5 +15,19 @@ namespace Perspex.Controls.Generators /// the root of the tree. /// ITreeItemContainerGenerator RootGenerator { get; } + + /// + /// Gets the item container for the specified item, anywhere in the tree. + /// + /// The item. + /// The container, or null if not found. + IControl TreeContainerFromItem(object item); + + /// + /// Gets the item for the specified item container, anywhere in the tree. + /// + /// The container. + /// The item, or null if not found. + object TreeItemFromContainer(IControl container); } } \ No newline at end of file diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs index 9e8183c322..db7d39250b 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs @@ -15,7 +15,7 @@ namespace Perspex.Controls.Generators /// public class ItemContainerGenerator : IItemContainerGenerator { - private Dictionary _containers = new Dictionary(); + private List _containers = new List(); private readonly Subject _containersInitialized = new Subject(); @@ -31,7 +31,7 @@ namespace Perspex.Controls.Generators /// /// Gets the currently realized containers. /// - public IEnumerable Containers => _containers.Values; + public IEnumerable Containers => _containers; /// /// Signalled whenever new containers are initialized. @@ -81,24 +81,12 @@ namespace Perspex.Controls.Generators /// /// The index of the first item of the data in the containing collection. /// - /// The items. + /// The the number of items to remove. /// The removed controls. - public IList RemoveContainers(int startingIndex, IEnumerable items) + public virtual IList RemoveContainers(int startingIndex, int count) { - var result = new List(); - var count = items.Cast().Count(); - - for (int i = startingIndex; i < startingIndex + count; ++i) - { - var container = _containers[i]; - - if (container != null) - { - result.Add(container); - _containers.Remove(i); - } - } - + var result = _containers.GetRange(startingIndex, count); + _containers.RemoveRange(startingIndex, count); return result; } @@ -106,11 +94,11 @@ namespace Perspex.Controls.Generators /// Clears the created containers from the index and returns the removed controls. /// /// The removed controls. - public IList ClearContainers() + public virtual IList ClearContainers() { var result = _containers; - _containers = new Dictionary(); - return result.Values.ToList(); + _containers = new List(); + return result; } /// @@ -120,9 +108,12 @@ namespace Perspex.Controls.Generators /// The container or null if no container created. public IControl ContainerFromIndex(int index) { - IControl result; - _containers.TryGetValue(index, out result); - return result; + if (index < _containers.Count) + { + return _containers[index]; + } + + return null; } /// @@ -132,15 +123,7 @@ namespace Perspex.Controls.Generators /// The index of the container or -1 if not found. public int IndexFromContainer(IControl container) { - foreach (var i in _containers) - { - if (i.Value == container) - { - return i.Key; - } - } - - return -1; + return _containers.IndexOf(container); } /// @@ -171,7 +154,16 @@ namespace Perspex.Controls.Generators foreach (var c in container) { - if (!_containers.ContainsKey(index)) + while (_containers.Count < index) + { + _containers.Add(null); + } + + if (_containers.Count == index) + { + _containers.Add(c); + } + else if (_containers[index] == null) { _containers[index] = c; } @@ -183,5 +175,10 @@ namespace Perspex.Controls.Generators ++index; } } + + protected IEnumerable GetContainerRange(int index, int count) + { + return _containers.GetRange(index, count); + } } } \ No newline at end of file diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index 29cc5f2d32..a08ee9ebbd 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -1,6 +1,8 @@ // 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.Collections; +using System.Collections.Generic; using Perspex.Controls.Templates; namespace Perspex.Controls.Generators @@ -12,7 +14,8 @@ namespace Perspex.Controls.Generators public class TreeItemContainerGenerator : ItemContainerGenerator, ITreeItemContainerGenerator where T : class, IControl, new() { - private ITreeItemContainerGenerator rootGenerator; + private Dictionary _itemToContainer; + private Dictionary _containerToItem; /// /// Initializes a new instance of the class. @@ -36,6 +39,12 @@ namespace Perspex.Controls.Generators ItemsProperty = itemsProperty; IsExpandedProperty = isExpandedProperty; RootGenerator = rootGenerator; + + if (rootGenerator == null) + { + _itemToContainer = new Dictionary(); + _containerToItem = new Dictionary(); + } } /// @@ -54,6 +63,30 @@ namespace Perspex.Controls.Generators /// protected PerspexProperty IsExpandedProperty { get; } + /// + /// Gets the item container for the specified item, anywhere in the tree. + /// + /// The item. + /// The container, or null if not found. + public IControl TreeContainerFromItem(object item) + { + T result; + _itemToContainer.TryGetValue(item, out result); + return result; + } + + /// + /// Gets the item for the specified item container, anywhere in the tree. + /// + /// The container. + /// The item, or null if not found. + public object TreeItemFromContainer(IControl container) + { + object result; + _containerToItem.TryGetValue(container, out result); + return result; + } + /// protected override IControl CreateContainer(object item) { @@ -81,10 +114,67 @@ namespace Perspex.Controls.Generators result.DataContext = item; } + AddToIndex(item, result); + return result; } } + public override IList ClearContainers() + { + ClearIndex(); + return base.ClearContainers(); + } + + public override IList RemoveContainers(int startingIndex, int count) + { + RemoveFromIndex(GetContainerRange(startingIndex, count)); + return base.RemoveContainers(startingIndex, count); + } + + private void AddToIndex(object item, T container) + { + if (RootGenerator != null) + { + ((TreeItemContainerGenerator)RootGenerator).AddToIndex(item, container); + } + else + { + _itemToContainer.Add(item, container); + _containerToItem.Add(container, item); + } + } + + private void RemoveFromIndex(IEnumerable containers) + { + if (RootGenerator != null) + { + ((TreeItemContainerGenerator)RootGenerator).RemoveFromIndex(containers); + } + else + { + foreach (var container in containers) + { + var item = _containerToItem[container]; + _containerToItem.Remove(container); + _itemToContainer.Remove(item); + } + } + } + + private void ClearIndex() + { + if (RootGenerator != null) + { + ((TreeItemContainerGenerator)RootGenerator).ClearIndex(); + } + else + { + _containerToItem.Clear(); + _itemToContainer.Clear(); + } + } + /// /// Gets the data template for the specified item. /// diff --git a/src/Perspex.Controls/Presenters/CarouselPresenter.cs b/src/Perspex.Controls/Presenters/CarouselPresenter.cs index 4687740adb..8641202acf 100644 --- a/src/Perspex.Controls/Presenters/CarouselPresenter.cs +++ b/src/Perspex.Controls/Presenters/CarouselPresenter.cs @@ -215,7 +215,7 @@ namespace Perspex.Controls.Presenters if (from != null) { Panel.Children.Remove(from); - generator.RemoveContainers(fromIndex, new[] { from }); + generator.RemoveContainers(fromIndex, 1); } } diff --git a/src/Perspex.Controls/Presenters/ItemsPresenter.cs b/src/Perspex.Controls/Presenters/ItemsPresenter.cs index 5c52f70df4..09132cfced 100644 --- a/src/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/src/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -248,7 +248,7 @@ namespace Perspex.Controls.Presenters case NotifyCollectionChangedAction.Remove: Panel.Children.RemoveAll( - generator.RemoveContainers(e.OldStartingIndex, e.OldItems)); + generator.RemoveContainers(e.OldStartingIndex, e.OldItems.Count)); break; } diff --git a/src/Perspex.Controls/TreeView.cs b/src/Perspex.Controls/TreeView.cs index ee6e7b9d84..d9b9271904 100644 --- a/src/Perspex.Controls/TreeView.cs +++ b/src/Perspex.Controls/TreeView.cs @@ -7,6 +7,7 @@ using Perspex.Controls.Generators; using Perspex.Controls.Primitives; using Perspex.Input; using Perspex.Interactivity; +using Perspex.Styling; using Perspex.VisualTree; namespace Perspex.Controls @@ -92,6 +93,23 @@ namespace Perspex.Controls bool rangeModifier = false, bool toggleModifier = false) { + var item = ItemContainerGenerator.TreeItemFromContainer(container); + + if (item != null) + { + if (SelectedItem != null) + { + var old = ItemContainerGenerator.TreeContainerFromItem(SelectedItem); + MarkContainerSelected(old, false); + } + + SelectedItem = item; + + if (SelectedItem != null) + { + MarkContainerSelected(container, true); + } + } } /// @@ -131,41 +149,45 @@ namespace Perspex.Controls protected IControl GetContainerFromEventSource(IInteractive eventSource) { var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.LogicalParent is TreeViewItem); + .OfType() + .FirstOrDefault(); if (item != null) { - var treeViewItem = (TreeViewItem)item.LogicalParent; - - if (treeViewItem.ItemContainerGenerator.RootGenerator == this.ItemContainerGenerator) + if (item.ItemContainerGenerator.RootGenerator == this.ItemContainerGenerator) { - return treeViewItem; + return item; } } return null; } - /// - private void SelectedItemChanged(object selected) + /// + /// Sets a container's 'selected' class or . + /// + /// The container. + /// Whether the control is selected + private void MarkContainerSelected(IControl container, bool selected) { - //var containers = ItemContainerGenerator.GetAllContainers().OfType(); - //var selectedContainer = (selected != null) ? - // ItemContainerGenerator.ContainerFromItem(selected) : - // null; - - //if (Presenter != null && Presenter.Panel != null) - //{ - // KeyboardNavigation.SetTabOnceActiveElement( - // (InputElement)Presenter.Panel, - // selectedContainer); - //} - - //foreach (var item in containers) - //{ - // item.IsSelected = item == selectedContainer; - //} + var selectable = container as ISelectable; + var styleable = container as IStyleable; + + if (selectable != null) + { + selectable.IsSelected = selected; + } + else if (styleable != null) + { + if (selected) + { + styleable.Classes.Add(":selected"); + } + else + { + styleable.Classes.Remove(":selected"); + } + } } } } diff --git a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs index 623e772e0a..ba1c3bb2de 100644 --- a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; +using Perspex.Input; using Perspex.LogicalTree; using Xunit; @@ -24,9 +25,80 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); - Assert.Equal(new[] { "Root" }, ExtractItemContent(target, 0)); - Assert.Equal(new[] { "Child1", "Child2" }, ExtractItemContent(target, 1)); - Assert.Equal(new[] { "Grandchild2a" }, ExtractItemContent(target, 2)); + Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); + Assert.Equal(new[] { "Child1", "Child2" }, ExtractItemHeader(target, 1)); + Assert.Equal(new[] { "Grandchild2a" }, ExtractItemHeader(target, 2)); + } + + [Fact] + public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers() + { + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = CreateTestTreeData(), + DataTemplates = CreateNodeDataTemplate(), + }; + + target.ApplyTemplate(); + + var container = (TreeViewItem)target.ItemContainerGenerator.Containers.Single(); + var header = (TextBlock)container.Header; + Assert.Equal("Root", header.Text); + } + + [Fact] + public void Root_TreeContainerFromItem_Should_Return_Descendent_Item() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + DataTemplates = CreateNodeDataTemplate(), + }; + + // For TreeViewItem to find its parent TreeView, OnAttachedToVisualTree needs + // to be called, which requires an IRenderRoot. + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + ApplyTemplates(target); + + var container = target.ItemContainerGenerator.TreeContainerFromItem( + tree[0].Children[1].Children[0]); + var header = ((TreeViewItem)container).Header; + var headerContent = ((TextBlock)header).Text; + + Assert.Equal("Grandchild2a", headerContent); + } + + [Fact] + public void Clicking_Item_Should_Select_It() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + DataTemplates = CreateNodeDataTemplate(), + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + ApplyTemplates(target); + + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.TreeContainerFromItem(item); + + container.RaiseEvent(new PointerPressEventArgs + { + RoutedEvent = InputElement.PointerPressedEvent, + MouseButton = MouseButton.Left, + }); + + Assert.Equal(item, target.SelectedItem); + Assert.True(container.IsSelected); } [Fact] @@ -40,12 +112,14 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); - Assert.Equal(3, target.GetLogicalChildren().Count()); + var result = target.GetLogicalChildren() + .OfType() + .Select(x => x.Header) + .OfType() + .Select(x => x.Text) + .ToList(); - foreach (var child in target.GetLogicalChildren()) - { - Assert.IsType(child); - } + Assert.Equal(new[] { "Foo", "Bar", "Baz " }, result); } [Fact] @@ -82,6 +156,22 @@ namespace Perspex.Controls.UnitTests dataContexts); } + private void ApplyTemplates(TreeView tree) + { + tree.ApplyTemplate(); + ApplyTemplates(tree.Presenter.Panel.Children); + } + + private void ApplyTemplates(IEnumerable controls) + { + foreach (TreeViewItem control in controls) + { + control.Template = CreateTreeViewItemTemplate(); + control.ApplyTemplate(); + ApplyTemplates(control.Presenter.Panel.Children); + } + } + private IList CreateTestTreeData() { return new[] @@ -139,7 +229,7 @@ namespace Perspex.Controls.UnitTests }); } - private List ExtractItemContent(TreeView tree, int level) + private List ExtractItemHeader(TreeView tree, int level) { return ExtractItemContent(tree.Presenter.Panel, 0, level) .Select(x => x.Header)