Browse Source

Make TreeView selection work etc.

Unfortunately stuff is still broken - I think ItemContainerGenerator
needs a refactor.
Steven Kirk 10 years ago
parent
commit
6a005d35c8

+ 5 - 1
src/Perspex.Controls/Control.cs

@@ -406,7 +406,11 @@ namespace Perspex.Controls
             base.OnAttachedToVisualTree(root);
 
             IStyler styler = PerspexLocator.Current.GetService<IStyler>();
-            styler.ApplyStyles(this);
+
+            if (styler != null)
+            {
+                styler.ApplyStyles(this);
+            }
         }
 
         /// <summary>

+ 3 - 3
src/Perspex.Controls/Generators/IItemContainerGenerator.cs

@@ -43,9 +43,9 @@ namespace Perspex.Controls.Generators
         /// <param name="startingIndex">
         /// The index of the first item of the data in the containing collection.
         /// </param>
-        /// <param name="items">The items.</param>
-        /// <returns>The removed controls.</returns>
-        IList<IControl> RemoveContainers(int startingIndex, IEnumerable items);
+        /// <param name="count">The the number of items to remove.</param>
+        /// <returns>The removed containers.</returns>
+        IList<IControl> RemoveContainers(int startingIndex, int count);
 
         /// <summary>
         /// Clears the created containers from the index and returns the removed controls.

+ 14 - 0
src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs

@@ -15,5 +15,19 @@ namespace Perspex.Controls.Generators
         /// the root of the tree.
         /// </summary>
         ITreeItemContainerGenerator RootGenerator { get; }
+
+        /// <summary>
+        /// Gets the item container for the specified item, anywhere in the tree.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>The container, or null if not found.</returns>
+        IControl TreeContainerFromItem(object item);
+
+        /// <summary>
+        /// Gets the item for the specified item container, anywhere in the tree.
+        /// </summary>
+        /// <param name="container">The container.</param>
+        /// <returns>The item, or null if not found.</returns>
+        object TreeItemFromContainer(IControl container);
     }
 }

+ 31 - 34
src/Perspex.Controls/Generators/ItemContainerGenerator.cs

@@ -15,7 +15,7 @@ namespace Perspex.Controls.Generators
     /// </summary>
     public class ItemContainerGenerator : IItemContainerGenerator
     {
-        private Dictionary<int, IControl> _containers = new Dictionary<int, IControl>();
+        private List<IControl> _containers = new List<IControl>();
 
         private readonly Subject<ItemContainers> _containersInitialized = new Subject<ItemContainers>();
 
@@ -31,7 +31,7 @@ namespace Perspex.Controls.Generators
         /// <summary>
         /// Gets the currently realized containers.
         /// </summary>
-        public IEnumerable<IControl> Containers => _containers.Values;
+        public IEnumerable<IControl> Containers => _containers;
 
         /// <summary>
         /// Signalled whenever new containers are initialized.
@@ -81,24 +81,12 @@ namespace Perspex.Controls.Generators
         /// <param name="startingIndex">
         /// The index of the first item of the data in the containing collection.
         /// </param>
-        /// <param name="items">The items.</param>
+        /// <param name="count">The the number of items to remove.</param>
         /// <returns>The removed controls.</returns>
-        public IList<IControl> RemoveContainers(int startingIndex, IEnumerable items)
+        public virtual IList<IControl> RemoveContainers(int startingIndex, int count)
         {
-            var result = new List<IControl>();
-            var count = items.Cast<object>().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.
         /// </summary>
         /// <returns>The removed controls.</returns>
-        public IList<IControl> ClearContainers()
+        public virtual IList<IControl> ClearContainers()
         {
             var result = _containers;
-            _containers = new Dictionary<int, IControl>();
-            return result.Values.ToList();
+            _containers = new List<IControl>();
+            return result;
         }
 
         /// <summary>
@@ -120,9 +108,12 @@ namespace Perspex.Controls.Generators
         /// <returns>The container or null if no container created.</returns>
         public IControl ContainerFromIndex(int index)
         {
-            IControl result;
-            _containers.TryGetValue(index, out result);
-            return result;
+            if (index < _containers.Count)
+            {
+                return _containers[index];
+            }
+
+            return null;
         }
 
         /// <summary>
@@ -132,15 +123,7 @@ namespace Perspex.Controls.Generators
         /// <returns>The index of the container or -1 if not found.</returns>
         public int IndexFromContainer(IControl container)
         {
-            foreach (var i in _containers)
-            {
-                if (i.Value == container)
-                {
-                    return i.Key;
-                }
-            }
-
-            return -1;
+            return _containers.IndexOf(container);
         }
 
         /// <summary>
@@ -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<IControl> GetContainerRange(int index, int count)
+        {
+            return _containers.GetRange(index, count);
+        }
     }
 }

