diff --git a/Perspex.Base/PerspexProperty.cs b/Perspex.Base/PerspexProperty.cs index 2c3f089b32..e886859a0e 100644 --- a/Perspex.Base/PerspexProperty.cs +++ b/Perspex.Base/PerspexProperty.cs @@ -77,8 +77,12 @@ namespace Perspex this.defaultValues.Add(ownerType, defaultValue); this.Inherits = inherits; this.DefaultBindingMode = defaultBindingMode; - this.validation.Add(ownerType, validate); this.IsAttached = isAttached; + + if (validate != null) + { + this.validation.Add(ownerType, validate); + } } /// diff --git a/Perspex.Controls/Generators/IItemContainerGenerator.cs b/Perspex.Controls/Generators/IItemContainerGenerator.cs index 29c6f4a57f..3f780dee40 100644 --- a/Perspex.Controls/Generators/IItemContainerGenerator.cs +++ b/Perspex.Controls/Generators/IItemContainerGenerator.cs @@ -1,6 +1,6 @@ // ----------------------------------------------------------------------- // -// Copyright 2014 MIT Licence. See licence.md for more information. +// Copyright 2015 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- @@ -9,30 +9,60 @@ namespace Perspex.Controls.Generators using System; using System.Collections; using System.Collections.Generic; + using Templates; - public enum ItemContainerGeneratorState - { - NoStarted, - Generating, - Generated, - } - + /// + /// Creates containers for items and maintains a list of created containers. + /// public interface IItemContainerGenerator { - event EventHandler StateChanged; - - ItemContainerGeneratorState State { get; } - - Control GetContainerForItem(object item); + /// + /// Signalled whenever new containers are initialized. + /// + IObservable ContainersInitialized { get; } - object GetItemForContainer(Control container); + /// + /// Creates container controls for a collection of items. + /// + /// + /// The index of the first item of the data in the containing collection. + /// + /// The items. + /// An optional item template. + /// The created controls. + IList CreateContainers( + int startingIndex, + IEnumerable items, + IDataTemplate itemTemplate); - IEnumerable> GetAll(); + /// + /// Removes a set of created containers from the index and returns the removed controls. + /// + /// + /// The index of the first item of the data in the containing collection. + /// + /// The number of items to remove. + /// The removed controls. + IList RemoveContainers(int startingIndex, int count); - IEnumerable Generate(IEnumerable items); + /// + /// Clears the created containers from the index and returns the removed controls. + /// + /// The removed controls. + IList ClearContainers(); - IEnumerable Remove(IEnumerable item); + /// + /// Gets the container control representing the item with the specified index. + /// + /// The index. + /// The container, or null if no container created. + IControl ContainerFromIndex(int index); - void RemoveAll(); + /// + /// Gets the index of the specified container control. + /// + /// The container. + /// The index of the container, or -1 if not found. + int IndexFromContainer(IControl container); } -} +} \ No newline at end of file diff --git a/Perspex.Controls/Generators/ItemContainerGenerator.cs b/Perspex.Controls/Generators/ItemContainerGenerator.cs index 1bd0d5a812..57270966ad 100644 --- a/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/Perspex.Controls/Generators/ItemContainerGenerator.cs @@ -1,6 +1,6 @@ // ----------------------------------------------------------------------- // -// Copyright 2014 MIT Licence. See licence.md for more information. +// Copyright 2015 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- @@ -10,145 +10,182 @@ namespace Perspex.Controls.Generators using System.Collections; using System.Collections.Generic; using System.Linq; + using System.Reactive.Subjects; using Perspex.Controls.Templates; + /// + /// Creates containers for items and maintains a list of created containers. + /// public class ItemContainerGenerator : IItemContainerGenerator { - private Dictionary containersByItem = new Dictionary(); + private Dictionary containers = new Dictionary(); - private Dictionary itemsByContainer = new Dictionary(); + private Subject containersInitialized; - private ItemContainerGeneratorState state; - - public ItemContainerGenerator(Control owner) + /// + /// Initializes a new instance of the class. + /// + /// The owner control. + public ItemContainerGenerator(IControl owner) { this.Owner = owner; + this.containersInitialized = new Subject(); } - public event EventHandler StateChanged; - - public ItemContainerGeneratorState State + /// + /// Signalled whenever new containers are initialized. + /// + public IObservable ContainersInitialized => this.containersInitialized; + + /// + /// Gets the owner control. + /// + public IControl Owner { get; } + + /// + /// Creates container controls for a collection of items. + /// + /// + /// The index of the first item of the data in the containing collection. + /// + /// The items. + /// An optional item template. + /// The created container controls. + public IList CreateContainers( + int startingIndex, + IEnumerable items, + IDataTemplate itemTemplate) { - get + Contract.Requires(items != null); + + int index = startingIndex; + var result = new List(); + + foreach (var item in items) { - return this.state; + IControl container = this.CreateContainer(item, itemTemplate); + result.Add(container); } - private set - { - if (this.state != value) - { - this.state = value; + this.AddContainers(startingIndex, result); + this.containersInitialized.OnNext(new ItemContainers(startingIndex, result)); - if (this.StateChanged != null) - { - this.StateChanged(this, EventArgs.Empty); - } - } - } + return result.Where(x => x != null).ToList(); } - protected Control Owner + /// + /// Removes a set of created containers from the index and returns the removed controls. + /// + /// + /// The index of the first item of the data in the containing collection. + /// + /// The number of items to remove. + /// The removed controls. + public IList RemoveContainers(int startingIndex, int count) { - get; - private set; - } + var result = new List(); + + for (int i = startingIndex; i < startingIndex + count; ++i) + { + var container = this.containers[i]; + + if (container != null) + { + result.Add(container); + this.containers[i] = null; + } + } - public Control GetContainerForItem(object item) - { - Control result; - this.containersByItem.TryGetValue(item, out result); return result; } - public object GetItemForContainer(Control container) + /// + /// Clears the created containers from the index and returns the removed controls. + /// + /// The removed controls. + public IList ClearContainers() { - object result; - this.itemsByContainer.TryGetValue(container, out result); - return result; + var result = this.containers; + this.containers = new Dictionary(); + return result.Values.ToList(); } - public IEnumerable> GetAll() + /// + /// Gets the container control representing the item with the specified index. + /// + /// The index. + /// The container or null if no container created. + public IControl ContainerFromIndex(int index) { - return this.containersByItem.Select(x => Tuple.Create(x.Key, x.Value)); + IControl result; + this.containers.TryGetValue(index, out result); + return result; } - IEnumerable IItemContainerGenerator.Generate(IEnumerable items) + /// + /// Gets the index of the specified container control. + /// + /// The container. + /// The index of the container or -1 if not found. + public int IndexFromContainer(IControl container) { - List result = new List(); - - this.State = ItemContainerGeneratorState.Generating; - - try + foreach (var i in this.containers) { - foreach (object item in items) + if (i.Value == container) { - Control container = this.CreateContainerOverride(item); - - if (container != null) - { - if (container.DataContext == null) - { - container.DataContext = item; - } - - container.TemplatedParent = null; - this.AddInternal(item, container); - result.Add(container); - } + return i.Key; } } - finally - { - this.State = ItemContainerGeneratorState.Generated; - } - return result; + return -1; } - IEnumerable IItemContainerGenerator.Remove(IEnumerable items) + /// + /// Creates the container for an item. + /// + /// The item. + /// An optional item template. + /// The created container control. + protected virtual IControl CreateContainer(object item, IDataTemplate itemTemplate) { - List result = new List(); - - foreach (var item in items) + if (item == null) { - result.Add(this.RemoveByItemInternal(item)); + return null; + } + else if (itemTemplate != null && itemTemplate.Match(item)) + { + var result = itemTemplate.Build(item); + result.DataContext = item; + return result; + } + else + { + return this.Owner.MaterializeDataTemplate(item); } - - return result; - } - - void IItemContainerGenerator.RemoveAll() - { - this.containersByItem.Clear(); - this.itemsByContainer.Clear(); - } - - protected virtual Control CreateContainerOverride(object item) - { - return (Control)this.Owner.MaterializeDataTemplate(item); } - protected void AddInternal(object item, Control container) + /// + /// Adds a collection of containers to the index. + /// + /// The starting index. + /// The container. + protected void AddContainers(int index, IList container) { - this.containersByItem.Add(item, container); - this.itemsByContainer.Add(container, item); - } + Contract.Requires(container != null); - protected object RemoveByContainerInternal(Control container) - { - object item = this.itemsByContainer[container]; - this.containersByItem.Remove(item); - this.itemsByContainer.Remove(container); - return item; - } + foreach (var c in container) + { + if (!this.containers.ContainsKey(index)) + { + this.containers[index] = c; + } + else + { + throw new InvalidOperationException("Container already created."); + } - protected Control RemoveByItemInternal(object item) - { - Control container = this.containersByItem[item]; - this.containersByItem.Remove(item); - this.itemsByContainer.Remove(container); - return container; + ++index; + } } } -} +} \ No newline at end of file diff --git a/Perspex.Controls/Generators/ItemContainers.cs b/Perspex.Controls/Generators/ItemContainers.cs new file mode 100644 index 0000000000..f74cccfb8b --- /dev/null +++ b/Perspex.Controls/Generators/ItemContainers.cs @@ -0,0 +1,37 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls.Generators +{ + using System.Collections.Generic; + + /// + /// Holds details about a set of item containers in an . + /// + public class ItemContainers + { + /// + /// Initializes a new instance of the class. + /// + /// The index of the first container in the source items. + /// The containers. + public ItemContainers(int startingIndex, IList containers) + { + this.StartingIndex = startingIndex; + this.Items = containers; + } + + /// + /// Gets the index of the first container in the source items. + /// + public int StartingIndex { get; } + + /// + /// Gets the containers. May contain null entries. + /// + public IList Items { get; } + } +} diff --git a/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index d5ecd58e83..dae0913e25 100644 --- a/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -19,74 +19,75 @@ namespace Perspex.Controls.Generators { } - IEnumerable IItemContainerGenerator.Remove(IEnumerable items) - { - var result = new List(); - - foreach (var item in items) - { - var container = (T)this.GetContainerForItem(item); - this.Remove(container, result); - } - - return result; - } - - protected override Control CreateContainerOverride(object item) - { - T result = item as T; - - if (result == null) - { - TreeDataTemplate template = this.GetTreeDataTemplate(item); - - result = new T - { - Header = template.Build(item), - Items = template.ItemsSelector(item), - IsExpanded = template.IsExpanded(item), - }; - } - - return result; - } - - private TreeDataTemplate GetTreeDataTemplate(object item) - { - IDataTemplate template = this.Owner.FindDataTemplate(item); - - if (template == null) - { - template = DataTemplate.Default; - } - - TreeDataTemplate treeTemplate = template as TreeDataTemplate; - - if (treeTemplate == null) - { - treeTemplate = new TreeDataTemplate(template.Build, x => null); - } - - return treeTemplate; - } - - private void Remove(T container, List removed) - { - if (container.Items != null) - { - foreach (var childItem in container.Items) - { - var childContainer = (T)this.GetContainerForItem(childItem); - - if (childContainer != null) - { - this.Remove(childContainer, removed); - } - } - } - - this.RemoveByContainerInternal(container); - removed.Add(container); - } + ////IEnumerable IItemContainerGenerator.Remove(IEnumerable items) + ////{ + //// throw new NotImplementedException(); + //// var result = new List(); + + //// foreach (var item in items) + //// { + //// var container = (T)this.GetContainerForItem(item); + //// this.Remove(container, result); + //// } + + //// return result; + ////} + + ////protected override Control CreateContainerOverride(object item) + ////{ + //// T result = item as T; + + //// if (result == null) + //// { + //// TreeDataTemplate template = this.GetTreeDataTemplate(item); + + //// result = new T + //// { + //// Header = template.Build(item), + //// Items = template.ItemsSelector(item), + //// IsExpanded = template.IsExpanded(item), + //// }; + //// } + + //// return result; + ////} + + ////private TreeDataTemplate GetTreeDataTemplate(object item) + ////{ + //// IDataTemplate template = this.Owner.FindDataTemplate(item); + + //// if (template == null) + //// { + //// template = DataTemplate.Default; + //// } + + //// TreeDataTemplate treeTemplate = template as TreeDataTemplate; + + //// if (treeTemplate == null) + //// { + //// treeTemplate = new TreeDataTemplate(template.Build, x => null); + //// } + + //// return treeTemplate; + ////} + + ////private void Remove(T container, List removed) + ////{ + //// if (container.Items != null) + //// { + //// foreach (var childItem in container.Items) + //// { + //// var childContainer = (T)this.GetContainerForItem(childItem); + + //// if (childContainer != null) + //// { + //// this.Remove(childContainer, removed); + //// } + //// } + //// } + + //// this.RemoveByContainerInternal(container); + //// removed.Add(container); + ////} } } diff --git a/Perspex.Controls/Generators/TypedItemContainerGenerator.cs b/Perspex.Controls/Generators/TypedItemContainerGenerator.cs index dc9196b3a2..aca08bc3a6 100644 --- a/Perspex.Controls/Generators/TypedItemContainerGenerator.cs +++ b/Perspex.Controls/Generators/TypedItemContainerGenerator.cs @@ -15,7 +15,7 @@ namespace Perspex.Controls.Generators { } - protected override Control CreateContainerOverride(object item) + protected override IControl CreateContainer(object item, IDataTemplate itemTemplate) { T result = item as T; diff --git a/Perspex.Controls/ItemsControl.cs b/Perspex.Controls/ItemsControl.cs index eb5c4801a4..340c5cca45 100644 --- a/Perspex.Controls/ItemsControl.cs +++ b/Perspex.Controls/ItemsControl.cs @@ -77,29 +77,29 @@ namespace Perspex.Controls set { this.SetValue(ItemsPanelProperty, value); } } - IPerspexReadOnlyList ILogical.LogicalChildren - { - get - { - this.ApplyTemplate(); - return this.logicalChildren; - } - } - - protected IItemsPresenter Presenter + public IItemsPresenter Presenter { get { return this.presenter; } - set + protected set { this.presenter = value; this.logicalChildren.Source = ((IVisual)value?.Panel)?.VisualChildren; } } + IPerspexReadOnlyList ILogical.LogicalChildren + { + get + { + this.ApplyTemplate(); + return this.logicalChildren; + } + } + protected virtual ItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator(this); diff --git a/Perspex.Controls/Menu.cs b/Perspex.Controls/Menu.cs index b8e8d91047..83c054c6d3 100644 --- a/Perspex.Controls/Menu.cs +++ b/Perspex.Controls/Menu.cs @@ -63,8 +63,9 @@ namespace Perspex.Controls { get { - return (this.SelectedItem != null) ? - (MenuItem)this.ItemContainerGenerator.GetContainerForItem(this.SelectedItem) : + var index = this.SelectedIndex; + return (index != -1) ? + (MenuItem)this.ItemContainerGenerator.ContainerFromIndex(index) : null; } } diff --git a/Perspex.Controls/MenuItem.cs b/Perspex.Controls/MenuItem.cs index d1f901aefc..e539c1dea3 100644 --- a/Perspex.Controls/MenuItem.cs +++ b/Perspex.Controls/MenuItem.cs @@ -476,11 +476,11 @@ namespace Perspex.Controls /// The event args. private void PopupOpened(object sender, EventArgs e) { - var selected = this.SelectedItem; + var selected = this.SelectedIndex; - if (selected != null) + if (selected != -1) { - var container = this.ItemContainerGenerator.GetContainerForItem(selected); + var container = this.ItemContainerGenerator.ContainerFromIndex(selected); if (container != null) { diff --git a/Perspex.Controls/Perspex.Controls.csproj b/Perspex.Controls/Perspex.Controls.csproj index db1499a57d..8dd68a6c89 100644 --- a/Perspex.Controls/Perspex.Controls.csproj +++ b/Perspex.Controls/Perspex.Controls.csproj @@ -36,6 +36,7 @@ 4 + diff --git a/Perspex.Controls/Presenters/DeckPresenter.cs b/Perspex.Controls/Presenters/DeckPresenter.cs index c86f9d698b..c445035845 100644 --- a/Perspex.Controls/Presenters/DeckPresenter.cs +++ b/Perspex.Controls/Presenters/DeckPresenter.cs @@ -102,7 +102,11 @@ namespace Perspex.Controls.Presenters ((IItemsPanel)this.Panel).ChildLogicalParent = this.TemplatedParent as ILogical; this.AddVisualChild(this.Panel); this.createdPanel = true; - this.SelectedItemChanged(Tuple.Create(null, this.SelectedItem)); + + if (this.SelectedItem != null) + { + this.SelectedItemChanged(Tuple.Create(null, this.SelectedItem)); + } } private IItemContainerGenerator GetGenerator() @@ -121,37 +125,37 @@ namespace Perspex.Controls.Presenters if (this.createdPanel) { var generator = this.GetGenerator(); - Control from = null; - Control to = null; + IControl from = null; + IControl to = null; int fromIndex = -1; int toIndex = -1; if (value.Item1 != null) { - from = generator.GetContainerForItem(value.Item1); fromIndex = this.Items.IndexOf(value.Item1); + from = generator.ContainerFromIndex(fromIndex); } if (value.Item2 != null) { - to = generator.Generate(new[] { value.Item2 }).FirstOrDefault(); + toIndex = this.Items.IndexOf(value.Item2); + to = generator.CreateContainers(toIndex, new[] { value.Item2 }, null).FirstOrDefault(); if (to != null) { this.Panel.Children.Add(to); - toIndex = this.Items.IndexOf(value.Item2); } } if (this.Transition != null) { - await this.Transition.Start(from, to, fromIndex < toIndex); + await this.Transition.Start((Visual)from, (Visual)to, fromIndex < toIndex); } if (from != null) { this.Panel.Children.Remove(from); - generator.Remove(new[] { value.Item1 }); + generator.RemoveContainers(fromIndex, 1); } } } diff --git a/Perspex.Controls/Presenters/ItemsPresenter.cs b/Perspex.Controls/Presenters/ItemsPresenter.cs index d57c7aa9c1..445568fb95 100644 --- a/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -147,7 +147,8 @@ namespace Perspex.Controls.Presenters { if (items != null) { - this.Panel.Children.AddRange(this.ItemContainerGenerator.Generate(this.Items)); + this.Panel.Children.AddRange( + this.ItemContainerGenerator.CreateContainers(0, this.Items, null)); INotifyCollectionChanged incc = items as INotifyCollectionChanged; @@ -170,7 +171,7 @@ namespace Perspex.Controls.Presenters if (e.OldValue != null) { - generator.RemoveAll(); + generator.ClearContainers(); this.Panel.Children.Clear(); INotifyCollectionChanged incc = e.OldValue as INotifyCollectionChanged; @@ -203,11 +204,13 @@ namespace Perspex.Controls.Presenters switch (e.Action) { case NotifyCollectionChangedAction.Add: - this.Panel.Children.AddRange(generator.Generate(e.NewItems)); + this.Panel.Children.AddRange( + generator.CreateContainers(e.NewStartingIndex, e.NewItems, null)); break; case NotifyCollectionChangedAction.Remove: - this.Panel.Children.RemoveAll(generator.Remove(e.OldItems)); + this.Panel.Children.RemoveAll( + generator.RemoveContainers(e.OldStartingIndex, e.OldItems.Count)); break; } diff --git a/Perspex.Controls/Primitives/SelectingItemsControl.cs b/Perspex.Controls/Primitives/SelectingItemsControl.cs index 26100da8f2..8f2b8e1363 100644 --- a/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -10,9 +10,10 @@ namespace Perspex.Controls.Primitives using System.Collections; using System.Collections.Specialized; using System.Linq; - using Perspex.Controls.Utils; + using Perspex.Controls.Generators; using Perspex.Input; using Perspex.Interactivity; + using Perspex.Styling; using Perspex.VisualTree; /// @@ -53,9 +54,17 @@ namespace Perspex.Controls.Primitives /// static SelectingItemsControl() { - IsSelectedChangedEvent.AddClassHandler(x => x.ItemIsSelectedChanged); - SelectedIndexProperty.Changed.Subscribe(SelectedIndexChanged); - SelectedItemProperty.Changed.Subscribe(SelectedItemChanged); + IsSelectedChangedEvent.AddClassHandler(x => x.ContainerSelectionChanged); + SelectedIndexProperty.Changed.AddClassHandler(x => x.SelectedIndexChanged); + SelectedItemProperty.Changed.AddClassHandler(x => x.SelectedItemChanged); + } + + /// + /// Initializes a new instance of the class. + /// + public SelectingItemsControl() + { + this.ItemContainerGenerator.ContainersInitialized.Subscribe(this.ContainersInitialized); } /// @@ -76,258 +85,186 @@ namespace Perspex.Controls.Primitives set { this.SetValue(SelectedItemProperty, value); } } - /// - /// Called when the property changes. - /// - /// The old value of the property. - /// The new value of the property. + /// protected override void ItemsChanged(IEnumerable oldValue, IEnumerable newValue) { base.ItemsChanged(oldValue, newValue); - - var selected = this.SelectedItem; - - if (selected != null) - { - if (newValue == null || !newValue.Contains(selected)) - { - this.SelectedItem = null; - } - } + this.SelectedIndex = IndexOf(newValue, this.SelectedItem); } - /// - /// Called when a event is raised - /// on . - /// - /// The event sender. - /// The event args. + /// protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { base.ItemsCollectionChanged(sender, e); - var selected = this.SelectedItem; - switch (e.Action) { case NotifyCollectionChangedAction.Remove: - case NotifyCollectionChangedAction.Reset: - if (e.OldItems.Contains(selected)) + case NotifyCollectionChangedAction.Replace: + var selectedIndex = this.SelectedIndex; + + if (selectedIndex >= e.OldStartingIndex && + selectedIndex < e.OldStartingIndex + e.OldItems.Count) { - this.SelectedItem = null; + this.SelectedIndex = -1; } break; - case NotifyCollectionChangedAction.Move: - this.SelectedItem = this.Items.IndexOf(selected); + + case NotifyCollectionChangedAction.Reset: + this.SelectedIndex = IndexOf(e.NewItems, this.SelectedItem); break; } } - /// - /// Called when the selection on a child item changes. - /// - /// The event args. - protected virtual void ItemIsSelectedChanged(RoutedEventArgs e) + /// + protected override void OnGotFocus(GotFocusEventArgs e) { - var selectable = e.Source as ISelectable; - - if (selectable != null && selectable != this && selectable.IsSelected) - { - var container = this.ItemContainerGenerator.GetItemForContainer((Control)selectable); - - if (container != null) - { - this.SelectedItem = container; - e.Handled = true; - } - } + base.OnGotFocus(e); + this.TrySetSelectionFromContainerEvent(e.Source); } /// - /// Moves the selection in the specified direction. + /// Gets the index of an item in a collection. /// - /// The direction. - protected virtual void MoveSelection(FocusNavigationDirection direction) + /// The collection. + /// The item. + /// The index of the item or -1 if the item was not found. + private static int IndexOf(IEnumerable items, object item) { - var panel = this.Presenter?.Panel as INavigablePanel; - var selected = this.SelectedItem; - var container = selected != null ? - this.ItemContainerGenerator.GetContainerForItem(selected) : - null; - - if (panel != null) + if (items != null && item != null) { - var next = panel.GetControl(direction, container); - - if (next != null) - { - this.SelectedItem = this.ItemContainerGenerator.GetItemForContainer((Control)next); - } - } - else - { - // TODO: Try doing a visual search? - } - } + var list = items as IList; - /// - /// Called when a key is pressed within the control. - /// - /// The event args. - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - - if (!e.Handled) - { - switch (e.Key) + if (list != null) { - case Key.Up: - this.MoveSelection(FocusNavigationDirection.Up); - break; - case Key.Down: - this.MoveSelection(FocusNavigationDirection.Down); - break; - case Key.Left: - this.MoveSelection(FocusNavigationDirection.Left); - break; - case Key.Right: - this.MoveSelection(FocusNavigationDirection.Right); - break; - default: - return; + return list.IndexOf(item); } - - var selected = this.SelectedItem; - - if (selected != null) + else { - var container = this.ItemContainerGenerator.GetContainerForItem(selected); + int index = 0; - if (container != null) + foreach (var i in items) { - container.BringIntoView(); - FocusManager.Instance.Focus(container, true); + if (object.Equals(i, item)) + { + return index; + } + + ++index; } } - - e.Handled = true; } + + return -1; } /// - /// Called when the pointer is pressed within the control. + /// Sets a container's 'selected' class or . /// - /// The event args. - protected override void OnPointerPressed(PointerPressEventArgs e) + /// The container. + /// Whether the control is selected + private static void MarkContainerSelected(IControl container, bool selected) { - IVisual source = (IVisual)e.Source; - var selectable = source.GetVisualAncestors() - .OfType() - .OfType() - .FirstOrDefault(); + var selectable = container as ISelectable; + var styleable = container as IStyleable; if (selectable != null) { - var item = this.ItemContainerGenerator.GetItemForContainer(selectable); - - if (item != null) + selectable.IsSelected = selected; + } + else if (styleable != null) + { + if (selected) { - this.SelectedItem = item; - selectable.BringIntoView(); - FocusManager.Instance.Focus(selectable); + styleable.Classes.Add("selected"); + } + else + { + styleable.Classes.Remove("selected"); } } - - e.Handled = true; } /// - /// Called when the control's template has been applied. + /// Coerces the property. /// - protected override void OnTemplateApplied() + /// The object with the property. + /// The proposed value of the property. + /// The final value of the property. + private static int ValidateSelectedIndex(SelectingItemsControl sender, int index) { - base.OnTemplateApplied(); - this.SelectedItemChanged(this.SelectedItem); + var items = sender.Items; + return (index >= 0 && index < items?.Cast().Count()) ? index : -1; } /// - /// Provides coercion for the property. + /// Coerces the property. /// - /// The object on which the property has changed. - /// The proposed value. - /// The coerced value. - private static int ValidateSelectedIndex(PerspexObject o, int value) + /// The object with the property. + /// The proposed value of the property. + /// The final value of the property. + private static object ValidateSelectedItem(SelectingItemsControl sender, object item) { - var control = o as SelectingItemsControl; - - if (control != null) - { - if (value < -1) - { - return -1; - } - else if (value > -1) - { - var items = control.Items; - - if (items != null) - { - var count = items.Count(); - return Math.Min(value, count - 1); - } - else - { - return -1; - } - } - } - - return value; + var items = sender.Items; + return items?.Cast().Contains(item) == true ? item : null; } /// - /// Provides coercion for the property. + /// Called when new containers are initialized by the . /// - /// The object on which the property has changed. - /// The proposed value. - /// The coerced value. - private static object ValidateSelectedItem(PerspexObject o, object value) + /// The containers. + private void ContainersInitialized(ItemContainers containers) { - var control = o as SelectingItemsControl; + var selectedIndex = this.SelectedIndex; + var selectedContainer = containers.Items.OfType().FirstOrDefault(x => x.IsSelected); - if (control != null) + if (selectedContainer != null) { - if (value != null && (control.Items == null || control.Items.IndexOf(value) == -1)) - { - return null; - } + this.SelectedIndex = containers.Items.IndexOf((IControl)selectedContainer) + containers.StartingIndex; + } + else if (selectedIndex >= containers.StartingIndex && + selectedIndex < containers.StartingIndex + containers.Items.Count) + { + var container = containers.Items[selectedIndex - containers.StartingIndex]; + MarkContainerSelected(container, true); } + } - return value; + /// + /// Called when a container raises the . + /// + /// The event. + private void ContainerSelectionChanged(RoutedEventArgs e) + { + this.TrySetSelectionFromContainerEvent(e.Source); } /// /// Called when the property changes. /// /// The event args. - private static void SelectedIndexChanged(PerspexPropertyChangedEventArgs e) + private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e) { - var control = e.Sender as SelectingItemsControl; + var index = (int)e.OldValue; - if (control != null) + if (index != -1) { - var index = (int)e.NewValue; + var container = this.ItemContainerGenerator.ContainerFromIndex(index); + MarkContainerSelected(container, false); + } - if (index == -1) - { - control.SelectedItem = null; - } - else - { - control.SelectedItem = control.Items.ElementAt((int)e.NewValue); - } + index = (int)e.NewValue; + + if (index == -1) + { + this.SelectedItem = null; + } + else + { + this.SelectedItem = this.Items.Cast().ElementAt((int)e.NewValue); + var container = this.ItemContainerGenerator.ContainerFromIndex(index); + MarkContainerSelected(container, true); } } @@ -335,50 +272,28 @@ namespace Perspex.Controls.Primitives /// Called when the property changes. /// /// The event args. - private static void SelectedItemChanged(PerspexPropertyChangedEventArgs e) + private void SelectedItemChanged(PerspexPropertyChangedEventArgs e) { - var control = e.Sender as SelectingItemsControl; - - if (control != null) - { - control.SelectedItemChanged(e.NewValue); - } + this.SelectedIndex = IndexOf(this.Items, e.NewValue); } /// - /// Called when the property changes. + /// Tries to set the selection to a container that raised an event. /// - /// The new selected item. - private void SelectedItemChanged(object selected) + /// The control that raised the event. + private void TrySetSelectionFromContainerEvent(IInteractive eventSource) { - var containers = this.ItemContainerGenerator.GetAll() - .Select(x => x.Item2) - .OfType(); - var selectedContainer = (selected != null) ? - this.ItemContainerGenerator.GetContainerForItem(selected) : - null; - - if (this.Presenter != null && this.Presenter.Panel != null) - { - KeyboardNavigation.SetTabOnceActiveElement(this.Presenter.Panel, selectedContainer); - } + var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(x => x.LogicalParent == this); - foreach (var item in containers) - { - item.IsSelected = item == selectedContainer; - } - - if (selected == null) - { - this.SelectedIndex = -1; - } - else + if (item != null) { - var items = this.Items; + var index = this.ItemContainerGenerator.IndexFromContainer((IControl)item); - if (items != null) + if (index != -1) { - this.SelectedIndex = items.IndexOf(selected); + this.SelectedIndex = index; } } } diff --git a/Perspex.Controls/Primitives/TabStrip.cs b/Perspex.Controls/Primitives/TabStrip.cs index 13dcfac641..9d26214caa 100644 --- a/Perspex.Controls/Primitives/TabStrip.cs +++ b/Perspex.Controls/Primitives/TabStrip.cs @@ -47,22 +47,22 @@ namespace Perspex.Controls.Primitives result = new TypedItemContainerGenerator(this); } - result.StateChanged += this.ItemsContainerGeneratorStateChanged; + ////result.StateChanged += this.ItemsContainerGeneratorStateChanged; return result; } private void ItemsContainerGeneratorStateChanged(object sender, EventArgs e) { - if (this.ItemContainerGenerator.State == ItemContainerGeneratorState.Generated) - { - var tabs = this.ItemContainerGenerator.GetAll() - .Select(x => x.Item2) - .OfType() - .ToList(); + ////if (this.ItemContainerGenerator.State == ItemContainerGeneratorState.Generated) + ////{ + //// var tabs = this.ItemContainerGenerator.GetAll() + //// .Select(x => x.Item2) + //// .OfType() + //// .ToList(); - this.SelectedItem = tabs.FirstOrDefault(x => x.IsSelected) ?? tabs.FirstOrDefault(); - } + //// this.SelectedItem = tabs.FirstOrDefault(x => x.IsSelected) ?? tabs.FirstOrDefault(); + ////} } } } diff --git a/Perspex.Controls/Templates/DataTemplateExtensions.cs b/Perspex.Controls/Templates/DataTemplateExtensions.cs index 5f352ec3a9..d7f7a09450 100644 --- a/Perspex.Controls/Templates/DataTemplateExtensions.cs +++ b/Perspex.Controls/Templates/DataTemplateExtensions.cs @@ -1,6 +1,6 @@ // ----------------------------------------------------------------------- // -// Copyright 2014 MIT Licence. See licence.md for more information. +// Copyright 2015 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- @@ -10,9 +10,18 @@ namespace Perspex.Controls.Templates using Perspex.LogicalTree; using Splat; + /// + /// Defines extension methods for working with s. + /// public static class DataTemplateExtensions { - public static IControl MaterializeDataTemplate(this Control control, object data) + /// + /// Materializes a piece of data based on a data template. + /// + /// The control materializing the data template. + /// The data. + /// The data materialized as a control. + public static IControl MaterializeDataTemplate(this IControl control, object data) { IDataTemplate template = control.FindDataTemplate(data); IControl result; @@ -26,9 +35,9 @@ namespace Perspex.Controls.Templates result.DataContext = data; } } - else if (data is Control) + else if (data is IControl) { - result = (Control)data; + result = (IControl)data; } else { @@ -38,9 +47,15 @@ namespace Perspex.Controls.Templates return result; } - public static IDataTemplate FindDataTemplate(this Control control, object data) + /// + /// Find a data template that matches a piece of data. + /// + /// The control searching for the data template. + /// The data. + /// The data template or null if no matching data template was found. + public static IDataTemplate FindDataTemplate(this IControl control, object data) { - foreach (var i in control.GetSelfAndLogicalAncestors().OfType()) + foreach (var i in control.GetSelfAndLogicalAncestors().OfType()) { foreach (IDataTemplate dt in i.DataTemplates.Reverse()) { diff --git a/Perspex.Controls/TreeView.cs b/Perspex.Controls/TreeView.cs index 4b1a068c07..7fdb9066d2 100644 --- a/Perspex.Controls/TreeView.cs +++ b/Perspex.Controls/TreeView.cs @@ -47,115 +47,115 @@ namespace Perspex.Controls { // TODO: Up and down movement is a *HACK* and probably pretty slow. Probably needs // rewriting at some point. - if (this.SelectedItem != null) - { - switch (direction) - { - case FocusNavigationDirection.Up: - { - var list = this.Flatten(); - var index = list.IndexOf(this.SelectedItem); - - if (index > 0) - { - this.SelectedItem = list[index - 1]; - } - - break; - } - - case FocusNavigationDirection.Down: - { - var list = this.Flatten(); - var index = list.IndexOf(this.SelectedItem); - - if (index + 1 < list.Count) - { - this.SelectedItem = list[index + 1]; - } - - break; - } - - case FocusNavigationDirection.Left: - { - var node = (TreeViewItem)this.ItemContainerGenerator.GetContainerForItem(this.SelectedItem); - node.IsExpanded = false; - break; - } - - case FocusNavigationDirection.Right: - { - var node = (TreeViewItem)this.ItemContainerGenerator.GetContainerForItem(this.SelectedItem); - node.IsExpanded = true; - break; - } - } - } + ////if (this.SelectedItem != null) + ////{ + //// switch (direction) + //// { + //// case FocusNavigationDirection.Up: + //// { + //// var list = this.Flatten(); + //// var index = list.IndexOf(this.SelectedItem); + + //// if (index > 0) + //// { + //// this.SelectedItem = list[index - 1]; + //// } + + //// break; + //// } + + //// case FocusNavigationDirection.Down: + //// { + //// var list = this.Flatten(); + //// var index = list.IndexOf(this.SelectedItem); + + //// if (index + 1 < list.Count) + //// { + //// this.SelectedItem = list[index + 1]; + //// } + + //// break; + //// } + + //// case FocusNavigationDirection.Left: + //// { + //// var node = (TreeViewItem)this.ItemContainerGenerator.GetContainerForItem(this.SelectedItem); + //// node.IsExpanded = false; + //// break; + //// } + + //// case FocusNavigationDirection.Right: + //// { + //// var node = (TreeViewItem)this.ItemContainerGenerator.GetContainerForItem(this.SelectedItem); + //// node.IsExpanded = true; + //// break; + //// } + //// } + ////} } protected override void OnKeyDown(KeyEventArgs e) { - base.OnKeyDown(e); - - if (!e.Handled) - { - switch (e.Key) - { - case Key.Up: - this.MoveSelection(FocusNavigationDirection.Up); - break; - case Key.Down: - this.MoveSelection(FocusNavigationDirection.Down); - break; - case Key.Left: - this.MoveSelection(FocusNavigationDirection.Left); - break; - case Key.Right: - this.MoveSelection(FocusNavigationDirection.Right); - break; - default: - return; - } - - var selected = this.SelectedItem; - - if (selected != null) - { - var container = this.ItemContainerGenerator.GetContainerForItem(selected); - - if (container != null) - { - container.BringIntoView(); - FocusManager.Instance.Focus(container, true); - } - } - - e.Handled = true; - } + ////base.OnKeyDown(e); + + ////if (!e.Handled) + ////{ + //// switch (e.Key) + //// { + //// case Key.Up: + //// this.MoveSelection(FocusNavigationDirection.Up); + //// break; + //// case Key.Down: + //// this.MoveSelection(FocusNavigationDirection.Down); + //// break; + //// case Key.Left: + //// this.MoveSelection(FocusNavigationDirection.Left); + //// break; + //// case Key.Right: + //// this.MoveSelection(FocusNavigationDirection.Right); + //// break; + //// default: + //// return; + //// } + + //// var selected = this.SelectedItem; + + //// if (selected != null) + //// { + //// var container = this.ItemContainerGenerator.GetContainerForItem(selected); + + //// if (container != null) + //// { + //// container.BringIntoView(); + //// FocusManager.Instance.Focus(container, true); + //// } + //// } + + //// e.Handled = true; + ////} } protected override void OnPointerPressed(PointerPressEventArgs e) { - IVisual source = (IVisual)e.Source; - var selectable = source.GetVisualAncestors() - .OfType() - .OfType() - .FirstOrDefault(); - - if (selectable != null) - { - var item = this.ItemContainerGenerator.GetItemForContainer(selectable); - - if (item != null) - { - this.SelectedItem = item; - selectable.BringIntoView(); - FocusManager.Instance.Focus(selectable); - } - } - - e.Handled = true; + ////IVisual source = (IVisual)e.Source; + ////var selectable = source.GetVisualAncestors() + //// .OfType() + //// .OfType() + //// .FirstOrDefault(); + + ////if (selectable != null) + ////{ + //// var item = this.ItemContainerGenerator.GetItemForContainer(selectable); + + //// if (item != null) + //// { + //// this.SelectedItem = item; + //// selectable.BringIntoView(); + //// FocusManager.Instance.Focus(selectable); + //// } + ////} + + ////e.Handled = true; } private static object ValidateSelectedItem(PerspexObject o, object value) @@ -182,39 +182,39 @@ namespace Perspex.Controls private void Flatten(IEnumerable items, List result) { - if (items != null) - { - foreach (object item in items) - { - var container = (TreeViewItem)this.ItemContainerGenerator.GetContainerForItem(item); - result.Add(item); - - if (container.IsExpanded) - { - this.Flatten(container.Items, result); - } - } - } + ////if (items != null) + ////{ + //// foreach (object item in items) + //// { + //// var container = (TreeViewItem)this.ItemContainerGenerator.GetContainerForItem(item); + //// result.Add(item); + + //// if (container.IsExpanded) + //// { + //// this.Flatten(container.Items, result); + //// } + //// } + ////} } private void SelectedItemChanged(object selected) { - var containers = this.ItemContainerGenerator.GetAll() - .Select(x => x.Item2) - .OfType(); - var selectedContainer = (selected != null) ? - this.ItemContainerGenerator.GetContainerForItem(selected) : - null; - - if (this.Presenter != null && this.Presenter.Panel != null) - { - KeyboardNavigation.SetTabOnceActiveElement(this.Presenter.Panel, selectedContainer); - } - - foreach (var item in containers) - { - item.IsSelected = item == selectedContainer; - } + ////var containers = this.ItemContainerGenerator.GetAll() + //// .Select(x => x.Item2) + //// .OfType(); + ////var selectedContainer = (selected != null) ? + //// this.ItemContainerGenerator.GetContainerForItem(selected) : + //// null; + + ////if (this.Presenter != null && this.Presenter.Panel != null) + ////{ + //// KeyboardNavigation.SetTabOnceActiveElement(this.Presenter.Panel, selectedContainer); + ////} + + ////foreach (var item in containers) + ////{ + //// item.IsSelected = item == selectedContainer; + ////} } } } diff --git a/Tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/Tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index e1b811c521..d6316f051f 100644 --- a/Tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/Tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -91,10 +91,14 @@ namespace Perspex.Controls.UnitTests.Presenters }; target.ApplyTemplate(); - items.RemoveAt(2); var text = target.Panel.Children.OfType().Select(x => x.Text).ToList(); Assert.Equal(new[] { "foo", "bar" }, text); + + items.RemoveAt(1); + + text = target.Panel.Children.OfType().Select(x => x.Text).ToList(); + Assert.Equal(new[] { "foo", "bar" }, text); } [Fact] diff --git a/Tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/Tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index dbbba6df6c..75f2932638 100644 --- a/Tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/Tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -6,6 +6,7 @@ namespace Perspex.Controls.UnitTests.Primitives { + using System.Collections.ObjectModel; using Perspex.Collections; using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; @@ -192,7 +193,7 @@ namespace Perspex.Controls.UnitTests.Primitives } [Fact] - public void Setting_SelectedIndex_Should_Coerce() + public void Setting_SelectedIndex_Out_Of_Bounds_Should_Clear_Selection() { var items = new[] { @@ -209,11 +210,11 @@ namespace Perspex.Controls.UnitTests.Primitives target.ApplyTemplate(); target.SelectedIndex = 2; - Assert.Equal(1, target.SelectedIndex); + Assert.Equal(-1, target.SelectedIndex); } [Fact] - public void Setting_SelectedIndex_With_No_Items_Should_Not_Throw_Exception() + public void Setting_SelectedItem_To_Non_Existent_Item_Should_Clear_Selection() { var target = new Target { @@ -221,25 +222,36 @@ namespace Perspex.Controls.UnitTests.Primitives }; target.ApplyTemplate(); - target.SelectedIndex = 2; + target.SelectedItem = new Item(); Assert.Equal(-1, target.SelectedIndex); + Assert.Null(target.SelectedItem); } [Fact] - public void Setting_SelectedItem_With_No_Items_Should_Not_Throw_Exception() + public void Adding_Selected_Item_Should_Update_Selection() { + var items = new PerspexList(new[] + { + new Item(), + new Item(), + }); + var target = new Target { + Items = items, Template = this.Template(), }; target.ApplyTemplate(); - target.SelectedItem = new Item(); + items.Add(new Item { IsSelected = true }); + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal(items[2], target.SelectedItem); } [Fact] - public void Clearing_Items_Should_Clear_Selection() + public void Setting_Items_To_Null_Should_Clear_Selection() { var items = new PerspexList { @@ -293,18 +305,54 @@ namespace Perspex.Controls.UnitTests.Primitives } [Fact] - public void PointerPressed_Event_Should_Be_Handled() + public void Resetting_Items_Collection_Should_Clear_Selection() { - var target = new Target(); + // Need to use ObservableCollection here as PerspexList signals a Clear as an + // add + remove. + var items = new ObservableCollection + { + new Item(), + new Item(), + }; - var e = new PointerPressEventArgs + var target = new Target { - RoutedEvent = InputElement.PointerPressedEvent + Items = items, + Template = this.Template(), }; - target.RaiseEvent(e); + target.ApplyTemplate(); + target.SelectedIndex = 1; - Assert.True(e.Handled); + Assert.Equal(items[1], target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + + items.Clear(); + + Assert.Equal(null, target.SelectedItem); + Assert.Equal(-1, target.SelectedIndex); + } + + [Fact] + public void Focusing_Item_Should_Select_It() + { + var target = new Target + { + Template = this.Template(), + Items = new[] { "foo", "bar" }, + }; + + target.ApplyTemplate(); + + var e = new GotFocusEventArgs + { + RoutedEvent = InputElement.GotFocusEvent + }; + + target.Presenter.Panel.Children[1].RaiseEvent(e); + + Assert.Equal(1, target.SelectedIndex); + Assert.False(e.Handled); } [Fact]