+ 91 - 1
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<T> : ItemContainerGenerator<T>, ITreeItemContainerGenerator
         where T : class, IControl, new()
     {
-        private ITreeItemContainerGenerator rootGenerator;
+        private Dictionary<object, T> _itemToContainer;
+        private Dictionary<IControl, object> _containerToItem;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="TreeItemContainerGenerator{T}"/> class.
@@ -36,6 +39,12 @@ namespace Perspex.Controls.Generators
             ItemsProperty = itemsProperty;
             IsExpandedProperty = isExpandedProperty;
             RootGenerator = rootGenerator;
+
+            if (rootGenerator == null)
+            {
+                _itemToContainer = new Dictionary<object, T>();
+                _containerToItem = new Dictionary<IControl, object>();
+            }
         }
 
         /// <summary>
@@ -54,6 +63,30 @@ namespace Perspex.Controls.Generators
         /// </summary>
         protected PerspexProperty IsExpandedProperty { get; }
 
+        /// <summary>
+        /// Gets the item container for the specified item, anywhere in the tree.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>The container, or null if not found.</returns>
+        public IControl TreeContainerFromItem(object item)
+        {
+            T result;
+            _itemToContainer.TryGetValue(item, out result);
+            return result;
+        }
+
+        /// <summary>
+        /// Gets the item for the specified item container, anywhere in the tree.
+        /// </summary>
+        /// <param name="container">The container.</param>
+        /// <returns>The item, or null if not found.</returns>
+        public object TreeItemFromContainer(IControl container)
+        {
+            object result;
+            _containerToItem.TryGetValue(container, out result);
+            return result;
+        }
+
         /// <inheritdoc/>
         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<IControl> ClearContainers()
+        {
+            ClearIndex();
+            return base.ClearContainers();
+        }
+
+        public override IList<IControl> 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<T>)RootGenerator).AddToIndex(item, container);
+            }
+            else
+            {
+                _itemToContainer.Add(item, container);
+                _containerToItem.Add(container, item);
+            }
+        }
+
+        private void RemoveFromIndex(IEnumerable<IControl> containers)
+        {
+            if (RootGenerator != null)
+            {
+                ((TreeItemContainerGenerator<T>)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<T>)RootGenerator).ClearIndex();
+            }
+            else
+            {
+                _containerToItem.Clear();
+                _itemToContainer.Clear();
+            }
+        }
+
         /// <summary>
         /// Gets the data template for the specified item.
         /// </summary>

+ 1 - 1
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);
             }
         }
 

+ 1 - 1
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;
                 }
 

+ 46 - 24
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);
+                }
+            }
         }
 
         /// <summary>
@@ -131,41 +149,45 @@ namespace Perspex.Controls
         protected IControl GetContainerFromEventSource(IInteractive eventSource)
         {
             var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
-                .OfType<ILogical>()
-                .FirstOrDefault(x => x.LogicalParent is TreeViewItem);
+                .OfType<TreeViewItem>()
+                .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;
         }
 
-        /// <inheritdoc/>
-        private void SelectedItemChanged(object selected)
+        /// <summary>
+        /// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
+        /// </summary>
+        /// <param name="container">The container.</param>
+        /// <param name="selected">Whether the control is selected</param>
+        private void MarkContainerSelected(IControl container, bool selected)
         {
-            //var containers = ItemContainerGenerator.GetAllContainers().OfType<ISelectable>();
-            //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");
+                }
+            }
         }
     }
 }

+ 99 - 9
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<TreeViewItem>()
+                .Select(x => x.Header)
+                .OfType<TextBlock>()
+                .Select(x => x.Text)
+                .ToList();
 
-            foreach (var child in target.GetLogicalChildren())
-            {
-                Assert.IsType<TreeViewItem>(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<IControl> controls)
+        {
+            foreach (TreeViewItem control in controls)
+            {
+                control.Template = CreateTreeViewItemTemplate();
+                control.ApplyTemplate();
+                ApplyTemplates(control.Presenter.Panel.Children);
+            }
+        }
+
         private IList<Node> CreateTestTreeData()
         {
             return new[]
@@ -139,7 +229,7 @@ namespace Perspex.Controls.UnitTests
             });
         }
 
-        private List<string> ExtractItemContent(TreeView tree, int level)
+        private List<string> ExtractItemHeader(TreeView tree, int level)
         {
             return ExtractItemContent(tree.Presenter.Panel, 0, level)
                 .Select(x => x.Header)