From 336e1c4ae246ea94835ca19df7d5d34758b16a65 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 19 Dec 2014 15:00:36 +0100 Subject: [PATCH 01/11] Implemented ILogical. --- Perspex.Animation/PropertyTransitions.cs | 2 + Perspex.Base/Collections/IPerspexList.cs | 15 +++ .../{ => Collections}/IReadOnlyPerspexList.cs | 4 +- Perspex.Base/{ => Collections}/PerspexList.cs | 14 ++- .../PerspexListExtensions.cs | 2 +- .../Collections/SingleItemPerspexList.cs | 113 +++++++++++++++++ Perspex.Base/Perspex.Base.csproj | 8 +- .../ContentControlTests.cs | 99 +++++++++++++++ Perspex.Controls.UnitTests/DecoratorTests.cs | 114 ++++++++++++++++++ Perspex.Controls.UnitTests/PanelTests.cs | 82 +++++++++++++ .../Perspex.Controls.UnitTests.csproj | 2 + Perspex.Controls/ColumnDefinitions.cs | 2 + Perspex.Controls/ContentControl.cs | 32 ++++- Perspex.Controls/Control.cs | 25 +++- Perspex.Controls/Controls.cs | 1 + Perspex.Controls/DataTemplates.cs | 1 + Perspex.Controls/Decorator.cs | 25 +++- Perspex.Controls/Grid.cs | 1 + Perspex.Controls/ILogical.cs | 27 +++++ Perspex.Controls/Panel.cs | 20 ++- Perspex.Controls/Perspex.Controls.csproj | 1 + Perspex.Controls/RowDefinitions.cs | 2 + Perspex.SceneGraph/IVisual.cs | 1 + Perspex.SceneGraph/Visual.cs | 1 + Perspex.Styling/Styles.cs | 2 + TestApplication/Program.cs | 1 + 26 files changed, 580 insertions(+), 17 deletions(-) create mode 100644 Perspex.Base/Collections/IPerspexList.cs rename Perspex.Base/{ => Collections}/IReadOnlyPerspexList.cs (74%) rename Perspex.Base/{ => Collections}/PerspexList.cs (91%) rename Perspex.Base/{ => Collections}/PerspexListExtensions.cs (99%) create mode 100644 Perspex.Base/Collections/SingleItemPerspexList.cs create mode 100644 Perspex.Controls.UnitTests/DecoratorTests.cs create mode 100644 Perspex.Controls.UnitTests/PanelTests.cs create mode 100644 Perspex.Controls/ILogical.cs diff --git a/Perspex.Animation/PropertyTransitions.cs b/Perspex.Animation/PropertyTransitions.cs index 9b578fc942..1bb91b18f1 100644 --- a/Perspex.Animation/PropertyTransitions.cs +++ b/Perspex.Animation/PropertyTransitions.cs @@ -4,6 +4,8 @@ // // ----------------------------------------------------------------------- +using Perspex.Collections; + namespace Perspex.Animation { public class PropertyTransitions : PerspexList diff --git a/Perspex.Base/Collections/IPerspexList.cs b/Perspex.Base/Collections/IPerspexList.cs new file mode 100644 index 0000000000..6d958f07fb --- /dev/null +++ b/Perspex.Base/Collections/IPerspexList.cs @@ -0,0 +1,15 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Collections +{ + using System.Collections; + using System.Collections.Generic; + + public interface IPerspexList : IList, IList, IReadOnlyPerspexList + { + } +} \ No newline at end of file diff --git a/Perspex.Base/IReadOnlyPerspexList.cs b/Perspex.Base/Collections/IReadOnlyPerspexList.cs similarity index 74% rename from Perspex.Base/IReadOnlyPerspexList.cs rename to Perspex.Base/Collections/IReadOnlyPerspexList.cs index 87b7074bd2..7329082f9f 100644 --- a/Perspex.Base/IReadOnlyPerspexList.cs +++ b/Perspex.Base/Collections/IReadOnlyPerspexList.cs @@ -4,13 +4,13 @@ // // ----------------------------------------------------------------------- -namespace Perspex +namespace Perspex.Collections { using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; - public interface IReadOnlyPerspexList : IReadOnlyList, INotifyCollectionChanged, INotifyPropertyChanged + public interface IReadOnlyPerspexList : IReadOnlyList, INotifyCollectionChanged, INotifyPropertyChanged { } } \ No newline at end of file diff --git a/Perspex.Base/PerspexList.cs b/Perspex.Base/Collections/PerspexList.cs similarity index 91% rename from Perspex.Base/PerspexList.cs rename to Perspex.Base/Collections/PerspexList.cs index 13fa97801c..b0775bd4a9 100644 --- a/Perspex.Base/PerspexList.cs +++ b/Perspex.Base/Collections/PerspexList.cs @@ -4,7 +4,7 @@ // // ----------------------------------------------------------------------- -namespace Perspex +namespace Perspex.Collections { using System; using System.Collections; @@ -13,7 +13,17 @@ namespace Perspex using System.ComponentModel; using System.Linq; - public class PerspexList : IList, IList, IReadOnlyPerspexList, INotifyCollectionChanged, INotifyPropertyChanged + /// + /// A notifying list. + /// + /// The type of the list items. + /// + /// PerspexList is similar to + /// except that when the method is called, it notifies with a + /// action, passing the items that were + /// removed. + /// + public class PerspexList : IPerspexList, INotifyCollectionChanged, INotifyPropertyChanged { private List inner; diff --git a/Perspex.Base/PerspexListExtensions.cs b/Perspex.Base/Collections/PerspexListExtensions.cs similarity index 99% rename from Perspex.Base/PerspexListExtensions.cs rename to Perspex.Base/Collections/PerspexListExtensions.cs index 13ee014d49..62c754f2b3 100644 --- a/Perspex.Base/PerspexListExtensions.cs +++ b/Perspex.Base/Collections/PerspexListExtensions.cs @@ -4,7 +4,7 @@ // // ----------------------------------------------------------------------- -namespace Perspex +namespace Perspex.Collections { using System; using System.Collections.Generic; diff --git a/Perspex.Base/Collections/SingleItemPerspexList.cs b/Perspex.Base/Collections/SingleItemPerspexList.cs new file mode 100644 index 0000000000..96a629d556 --- /dev/null +++ b/Perspex.Base/Collections/SingleItemPerspexList.cs @@ -0,0 +1,113 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Collections +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; + using System.Linq; + + /// + /// Implements the interface for single items. + /// + /// The type of the single item. + /// + /// Classes such as Border can only ever have a single logical child, but they need to + /// implement a list of logical children in their ILogical.LogicalChildren property using the + /// interface. This class facilitates that + /// without creating an actual . + /// + public class SingleItemPerspexList : IReadOnlyPerspexList where T : class + { + private T item; + + public SingleItemPerspexList() + { + } + + public SingleItemPerspexList(T item) + { + this.item = item; + } + + public T this[int index] + { + get + { + if (index < 0 || index >= this.Count) + { + throw new ArgumentOutOfRangeException(); + } + + return item; + } + } + + public int Count + { + get { return this.item != null ? 1 : 0; } + } + + public T SingleItem + { + get + { + return this.item; + } + + set + { + NotifyCollectionChangedEventArgs e = null; + bool countChanged = false; + + if (value == null && this.item != null ) + { + e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, this.item, 0); + this.item = null; + countChanged = true; + } + else if (value != null && this.item == null) + { + e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, this.item, 0); + this.item = value; + countChanged = true; + } + else + { + e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, this.item); + this.item = value; + } + + if (e != null && this.CollectionChanged != null) + { + this.CollectionChanged(this, e); + } + + if (countChanged && this.PropertyChanged != null) + { + this.PropertyChanged(this, new PropertyChangedEventArgs("Count")); + } + } + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + + public event PropertyChangedEventHandler PropertyChanged; + + public IEnumerator GetEnumerator() + { + return Enumerable.Repeat(this.item, this.Count).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/Perspex.Base/Perspex.Base.csproj b/Perspex.Base/Perspex.Base.csproj index 715391fbfa..70631febbb 100644 --- a/Perspex.Base/Perspex.Base.csproj +++ b/Perspex.Base/Perspex.Base.csproj @@ -35,13 +35,15 @@ + + + + + - - - diff --git a/Perspex.Controls.UnitTests/ContentControlTests.cs b/Perspex.Controls.UnitTests/ContentControlTests.cs index 67dc838d4f..c7d3351ec8 100644 --- a/Perspex.Controls.UnitTests/ContentControlTests.cs +++ b/Perspex.Controls.UnitTests/ContentControlTests.cs @@ -7,6 +7,7 @@ namespace Perspex.Controls.UnitTests { using System; + using System.Collections.Specialized; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -92,6 +93,104 @@ namespace Perspex.Controls.UnitTests Assert.IsNull(child.TemplatedParent); } + [TestMethod] + public void Setting_Content_Should_Set_Child_Controls_Parent() + { + var target = new ContentControl(); + var child = new Control(); + + target.Content = child; + + Assert.AreEqual(child.Parent, target); + Assert.AreEqual(((ILogical)child).LogicalParent, target); + } + + [TestMethod] + public void Clearing_Content_Should_Clear_Child_Controls_Parent() + { + var target = new ContentControl(); + var child = new Control(); + + target.Content = child; + target.Content = null; + + Assert.IsNull(child.Parent); + Assert.IsNull(((ILogical)child).LogicalParent); + } + + [TestMethod] + public void Setting_Content_To_Control_Should_Make_Control_Appear_In_LogicalChildren() + { + var target = new ContentControl(); + var child = new Control(); + + target.Content = child; + + CollectionAssert.AreEqual(new[] { child }, ((ILogical)target).LogicalChildren.ToList()); + } + + [TestMethod] + public void Clearing_Content_Should_Remove_From_LogicalChildren() + { + var contentControl = new ContentControl(); + var child = new Control(); + + contentControl.Content = child; + contentControl.Content = null; + + CollectionAssert.AreEqual(new ILogical[0], ((ILogical)contentControl).LogicalChildren.ToList()); + } + + [TestMethod] + public void Setting_Content_Should_Fire_LogicalChildren_CollectionChanged() + { + var contentControl = new ContentControl(); + var child = new Control(); + var called = false; + + ((ILogical)contentControl).LogicalChildren.CollectionChanged += (s, e) => + called = e.Action == NotifyCollectionChangedAction.Add; + + contentControl.Content = child; + + Assert.IsTrue(called); + } + + [TestMethod] + public void Clearing_Content_Should_Fire_LogicalChildren_CollectionChanged() + { + var contentControl = new ContentControl(); + var child = new Control(); + var called = false; + + contentControl.Content = child; + + ((ILogical)contentControl).LogicalChildren.CollectionChanged += (s, e) => + called = e.Action == NotifyCollectionChangedAction.Remove; + + contentControl.Content = null; + + Assert.IsTrue(called); + } + + [TestMethod] + public void Changing_Content_Should_Fire_LogicalChildren_CollectionChanged() + { + var contentControl = new ContentControl(); + var child1 = new Control(); + var child2 = new Control(); + var called = false; + + contentControl.Content = child1; + + ((ILogical)contentControl).LogicalChildren.CollectionChanged += (s, e) => + called = e.Action == NotifyCollectionChangedAction.Replace; + + contentControl.Content = child2; + + Assert.IsTrue(called); + } + private void ApplyTemplate(ILayoutable control) { control.Measure(new Size(100, 100)); diff --git a/Perspex.Controls.UnitTests/DecoratorTests.cs b/Perspex.Controls.UnitTests/DecoratorTests.cs new file mode 100644 index 0000000000..ae6a82d98e --- /dev/null +++ b/Perspex.Controls.UnitTests/DecoratorTests.cs @@ -0,0 +1,114 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2013 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls.UnitTests +{ + using System.Collections.Specialized; + using System.Linq; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class DecoratorTests + { + [TestMethod] + public void Setting_Content_Should_Set_Child_Controls_Parent() + { + var decorator = new Decorator(); + var child = new Control(); + + decorator.Content = child; + + Assert.AreEqual(child.Parent, decorator); + Assert.AreEqual(((ILogical)child).LogicalParent, decorator); + } + + [TestMethod] + public void Clearing_Content_Should_Clear_Child_Controls_Parent() + { + var decorator = new Decorator(); + var child = new Control(); + + decorator.Content = child; + decorator.Content = null; + + Assert.IsNull(child.Parent); + Assert.IsNull(((ILogical)child).LogicalParent); + } + + [TestMethod] + public void Content_Control_Should_Appear_In_LogicalChildren() + { + var decorator = new Decorator(); + var child = new Control(); + + decorator.Content = child; + + CollectionAssert.AreEqual(new[] { child }, ((ILogical)decorator).LogicalChildren.ToList()); + } + + [TestMethod] + public void Clearing_Content_Should_Remove_From_LogicalChildren() + { + var decorator = new Decorator(); + var child = new Control(); + + decorator.Content = child; + decorator.Content = null; + + CollectionAssert.AreEqual(new ILogical[0], ((ILogical)decorator).LogicalChildren.ToList()); + } + + [TestMethod] + public void Setting_Content_Should_Fire_LogicalChildren_CollectionChanged() + { + var decorator = new Decorator(); + var child = new Control(); + var called = false; + + ((ILogical)decorator).LogicalChildren.CollectionChanged += (s, e) => + called = e.Action == NotifyCollectionChangedAction.Add; + + decorator.Content = child; + + Assert.IsTrue(called); + } + + [TestMethod] + public void Clearing_Content_Should_Fire_LogicalChildren_CollectionChanged() + { + var decorator = new Decorator(); + var child = new Control(); + var called = false; + + decorator.Content = child; + + ((ILogical)decorator).LogicalChildren.CollectionChanged += (s, e) => + called = e.Action == NotifyCollectionChangedAction.Remove; + + decorator.Content = null; + + Assert.IsTrue(called); + } + + [TestMethod] + public void Changing_Content_Should_Fire_LogicalChildren_CollectionChanged() + { + var decorator = new Decorator(); + var child1 = new Control(); + var child2 = new Control(); + var called = false; + + decorator.Content = child1; + + ((ILogical)decorator).LogicalChildren.CollectionChanged += (s, e) => + called = e.Action == NotifyCollectionChangedAction.Replace; + + decorator.Content = child2; + + Assert.IsTrue(called); + } + } +} diff --git a/Perspex.Controls.UnitTests/PanelTests.cs b/Perspex.Controls.UnitTests/PanelTests.cs new file mode 100644 index 0000000000..f4976e9399 --- /dev/null +++ b/Perspex.Controls.UnitTests/PanelTests.cs @@ -0,0 +1,82 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2013 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls.UnitTests +{ + using System.Linq; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class PanelTests + { + [TestMethod] + public void Adding_Control_To_Panel_Should_Set_Child_Controls_Parent() + { + var panel = new Panel(); + var child = new Control(); + + panel.Children.Add(child); + + Assert.AreEqual(child.Parent, panel); + Assert.AreEqual(((ILogical)child).LogicalParent, panel); + } + + [TestMethod] + public void Removing_Control_From_Panel_Should_Clear_Child_Controls_Parent() + { + var panel = new Panel(); + var child = new Control(); + + panel.Children.Add(child); + panel.Children.Remove(child); + + Assert.IsNull(child.Parent); + Assert.IsNull(((ILogical)child).LogicalParent); + } + + [TestMethod] + public void Clearing_Panel_Children_Should_Clear_Child_Controls_Parent() + { + var panel = new Panel(); + var child1 = new Control(); + var child2 = new Control(); + + panel.Children.Add(child1); + panel.Children.Add(child2); + panel.Children.Clear(); + + Assert.IsNull(child1.Parent); + Assert.IsNull(((ILogical)child1).LogicalParent); + Assert.IsNull(child2.Parent); + Assert.IsNull(((ILogical)child2).LogicalParent); + } + + [TestMethod] + public void Child_Control_Should_Appear_In_Panel_Children() + { + var panel = new Panel(); + var child = new Control(); + + panel.Children.Add(child); + + CollectionAssert.AreEqual(new[] { child }, panel.Children); + CollectionAssert.AreEqual(new[] { child }, ((ILogical)panel).LogicalChildren.ToList()); + } + + [TestMethod] + public void Removing_Child_Control_Should_Remove_From_Panel_Children() + { + var panel = new Panel(); + var child = new Control(); + + panel.Children.Add(child); + panel.Children.Remove(child); + + CollectionAssert.AreEqual(new Control[0], panel.Children); + CollectionAssert.AreEqual(new ILogical[0], ((ILogical)panel).LogicalChildren.ToList()); + } + } +} diff --git a/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 4eea508951..c16687597e 100644 --- a/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -65,6 +65,8 @@ + + diff --git a/Perspex.Controls/ColumnDefinitions.cs b/Perspex.Controls/ColumnDefinitions.cs index 73656b8854..0dc5a1834a 100644 --- a/Perspex.Controls/ColumnDefinitions.cs +++ b/Perspex.Controls/ColumnDefinitions.cs @@ -4,6 +4,8 @@ // // ----------------------------------------------------------------------- +using Perspex.Collections; + namespace Perspex.Controls { public class ColumnDefinitions : PerspexList diff --git a/Perspex.Controls/ContentControl.cs b/Perspex.Controls/ContentControl.cs index ad4bb2f6d2..df9ec3a8ca 100644 --- a/Perspex.Controls/ContentControl.cs +++ b/Perspex.Controls/ContentControl.cs @@ -6,10 +6,12 @@ namespace Perspex.Controls { + using System; + using Perspex.Collections; using Perspex.Controls.Primitives; using Perspex.Layout; - public class ContentControl : TemplatedControl + public class ContentControl : TemplatedControl, ILogical { public static readonly PerspexProperty ContentProperty = PerspexProperty.Register("Content"); @@ -20,12 +22,40 @@ namespace Perspex.Controls public static readonly PerspexProperty VerticalContentAlignmentProperty = PerspexProperty.Register("VerticalContentAlignment"); + private SingleItemPerspexList logicalChild = new SingleItemPerspexList(); + + public ContentControl() + { + this.GetObservableWithHistory(ContentProperty).Subscribe(x => + { + var control1 = x.Item1 as Control; + var control2 = x.Item2 as Control; + + if (control1 != null) + { + control1.Parent = null; + } + + if (control2 != null) + { + control2.Parent = this; + } + + this.logicalChild.SingleItem = control2; + }); + } + public object Content { get { return this.GetValue(ContentProperty); } set { this.SetValue(ContentProperty, value); } } + IReadOnlyPerspexList ILogical.LogicalChildren + { + get { return this.logicalChild; } + } + public HorizontalAlignment HorizontalContentAlignment { get { return this.GetValue(HorizontalContentAlignmentProperty); } diff --git a/Perspex.Controls/Control.cs b/Perspex.Controls/Control.cs index f7b60daf9d..2b5a8b5075 100644 --- a/Perspex.Controls/Control.cs +++ b/Perspex.Controls/Control.cs @@ -7,8 +7,10 @@ namespace Perspex.Controls { using System; + using System.Collections.Generic; + using System.Linq; using System.Reactive.Linq; - using Perspex.Animation; + using Perspex.Collections; using Perspex.Input; using Perspex.Interactivity; using Perspex.Media; @@ -16,7 +18,7 @@ namespace Perspex.Controls using Perspex.Styling; using Splat; - public class Control : InputElement, IStyleable, IStyleHost + public class Control : InputElement, ILogical, IStyleable, IStyleHost { public static readonly PerspexProperty BorderBrushProperty = PerspexProperty.Register("BorderBrush"); @@ -27,6 +29,9 @@ namespace Perspex.Controls public static readonly PerspexProperty ForegroundProperty = PerspexProperty.Register("Foreground", new SolidColorBrush(0xff000000), inherits: true); + public static readonly PerspexProperty ParentProperty = + PerspexProperty.Register("Parent"); + public static readonly PerspexProperty TemplatedParentProperty = PerspexProperty.Register("TemplatedParent", inherits: true); @@ -143,12 +148,28 @@ namespace Perspex.Controls } } + public Control Parent + { + get { return this.GetValue(ParentProperty); } + internal set { this.SetValue(ParentProperty, value); } + } + public ITemplatedControl TemplatedParent { get { return this.GetValue(TemplatedParentProperty); } internal set { this.SetValue(TemplatedParentProperty, value); } } + ILogical ILogical.LogicalParent + { + get { return this.Parent; } + } + + IReadOnlyPerspexList ILogical.LogicalChildren + { + get { throw new NotImplementedException(); } + } + public void BringIntoView() { this.BringIntoView(new Rect(this.ActualSize)); diff --git a/Perspex.Controls/Controls.cs b/Perspex.Controls/Controls.cs index 5214e59ade..353ecf03db 100644 --- a/Perspex.Controls/Controls.cs +++ b/Perspex.Controls/Controls.cs @@ -7,6 +7,7 @@ namespace Perspex.Controls { using System.Collections.Generic; + using Perspex.Collections; public class Controls : PerspexList { diff --git a/Perspex.Controls/DataTemplates.cs b/Perspex.Controls/DataTemplates.cs index 1f81fde90f..42a1a17182 100644 --- a/Perspex.Controls/DataTemplates.cs +++ b/Perspex.Controls/DataTemplates.cs @@ -13,6 +13,7 @@ namespace Perspex.Controls using System.Linq; using System.Reactive; using System.Reactive.Subjects; + using Perspex.Collections; public class DataTemplates : PerspexList { diff --git a/Perspex.Controls/Decorator.cs b/Perspex.Controls/Decorator.cs index 231fe7b820..1ae8711fcd 100644 --- a/Perspex.Controls/Decorator.cs +++ b/Perspex.Controls/Decorator.cs @@ -10,9 +10,10 @@ namespace Perspex.Controls using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; + using Perspex.Collections; using Perspex.Layout; - public class Decorator : Control, IVisual + public class Decorator : Control, IVisual, ILogical { public static readonly PerspexProperty ContentProperty = PerspexProperty.Register("Content"); @@ -20,16 +21,25 @@ namespace Perspex.Controls public static readonly PerspexProperty PaddingProperty = PerspexProperty.Register("Padding"); + private SingleItemPerspexList logicalChild = new SingleItemPerspexList(); + public Decorator() { - this.GetObservable(ContentProperty).Subscribe(x => + this.GetObservableWithHistory(ContentProperty).Subscribe(x => { - this.ClearVisualChildren(); + if (x.Item1 != null) + { + this.RemoveVisualChild(x.Item1); + x.Item1.Parent = null; + } - if (x != null) + if (x.Item2 != null) { - this.AddVisualChild(x); + this.AddVisualChild(x.Item2); + x.Item2.Parent = this; } + + this.logicalChild.SingleItem = x.Item2; }); } @@ -45,6 +55,11 @@ namespace Perspex.Controls set { this.SetValue(PaddingProperty, value); } } + IReadOnlyPerspexList ILogical.LogicalChildren + { + get { return this.logicalChild; } + } + protected override Size ArrangeOverride(Size finalSize) { Control content = this.Content; diff --git a/Perspex.Controls/Grid.cs b/Perspex.Controls/Grid.cs index 789c4e06c7..b5e57cc178 100644 --- a/Perspex.Controls/Grid.cs +++ b/Perspex.Controls/Grid.cs @@ -9,6 +9,7 @@ namespace Perspex.Controls using System; using System.Collections.Generic; using System.Linq; + using Perspex.Collections; public class Grid : Panel { diff --git a/Perspex.Controls/ILogical.cs b/Perspex.Controls/ILogical.cs new file mode 100644 index 0000000000..1435f85651 --- /dev/null +++ b/Perspex.Controls/ILogical.cs @@ -0,0 +1,27 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls +{ + using System.Collections.Generic; + using Perspex.Collections; + + /// + /// Represents a node in the logical tree. + /// + public interface ILogical + { + /// + /// Gets the logical parent. + /// + ILogical LogicalParent { get; } + + /// + /// Gets the logical children. + /// + IReadOnlyPerspexList LogicalChildren { get; } + } +} diff --git a/Perspex.Controls/Panel.cs b/Perspex.Controls/Panel.cs index 147831e3bf..d449f4da05 100644 --- a/Perspex.Controls/Panel.cs +++ b/Perspex.Controls/Panel.cs @@ -9,11 +9,12 @@ namespace Perspex.Controls using System; using System.Collections.Specialized; using System.Linq; + using Perspex.Collections; /// /// Base class for controls that can contain multiple children. /// - public class Panel : Control + public class Panel : Control, ILogical { private Controls children; @@ -54,6 +55,11 @@ namespace Perspex.Controls } } + IReadOnlyPerspexList ILogical.LogicalChildren + { + get { return this.children; } + } + private void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) { // TODO: Handle Move and Replace. @@ -61,10 +67,22 @@ namespace Perspex.Controls { case NotifyCollectionChangedAction.Add: this.AddVisualChildren(e.NewItems.OfType()); + + foreach (var child in e.NewItems.OfType()) + { + child.Parent = this; + } + break; case NotifyCollectionChangedAction.Remove: this.RemoveVisualChildren(e.OldItems.OfType()); + + foreach (var child in e.OldItems.OfType()) + { + child.Parent = null; + } + break; case NotifyCollectionChangedAction.Reset: diff --git a/Perspex.Controls/Perspex.Controls.csproj b/Perspex.Controls/Perspex.Controls.csproj index 98bf840977..1ce77bb0e5 100644 --- a/Perspex.Controls/Perspex.Controls.csproj +++ b/Perspex.Controls/Perspex.Controls.csproj @@ -36,6 +36,7 @@ + diff --git a/Perspex.Controls/RowDefinitions.cs b/Perspex.Controls/RowDefinitions.cs index 60ea0da556..7affaa019f 100644 --- a/Perspex.Controls/RowDefinitions.cs +++ b/Perspex.Controls/RowDefinitions.cs @@ -6,6 +6,8 @@ namespace Perspex.Controls { + using Perspex.Collections; + public class RowDefinitions : PerspexList { } diff --git a/Perspex.SceneGraph/IVisual.cs b/Perspex.SceneGraph/IVisual.cs index 12a1104864..24dee5fc20 100644 --- a/Perspex.SceneGraph/IVisual.cs +++ b/Perspex.SceneGraph/IVisual.cs @@ -7,6 +7,7 @@ namespace Perspex { using System.Collections.Generic; + using Perspex.Collections; using Perspex.Media; /// diff --git a/Perspex.SceneGraph/Visual.cs b/Perspex.SceneGraph/Visual.cs index 28eed3521f..2f9351fc12 100644 --- a/Perspex.SceneGraph/Visual.cs +++ b/Perspex.SceneGraph/Visual.cs @@ -12,6 +12,7 @@ namespace Perspex using System.Linq; using System.Reactive.Linq; using Perspex.Animation; + using Perspex.Collections; using Perspex.Media; using Perspex.Rendering; using Splat; diff --git a/Perspex.Styling/Styles.cs b/Perspex.Styling/Styles.cs index b6e1111b54..5e95978abe 100644 --- a/Perspex.Styling/Styles.cs +++ b/Perspex.Styling/Styles.cs @@ -6,6 +6,8 @@ namespace Perspex.Styling { + using Perspex.Collections; + public class Styles : PerspexList, IStyle { public void Attach(IStyleable control) diff --git a/TestApplication/Program.cs b/TestApplication/Program.cs index 297e244473..f3543bff94 100644 --- a/TestApplication/Program.cs +++ b/TestApplication/Program.cs @@ -2,6 +2,7 @@ using System.Reactive.Linq; using Perspex; using Perspex.Animation; +using Perspex.Collections; using Perspex.Controls; using Perspex.Controls.Primitives; using Perspex.Controls.Shapes; From 9a994e582ced1edf4367d6dee00d6276f136534f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 20 Dec 2014 22:21:12 +0100 Subject: [PATCH 02/11] Moved VisualTree extensions into own namespace. So that they don't pollute the whole Perspex namespace - you now need to "opt-in" to the visual tree. --- Perspex.Controls.UnitTests/ContentControlTests.cs | 1 + Perspex.Controls.UnitTests/TemplatedControlTests.cs | 1 + Perspex.Controls/ControlExtensions.cs | 1 + Perspex.Controls/DataTemplateExtensions.cs | 1 + Perspex.Controls/GridSplitter.cs | 1 + Perspex.Controls/Presenters/ScrollContentPresenter.cs | 1 + Perspex.Controls/Presenters/TextPresenter.cs | 1 + Perspex.Controls/Primitives/SelectingItemsControl.cs | 1 + Perspex.Controls/Primitives/TemplatedControl.cs | 1 + Perspex.Controls/RadioButton.cs | 1 + Perspex.Controls/TreeViewItem.cs | 1 + Perspex.Input/FocusManager.cs | 1 + Perspex.Input/InputElement.cs | 1 + Perspex.Input/InputManager.cs | 1 + Perspex.Input/MouseDevice.cs | 1 + Perspex.Interactivity/Interactive.cs | 1 + Perspex.Layout/LayoutManager.cs | 1 + Perspex.Layout/Layoutable.cs | 1 + Perspex.SceneGraph.UnitTests/VisualTests.cs | 1 + Perspex.SceneGraph/Perspex.SceneGraph.csproj | 2 +- Perspex.SceneGraph/Visual.cs | 1 + Perspex.SceneGraph/{ => VisualTree}/VisualExtensions.cs | 2 +- Perspex.Styling/Styler.cs | 1 + 23 files changed, 23 insertions(+), 2 deletions(-) rename Perspex.SceneGraph/{ => VisualTree}/VisualExtensions.cs (99%) diff --git a/Perspex.Controls.UnitTests/ContentControlTests.cs b/Perspex.Controls.UnitTests/ContentControlTests.cs index c7d3351ec8..5731afa76d 100644 --- a/Perspex.Controls.UnitTests/ContentControlTests.cs +++ b/Perspex.Controls.UnitTests/ContentControlTests.cs @@ -16,6 +16,7 @@ namespace Perspex.Controls.UnitTests using Perspex.Layout; using Perspex.Platform; using Perspex.Styling; + using Perspex.VisualTree; using Ploeh.AutoFixture; using Ploeh.AutoFixture.AutoMoq; using Splat; diff --git a/Perspex.Controls.UnitTests/TemplatedControlTests.cs b/Perspex.Controls.UnitTests/TemplatedControlTests.cs index b143b819d1..510b99cf99 100644 --- a/Perspex.Controls.UnitTests/TemplatedControlTests.cs +++ b/Perspex.Controls.UnitTests/TemplatedControlTests.cs @@ -14,6 +14,7 @@ namespace Perspex.Controls.UnitTests using Microsoft.VisualStudio.TestTools.UnitTesting; using Perspex.Controls; using Perspex.Controls.Primitives; + using Perspex.VisualTree; [TestClass] public class TemplatedControlTests diff --git a/Perspex.Controls/ControlExtensions.cs b/Perspex.Controls/ControlExtensions.cs index 437aff1983..e1b400365a 100644 --- a/Perspex.Controls/ControlExtensions.cs +++ b/Perspex.Controls/ControlExtensions.cs @@ -11,6 +11,7 @@ namespace Perspex.Controls using System.Linq; using Perspex.Controls; using Perspex.Styling; + using Perspex.VisualTree; public static class ControlExtensions { diff --git a/Perspex.Controls/DataTemplateExtensions.cs b/Perspex.Controls/DataTemplateExtensions.cs index 48e6c51c82..576aa3beb2 100644 --- a/Perspex.Controls/DataTemplateExtensions.cs +++ b/Perspex.Controls/DataTemplateExtensions.cs @@ -7,6 +7,7 @@ namespace Perspex.Controls { using System.Linq; + using Perspex.VisualTree; using Splat; public static class DataTemplateExtensions diff --git a/Perspex.Controls/GridSplitter.cs b/Perspex.Controls/GridSplitter.cs index 99e713d6d9..07391d8305 100644 --- a/Perspex.Controls/GridSplitter.cs +++ b/Perspex.Controls/GridSplitter.cs @@ -9,6 +9,7 @@ namespace Perspex.Controls using System; using Perspex.Controls.Primitives; using Perspex.Input; + using Perspex.VisualTree; public class GridSplitter : Thumb { diff --git a/Perspex.Controls/Presenters/ScrollContentPresenter.cs b/Perspex.Controls/Presenters/ScrollContentPresenter.cs index 8a795d5199..01f092f0c8 100644 --- a/Perspex.Controls/Presenters/ScrollContentPresenter.cs +++ b/Perspex.Controls/Presenters/ScrollContentPresenter.cs @@ -10,6 +10,7 @@ namespace Perspex.Controls.Presenters using System.Linq; using Perspex.Input; using Perspex.Layout; + using Perspex.VisualTree; public class ScrollContentPresenter : ContentPresenter { diff --git a/Perspex.Controls/Presenters/TextPresenter.cs b/Perspex.Controls/Presenters/TextPresenter.cs index d0abc73530..6a1af946fe 100644 --- a/Perspex.Controls/Presenters/TextPresenter.cs +++ b/Perspex.Controls/Presenters/TextPresenter.cs @@ -15,6 +15,7 @@ namespace Perspex.Controls using Perspex.Input; using Perspex.Media; using Perspex.Threading; + using Perspex.VisualTree; public class TextPresenter : TextBlock { diff --git a/Perspex.Controls/Primitives/SelectingItemsControl.cs b/Perspex.Controls/Primitives/SelectingItemsControl.cs index 6390b17f36..f918fd56f1 100644 --- a/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -12,6 +12,7 @@ namespace Perspex.Controls.Primitives using Perspex.Controls.Presenters; using Perspex.Input; using Perspex.Interactivity; + using Perspex.VisualTree; public class SelectingItemsControl : ItemsControl { diff --git a/Perspex.Controls/Primitives/TemplatedControl.cs b/Perspex.Controls/Primitives/TemplatedControl.cs index 29752b7049..ffe27a7471 100644 --- a/Perspex.Controls/Primitives/TemplatedControl.cs +++ b/Perspex.Controls/Primitives/TemplatedControl.cs @@ -10,6 +10,7 @@ namespace Perspex.Controls.Primitives using System.Linq; using Perspex.Media; using Perspex.Styling; + using Perspex.VisualTree; using Splat; public class TemplatedControl : Control, ITemplatedControl diff --git a/Perspex.Controls/RadioButton.cs b/Perspex.Controls/RadioButton.cs index 33c9bfa298..a88c0d5e3e 100644 --- a/Perspex.Controls/RadioButton.cs +++ b/Perspex.Controls/RadioButton.cs @@ -9,6 +9,7 @@ namespace Perspex.Controls using System; using System.Linq; using Perspex.Controls.Primitives; + using Perspex.VisualTree; public class RadioButton : ToggleButton { diff --git a/Perspex.Controls/TreeViewItem.cs b/Perspex.Controls/TreeViewItem.cs index c0c028818f..51d0673f02 100644 --- a/Perspex.Controls/TreeViewItem.cs +++ b/Perspex.Controls/TreeViewItem.cs @@ -10,6 +10,7 @@ namespace Perspex.Controls using System.Linq; using Perspex.Controls.Generators; using Perspex.Controls.Primitives; + using Perspex.VisualTree; public class TreeViewItem : HeaderedItemsControl, ISelectable { diff --git a/Perspex.Input/FocusManager.cs b/Perspex.Input/FocusManager.cs index 1cbb401975..ccf9be29a4 100644 --- a/Perspex.Input/FocusManager.cs +++ b/Perspex.Input/FocusManager.cs @@ -10,6 +10,7 @@ namespace Perspex.Input using System.Collections.Generic; using System.Linq; using Perspex.Interactivity; + using Perspex.VisualTree; using Splat; public class FocusManager : IFocusManager diff --git a/Perspex.Input/InputElement.cs b/Perspex.Input/InputElement.cs index 99b1c1c349..2c3a12c612 100644 --- a/Perspex.Input/InputElement.cs +++ b/Perspex.Input/InputElement.cs @@ -10,6 +10,7 @@ namespace Perspex.Input using System.Linq; using Perspex.Interactivity; using Perspex.Rendering; + using Perspex.VisualTree; public class InputElement : Interactive, IInputElement { diff --git a/Perspex.Input/InputManager.cs b/Perspex.Input/InputManager.cs index 16086d421c..f7e9297ade 100644 --- a/Perspex.Input/InputManager.cs +++ b/Perspex.Input/InputManager.cs @@ -11,6 +11,7 @@ namespace Perspex.Input using System.Linq; using System.Reactive.Subjects; using Perspex.Input.Raw; + using Perspex.VisualTree; public class InputManager : IInputManager { diff --git a/Perspex.Input/MouseDevice.cs b/Perspex.Input/MouseDevice.cs index b0808ca8e4..dd2bc1b1b2 100644 --- a/Perspex.Input/MouseDevice.cs +++ b/Perspex.Input/MouseDevice.cs @@ -12,6 +12,7 @@ namespace Perspex.Input using Perspex.Input.Raw; using Perspex.Interactivity; using Perspex.Platform; + using Perspex.VisualTree; using Splat; public abstract class MouseDevice : IMouseDevice diff --git a/Perspex.Interactivity/Interactive.cs b/Perspex.Interactivity/Interactive.cs index 509ffcedd5..c4bf1762e5 100644 --- a/Perspex.Interactivity/Interactive.cs +++ b/Perspex.Interactivity/Interactive.cs @@ -12,6 +12,7 @@ namespace Perspex.Interactivity using System.Reactive; using System.Reactive.Linq; using Perspex.Layout; + using Perspex.VisualTree; public class Interactive : Layoutable, IInteractive { diff --git a/Perspex.Layout/LayoutManager.cs b/Perspex.Layout/LayoutManager.cs index d02c9edc8a..7fff4cb3a0 100644 --- a/Perspex.Layout/LayoutManager.cs +++ b/Perspex.Layout/LayoutManager.cs @@ -10,6 +10,7 @@ namespace Perspex.Layout using System.Reactive; using System.Reactive.Subjects; using NGenerics.DataStructures.General; + using Perspex.VisualTree; /// /// Manages measuring and arranging of controls. diff --git a/Perspex.Layout/Layoutable.cs b/Perspex.Layout/Layoutable.cs index 69f1277388..f41b07b618 100644 --- a/Perspex.Layout/Layoutable.cs +++ b/Perspex.Layout/Layoutable.cs @@ -8,6 +8,7 @@ namespace Perspex.Layout { using System; using System.Linq; + using Perspex.VisualTree; using Splat; public enum HorizontalAlignment diff --git a/Perspex.SceneGraph.UnitTests/VisualTests.cs b/Perspex.SceneGraph.UnitTests/VisualTests.cs index a7e0755a47..37a307d054 100644 --- a/Perspex.SceneGraph.UnitTests/VisualTests.cs +++ b/Perspex.SceneGraph.UnitTests/VisualTests.cs @@ -8,6 +8,7 @@ namespace Perspex.SceneGraph.UnitTests { using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Perspex.VisualTree; [TestClass] public class VisualTests diff --git a/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/Perspex.SceneGraph/Perspex.SceneGraph.csproj index c3ee00e99c..1cf8facec2 100644 --- a/Perspex.SceneGraph/Perspex.SceneGraph.csproj +++ b/Perspex.SceneGraph/Perspex.SceneGraph.csproj @@ -91,7 +91,7 @@ - + diff --git a/Perspex.SceneGraph/Visual.cs b/Perspex.SceneGraph/Visual.cs index 2f9351fc12..5d9900e010 100644 --- a/Perspex.SceneGraph/Visual.cs +++ b/Perspex.SceneGraph/Visual.cs @@ -15,6 +15,7 @@ namespace Perspex using Perspex.Collections; using Perspex.Media; using Perspex.Rendering; + using Perspex.VisualTree; using Splat; public class Visual : Animatable, IVisual diff --git a/Perspex.SceneGraph/VisualExtensions.cs b/Perspex.SceneGraph/VisualTree/VisualExtensions.cs similarity index 99% rename from Perspex.SceneGraph/VisualExtensions.cs rename to Perspex.SceneGraph/VisualTree/VisualExtensions.cs index d7452ed012..f21bc1ec96 100644 --- a/Perspex.SceneGraph/VisualExtensions.cs +++ b/Perspex.SceneGraph/VisualTree/VisualExtensions.cs @@ -4,7 +4,7 @@ // // ----------------------------------------------------------------------- -namespace Perspex +namespace Perspex.VisualTree { using System; using System.Collections.Generic; diff --git a/Perspex.Styling/Styler.cs b/Perspex.Styling/Styler.cs index 8ededfe417..263cc33d4a 100644 --- a/Perspex.Styling/Styler.cs +++ b/Perspex.Styling/Styler.cs @@ -8,6 +8,7 @@ namespace Perspex.Styling { using System; using System.Linq; + using Perspex.VisualTree; using Splat; public class Styler : IStyler From 09c1f41cd498df076ea981f49f490673c189554c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 20 Dec 2014 22:32:30 +0100 Subject: [PATCH 03/11] Reinstate Descendent selector tests. Now we have logical tree back. --- Perspex.Controls/Perspex.Controls.csproj | 1 - .../ILogical.cs | 3 +- Perspex.SceneGraph/Perspex.SceneGraph.csproj | 1 + .../SelectorTests_Descendent.cs | 96 ++++++++++--------- Perspex.Styling/Selectors.cs | 5 +- 5 files changed, 56 insertions(+), 50 deletions(-) rename {Perspex.Controls => Perspex.SceneGraph}/ILogical.cs (91%) diff --git a/Perspex.Controls/Perspex.Controls.csproj b/Perspex.Controls/Perspex.Controls.csproj index 1ce77bb0e5..98bf840977 100644 --- a/Perspex.Controls/Perspex.Controls.csproj +++ b/Perspex.Controls/Perspex.Controls.csproj @@ -36,7 +36,6 @@ - diff --git a/Perspex.Controls/ILogical.cs b/Perspex.SceneGraph/ILogical.cs similarity index 91% rename from Perspex.Controls/ILogical.cs rename to Perspex.SceneGraph/ILogical.cs index 1435f85651..154a54e0a1 100644 --- a/Perspex.Controls/ILogical.cs +++ b/Perspex.SceneGraph/ILogical.cs @@ -4,9 +4,8 @@ // // ----------------------------------------------------------------------- -namespace Perspex.Controls +namespace Perspex { - using System.Collections.Generic; using Perspex.Collections; /// diff --git a/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/Perspex.SceneGraph/Perspex.SceneGraph.csproj index 1cf8facec2..3760967f12 100644 --- a/Perspex.SceneGraph/Perspex.SceneGraph.csproj +++ b/Perspex.SceneGraph/Perspex.SceneGraph.csproj @@ -45,6 +45,7 @@ + diff --git a/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs b/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs index 245a7f573b..073dad0717 100644 --- a/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs @@ -11,6 +11,7 @@ namespace Perspex.Styling.UnitTests using System.Reactive.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; + using Perspex.Collections; using Perspex.Styling; [TestClass] @@ -19,67 +20,64 @@ namespace Perspex.Styling.UnitTests [TestMethod] public void Descendent_Matches_Control_When_It_Is_Child_OfType() { - Assert.Inconclusive("Need to implement logical tree."); - ////var parent = new Mock(); - ////var child = new Mock(); - - ////child.Setup(x => x.LogicalParent).Returns(parent.Object); + var parent = new Mock(); + var child = new Mock(); + var childStyleable = child.As(); - ////var selector = new Selector().OfType(parent.Object.GetType()).Descendent().OfType(child.Object.GetType()); + child.Setup(x => x.LogicalParent).Returns(parent.Object); - ////Assert.IsTrue(ActivatorValue(selector, child.Object)); + var selector = new Selector().OfType(parent.Object.GetType()).Descendent().OfType(child.Object.GetType()); + + Assert.IsTrue(ActivatorValue(selector, childStyleable.Object)); } [TestMethod] public void Descendent_Matches_Control_When_It_Is_Descendent_OfType() { - Assert.Inconclusive("Need to implement logical tree."); - ////var grandparent = new Mock(); - ////var parent = new Mock(); - ////var child = new Mock(); + var grandparent = new Mock(); + var parent = new Mock(); + var child = new Mock(); - ////parent.Setup(x => x.LogicalParent).Returns(grandparent.Object); - ////child.Setup(x => x.LogicalParent).Returns(parent.Object); + parent.Setup(x => x.LogicalParent).Returns(grandparent.Object); + child.Setup(x => x.LogicalParent).Returns(parent.Object); - ////var selector = new Selector().OfType(grandparent.Object.GetType()).Descendent().OfType(child.Object.GetType()); + var selector = new Selector().OfType(grandparent.Object.GetType()).Descendent().OfType(child.Object.GetType()); - ////Assert.IsTrue(ActivatorValue(selector, child.Object)); + Assert.IsTrue(ActivatorValue(selector, child.Object)); } [TestMethod] public void Descendent_Matches_Control_When_It_Is_Descendent_OfType_And_Class() { - Assert.Inconclusive("Need to implement logical tree."); - ////var grandparent = new Mock(); - ////var parent = new Mock(); - ////var child = new Mock(); + var grandparent = new Mock(); + var parent = new Mock(); + var child = new Mock(); - ////grandparent.Setup(x => x.Classes).Returns(new Classes("foo")); - ////parent.Setup(x => x.LogicalParent).Returns(grandparent.Object); - ////parent.Setup(x => x.Classes).Returns(new Classes()); - ////child.Setup(x => x.LogicalParent).Returns(parent.Object); + grandparent.Setup(x => x.Classes).Returns(new Classes("foo")); + parent.Setup(x => x.LogicalParent).Returns(grandparent.Object); + parent.Setup(x => x.Classes).Returns(new Classes()); + child.Setup(x => x.LogicalParent).Returns(parent.Object); - ////var selector = new Selector().OfType(grandparent.Object.GetType()).Class("foo").Descendent().OfType(child.Object.GetType()); + var selector = new Selector().OfType(grandparent.Object.GetType()).Class("foo").Descendent().OfType(child.Object.GetType()); - ////Assert.IsTrue(ActivatorValue(selector, child.Object)); + Assert.IsTrue(ActivatorValue(selector, child.Object)); } [TestMethod] public void Descendent_Doesnt_Match_Control_When_It_Is_Descendent_OfType_But_Wrong_Class() { - Assert.Inconclusive("Need to implement logical tree."); - ////var grandparent = new Mock(); - ////var parent = new Mock(); - ////var child = new Mock(); + var grandparent = new Mock(); + var parent = new Mock(); + var child = new Mock(); - ////grandparent.Setup(x => x.Classes).Returns(new Classes("bar")); - ////parent.Setup(x => x.LogicalParent).Returns(grandparent.Object); - ////parent.Setup(x => x.Classes).Returns(new Classes("foo")); - ////child.Setup(x => x.LogicalParent).Returns(parent.Object); + grandparent.Setup(x => x.Classes).Returns(new Classes("bar")); + parent.Setup(x => x.LogicalParent).Returns(grandparent.Object); + parent.Setup(x => x.Classes).Returns(new Classes("foo")); + child.Setup(x => x.LogicalParent).Returns(parent.Object); - ////var selector = new Selector().OfType().Class("foo").Descendent().OfType(); + var selector = new Selector().OfType().Class("foo").Descendent().OfType(); - ////Assert.IsFalse(ActivatorValue(selector, child.Object)); + Assert.IsFalse(ActivatorValue(selector, child.Object)); } private static bool ActivatorValue(Selector selector, IStyleable control) @@ -87,16 +85,26 @@ namespace Perspex.Styling.UnitTests return selector.GetActivator(control).Take(1).ToEnumerable().Single(); } - ////public abstract class TestLogical1 : TestLogical - ////{ - ////} + public abstract class TestLogical : ILogical, IStyleable + { + public abstract Classes Classes { get; } + public abstract string Id { get; } + public abstract IReadOnlyPerspexList LogicalChildren { get; } + public abstract ILogical LogicalParent { get; } + public abstract ITemplatedControl TemplatedParent { get; } + public abstract IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue); + } + + public abstract class TestLogical1 : TestLogical + { + } - ////public abstract class TestLogical2 : TestLogical - ////{ - ////} + public abstract class TestLogical2 : TestLogical + { + } - ////public abstract class TestLogical3 : TestLogical - ////{ - ////} + public abstract class TestLogical3 : TestLogical + { + } } } diff --git a/Perspex.Styling/Selectors.cs b/Perspex.Styling/Selectors.cs index 157b2e3921..120529b5c0 100644 --- a/Perspex.Styling/Selectors.cs +++ b/Perspex.Styling/Selectors.cs @@ -33,13 +33,12 @@ namespace Perspex.Styling SelectorString = " ", GetObservable = control => { - // TODO: This needs to traverse the logical tree, not the visual. - IVisual c = (IVisual)control; + ILogical c = (ILogical)control; List> descendentMatches = new List>(); while (c != null) { - c = c.VisualParent; + c = c.LogicalParent; if (c is IStyleable) { From 309ac30a418fbeb85cb3d74dcabc0b4f67301944 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 24 Dec 2014 14:53:12 +0000 Subject: [PATCH 04/11] Make ContentControl child be logical child. The control this is created by the ContentPresenter now appears as the logical child of the parent ContentControl. --- .../ContentControlTests.cs | 36 ++++++++++++++ Perspex.Controls/ContentControl.cs | 20 +++++++- .../Presenters/ContentPresenter.cs | 47 +++++++++++++++---- Perspex.Themes.Default/ContentControlStyle.cs | 3 +- Perspex.Themes.Default/WindowStyle.cs | 1 + 5 files changed, 96 insertions(+), 11 deletions(-) diff --git a/Perspex.Controls.UnitTests/ContentControlTests.cs b/Perspex.Controls.UnitTests/ContentControlTests.cs index 5731afa76d..f4592aac32 100644 --- a/Perspex.Controls.UnitTests/ContentControlTests.cs +++ b/Perspex.Controls.UnitTests/ContentControlTests.cs @@ -125,11 +125,28 @@ namespace Perspex.Controls.UnitTests var target = new ContentControl(); var child = new Control(); + target.Template = this.GetTemplate(); target.Content = child; + target.ApplyTemplate(); CollectionAssert.AreEqual(new[] { child }, ((ILogical)target).LogicalChildren.ToList()); } + [TestMethod] + public void Setting_Content_To_String_Should_Make_TextBlock_Appear_In_LogicalChildren() + { + var target = new ContentControl(); + var child = new Control(); + + target.Template = this.GetTemplate(); + target.Content = "Foo"; + target.ApplyTemplate(); + + var logical = (ILogical)target; + Assert.AreEqual(1, logical.LogicalChildren.Count); + Assert.IsInstanceOfType(logical.LogicalChildren[0], typeof(TextBlock)); + } + [TestMethod] public void Clearing_Content_Should_Remove_From_LogicalChildren() { @@ -152,7 +169,13 @@ namespace Perspex.Controls.UnitTests ((ILogical)contentControl).LogicalChildren.CollectionChanged += (s, e) => called = e.Action == NotifyCollectionChangedAction.Add; + contentControl.Template = this.GetTemplate(); contentControl.Content = child; + contentControl.ApplyTemplate(); + + // Need to call ApplyTemplate on presenter for CollectionChanged to be called. + var presenter = contentControl.GetTemplateControls().Single(x => x.Id == "presenter"); + presenter.ApplyTemplate(); Assert.IsTrue(called); } @@ -164,13 +187,19 @@ namespace Perspex.Controls.UnitTests var child = new Control(); var called = false; + contentControl.Template = this.GetTemplate(); contentControl.Content = child; + ApplyTemplate(contentControl); ((ILogical)contentControl).LogicalChildren.CollectionChanged += (s, e) => called = e.Action == NotifyCollectionChangedAction.Remove; contentControl.Content = null; + // Need to call ApplyTemplate on presenter for CollectionChanged to be called. + var presenter = contentControl.GetTemplateControls().Single(x => x.Id == "presenter"); + presenter.ApplyTemplate(); + Assert.IsTrue(called); } @@ -182,13 +211,19 @@ namespace Perspex.Controls.UnitTests var child2 = new Control(); var called = false; + contentControl.Template = this.GetTemplate(); contentControl.Content = child1; + contentControl.ApplyTemplate(); ((ILogical)contentControl).LogicalChildren.CollectionChanged += (s, e) => called = e.Action == NotifyCollectionChangedAction.Replace; contentControl.Content = child2; + // Need to call ApplyTemplate on presenter for CollectionChanged to be called. + var presenter = contentControl.GetTemplateControls().Single(x => x.Id == "presenter"); + presenter.ApplyTemplate(); + Assert.IsTrue(called); } @@ -206,6 +241,7 @@ namespace Perspex.Controls.UnitTests Background = new Perspex.Media.SolidColorBrush(0xffffffff), Content = new ContentPresenter { + Id = "presenter", [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], } }; diff --git a/Perspex.Controls/ContentControl.cs b/Perspex.Controls/ContentControl.cs index df9ec3a8ca..decaf5158a 100644 --- a/Perspex.Controls/ContentControl.cs +++ b/Perspex.Controls/ContentControl.cs @@ -8,6 +8,7 @@ namespace Perspex.Controls { using System; using Perspex.Collections; + using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; using Perspex.Layout; @@ -24,6 +25,10 @@ namespace Perspex.Controls private SingleItemPerspexList logicalChild = new SingleItemPerspexList(); + private ContentPresenter presenter; + + private IDisposable presenterSubscription; + public ContentControl() { this.GetObservableWithHistory(ContentProperty).Subscribe(x => @@ -40,8 +45,6 @@ namespace Perspex.Controls { control2.Parent = this; } - - this.logicalChild.SingleItem = control2; }); } @@ -67,5 +70,18 @@ namespace Perspex.Controls get { return this.GetValue(VerticalContentAlignmentProperty); } set { this.SetValue(VerticalContentAlignmentProperty, value); } } + + protected override void OnTemplateApplied() + { + if (this.presenterSubscription != null) + { + this.presenterSubscription.Dispose(); + this.presenterSubscription = null; + } + + this.presenter = this.GetTemplateChild("presenter"); + this.presenterSubscription = this.presenter.ChildObservable + .Subscribe(x => this.logicalChild.SingleItem = x); + } } } diff --git a/Perspex.Controls/Presenters/ContentPresenter.cs b/Perspex.Controls/Presenters/ContentPresenter.cs index 617489ad67..4646348df4 100644 --- a/Perspex.Controls/Presenters/ContentPresenter.cs +++ b/Perspex.Controls/Presenters/ContentPresenter.cs @@ -9,8 +9,8 @@ namespace Perspex.Controls.Presenters using System; using System.Linq; using System.Reactive.Linq; + using System.Reactive.Subjects; using Perspex.Controls.Primitives; - using Perspex.Layout; public class ContentPresenter : Control, IVisual { @@ -19,11 +19,44 @@ namespace Perspex.Controls.Presenters private bool createdChild; + private Control child; + + private BehaviorSubject childObservable = new BehaviorSubject(null); + public ContentPresenter() { this.GetObservable(ContentProperty).Skip(1).Subscribe(this.ContentChanged); } + public Control Child + { + get + { + return this.Child; + } + + private set + { + if (this.child != value) + { + this.ClearVisualChildren(); + this.child = value; + + if (value != null) + { + this.AddVisualChild(value); + } + + this.childObservable.OnNext(value); + } + } + } + + public IObservable ChildObservable + { + get { return this.childObservable; } + } + public object Content { get { return this.GetValue(ContentProperty); } @@ -45,12 +78,10 @@ namespace Perspex.Controls.Presenters protected override Size MeasureOverride(Size availableSize) { - Control child = ((IVisual)this).VisualChildren.SingleOrDefault() as Control; - - if (child != null) + if (this.child != null) { - child.Measure(availableSize); - return child.DesiredSize.Value; + this.child.Measure(availableSize); + return this.child.DesiredSize.Value; } return new Size(); @@ -64,13 +95,13 @@ namespace Perspex.Controls.Presenters private void CreateChild() { + Control result = null; object content = this.Content; this.ClearVisualChildren(); if (content != null) { - Control result; if (content is Control) { @@ -101,9 +132,9 @@ namespace Perspex.Controls.Presenters } result.TemplatedParent = foo; - this.AddVisualChild(result); } + this.Child = result; this.createdChild = true; } } diff --git a/Perspex.Themes.Default/ContentControlStyle.cs b/Perspex.Themes.Default/ContentControlStyle.cs index 8afb594a5e..1dac4417f9 100644 --- a/Perspex.Themes.Default/ContentControlStyle.cs +++ b/Perspex.Themes.Default/ContentControlStyle.cs @@ -31,7 +31,8 @@ namespace Perspex.Themes.Default private Control Template(ContentControl control) { return new ContentPresenter - { + { + Id = "presenter", [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], }; } diff --git a/Perspex.Themes.Default/WindowStyle.cs b/Perspex.Themes.Default/WindowStyle.cs index 1b0f749e56..0e163d55aa 100644 --- a/Perspex.Themes.Default/WindowStyle.cs +++ b/Perspex.Themes.Default/WindowStyle.cs @@ -37,6 +37,7 @@ namespace Perspex.Themes.Default [~Border.BackgroundProperty] = control[~Window.BackgroundProperty], Content = new ContentPresenter { + Id = "presenter", [~ContentPresenter.ContentProperty] = control[~Window.ContentProperty], } }; From 90273c74dd5d1f14213dc6cf86f76a239d6c0ddb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 24 Dec 2014 15:04:37 +0000 Subject: [PATCH 05/11] Added missing Ids to templates. And allow ContentControl to not have a ContentPresenter in its control template, as one isn't needed for TreeViewItem for example. --- Perspex.Controls/ContentControl.cs | 10 +++++++--- Perspex.Themes.Default/ButtonStyle.cs | 1 + Perspex.Themes.Default/CheckBoxStyle.cs | 1 + Perspex.Themes.Default/RadioButtonStyle.cs | 1 + Perspex.Themes.Default/TabItemStyle.cs | 1 + Perspex.Themes.Default/ToggleButtonStyle.cs | 1 + 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Perspex.Controls/ContentControl.cs b/Perspex.Controls/ContentControl.cs index decaf5158a..d51c85176b 100644 --- a/Perspex.Controls/ContentControl.cs +++ b/Perspex.Controls/ContentControl.cs @@ -79,9 +79,13 @@ namespace Perspex.Controls this.presenterSubscription = null; } - this.presenter = this.GetTemplateChild("presenter"); - this.presenterSubscription = this.presenter.ChildObservable - .Subscribe(x => this.logicalChild.SingleItem = x); + this.presenter = this.FindTemplateChild("presenter"); + + if (this.presenter != null) + { + this.presenterSubscription = this.presenter.ChildObservable + .Subscribe(x => this.logicalChild.SingleItem = x); + } } } } diff --git a/Perspex.Themes.Default/ButtonStyle.cs b/Perspex.Themes.Default/ButtonStyle.cs index 925d9c1ae6..e32d4517ff 100644 --- a/Perspex.Themes.Default/ButtonStyle.cs +++ b/Perspex.Themes.Default/ButtonStyle.cs @@ -78,6 +78,7 @@ namespace Perspex.Themes.Default Padding = new Thickness(3), Content = new ContentPresenter { + Id = "presenter", [~ContentPresenter.ContentProperty] = control[~Button.ContentProperty], [~ContentPresenter.HorizontalAlignmentProperty] = control[~Button.HorizontalContentAlignmentProperty], [~ContentPresenter.VerticalAlignmentProperty] = control[~Button.VerticalContentAlignmentProperty], diff --git a/Perspex.Themes.Default/CheckBoxStyle.cs b/Perspex.Themes.Default/CheckBoxStyle.cs index b55de93331..2ee0b0105c 100644 --- a/Perspex.Themes.Default/CheckBoxStyle.cs +++ b/Perspex.Themes.Default/CheckBoxStyle.cs @@ -83,6 +83,7 @@ namespace Perspex.Themes.Default }, new ContentPresenter { + Id = "presenter", Margin = new Thickness(4, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, [~ContentPresenter.ContentProperty] = control[~CheckBox.ContentProperty], diff --git a/Perspex.Themes.Default/RadioButtonStyle.cs b/Perspex.Themes.Default/RadioButtonStyle.cs index 1f374df041..ac56bdcd61 100644 --- a/Perspex.Themes.Default/RadioButtonStyle.cs +++ b/Perspex.Themes.Default/RadioButtonStyle.cs @@ -82,6 +82,7 @@ namespace Perspex.Themes.Default }, new ContentPresenter { + Id = "presenter", Margin = new Thickness(4, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, [~ContentPresenter.ContentProperty] = control[~RadioButton.ContentProperty], diff --git a/Perspex.Themes.Default/TabItemStyle.cs b/Perspex.Themes.Default/TabItemStyle.cs index eac19be1b9..93c38f03a4 100644 --- a/Perspex.Themes.Default/TabItemStyle.cs +++ b/Perspex.Themes.Default/TabItemStyle.cs @@ -41,6 +41,7 @@ namespace Perspex.Themes.Default { return new ContentPresenter { + Id = "presenter", [~ContentPresenter.ContentProperty] = control[~TabItem.HeaderProperty], }; } diff --git a/Perspex.Themes.Default/ToggleButtonStyle.cs b/Perspex.Themes.Default/ToggleButtonStyle.cs index a8bd8d7155..038c934143 100644 --- a/Perspex.Themes.Default/ToggleButtonStyle.cs +++ b/Perspex.Themes.Default/ToggleButtonStyle.cs @@ -93,6 +93,7 @@ namespace Perspex.Themes.Default Padding = new Thickness(3), Content = new ContentPresenter { + Id = "presenter", [~ContentPresenter.ContentProperty] = control[~ToggleButton.ContentProperty], [~ContentPresenter.HorizontalAlignmentProperty] = control[~ToggleButton.HorizontalContentAlignmentProperty], [~ContentPresenter.VerticalAlignmentProperty] = control[~ToggleButton.VerticalContentAlignmentProperty], From 70ff41b06081e46aca002aba1292aab66e66d010 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 26 Dec 2014 13:21:05 +0000 Subject: [PATCH 06/11] Use Control.ApplyTemplate to apply template. --- Perspex.Controls.UnitTests/ContentControlTests.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Perspex.Controls.UnitTests/ContentControlTests.cs b/Perspex.Controls.UnitTests/ContentControlTests.cs index f4592aac32..5ef6f09a58 100644 --- a/Perspex.Controls.UnitTests/ContentControlTests.cs +++ b/Perspex.Controls.UnitTests/ContentControlTests.cs @@ -58,7 +58,7 @@ namespace Perspex.Controls.UnitTests target.Template = this.GetTemplate(); root.Content = target; - this.ApplyTemplate(target); + target.ApplyTemplate(); styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once()); styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once()); @@ -75,7 +75,7 @@ namespace Perspex.Controls.UnitTests target.Template = this.GetTemplate(); target.Content = child; - this.ApplyTemplate(target); + target.ApplyTemplate(); var contentPresenter = child.GetVisualParent(); Assert.AreEqual(target, contentPresenter.TemplatedParent); @@ -89,7 +89,7 @@ namespace Perspex.Controls.UnitTests target.Template = this.GetTemplate(); target.Content = child; - this.ApplyTemplate(target); + target.ApplyTemplate(); Assert.IsNull(child.TemplatedParent); } @@ -189,7 +189,7 @@ namespace Perspex.Controls.UnitTests contentControl.Template = this.GetTemplate(); contentControl.Content = child; - ApplyTemplate(contentControl); + contentControl.ApplyTemplate(); ((ILogical)contentControl).LogicalChildren.CollectionChanged += (s, e) => called = e.Action == NotifyCollectionChangedAction.Remove; @@ -227,11 +227,6 @@ namespace Perspex.Controls.UnitTests Assert.IsTrue(called); } - private void ApplyTemplate(ILayoutable control) - { - control.Measure(new Size(100, 100)); - } - private ControlTemplate GetTemplate() { return ControlTemplate.Create(parent => From f5a856e109768d2e1c7328316a8aaa0f5f01c2c2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 26 Dec 2014 22:31:22 +0000 Subject: [PATCH 07/11] Make ItemsControl items be logical children. --- Perspex.Controls/Control.cs | 4 +- Perspex.Controls/ItemsControl.cs | 43 ++++++++++++++++++- Perspex.Controls/Panel.cs | 9 +++- Perspex.Controls/Presenters/ItemsPresenter.cs | 2 +- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/Perspex.Controls/Control.cs b/Perspex.Controls/Control.cs index 2b5a8b5075..1e4bff5d94 100644 --- a/Perspex.Controls/Control.cs +++ b/Perspex.Controls/Control.cs @@ -38,6 +38,8 @@ namespace Perspex.Controls public static readonly RoutedEvent RequestBringIntoViewEvent = RoutedEvent.Register("RequestBringIntoView", RoutingStrategy.Bubble); + private static readonly IReadOnlyPerspexList EmptyChildren = new SingleItemPerspexList(); + private Classes classes = new Classes(); private DataTemplates dataTemplates; @@ -167,7 +169,7 @@ namespace Perspex.Controls IReadOnlyPerspexList ILogical.LogicalChildren { - get { throw new NotImplementedException(); } + get { return EmptyChildren; } } public void BringIntoView() diff --git a/Perspex.Controls/ItemsControl.cs b/Perspex.Controls/ItemsControl.cs index 1d8d92b8e4..133fa41559 100644 --- a/Perspex.Controls/ItemsControl.cs +++ b/Perspex.Controls/ItemsControl.cs @@ -11,10 +11,13 @@ namespace Perspex.Controls using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; + using Perspex.Collections; using Perspex.Controls.Generators; + using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; + using Perspex.VisualTree; - public class ItemsControl : TemplatedControl + public class ItemsControl : TemplatedControl, ILogical { [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Needs to be before or a NullReferenceException is thrown.")] private static readonly ItemsPanelTemplate DefaultPanel = @@ -28,6 +31,10 @@ namespace Perspex.Controls private ItemContainerGenerator itemContainerGenerator; + private PerspexReadOnlyListView logicalChildren; + + private ItemsPresenter presenter; + public ItemsControl() { this.GetObservableWithHistory(ItemsProperty).Subscribe(this.ItemsChanged); @@ -58,11 +65,45 @@ namespace Perspex.Controls set { this.SetValue(ItemsPanelProperty, value); } } + IReadOnlyPerspexList ILogical.LogicalChildren + { + get + { + if (this.logicalChildren == null) + { + this.logicalChildren = new PerspexReadOnlyListView( + new PerspexList(), + x => (ILogical)x); + } + + return this.logicalChildren; + } + } + protected virtual ItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator(this); } + protected override void OnTemplateApplied() + { + if (this.logicalChildren != null) + { + this.logicalChildren.Dispose(); + this.logicalChildren = null; + } + + this.presenter = this.FindTemplateChild("presenter"); + + if (presenter != null) + { + var panel = (IVisual)this.presenter.GetVisualChildren().Single(); + this.logicalChildren = new PerspexReadOnlyListView( + panel.VisualChildren, + x => (ILogical)x); + } + } + private void ItemsChanged(Tuple value) { INotifyPropertyChanged inpc = value.Item1 as INotifyPropertyChanged; diff --git a/Perspex.Controls/Panel.cs b/Perspex.Controls/Panel.cs index d449f4da05..4a1db10ac6 100644 --- a/Perspex.Controls/Panel.cs +++ b/Perspex.Controls/Panel.cs @@ -62,6 +62,13 @@ namespace Perspex.Controls private void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) { + var logicalParent = (Control)this; + + while (logicalParent.TemplatedParent != null) + { + logicalParent = (Control)logicalParent.TemplatedParent; + } + // TODO: Handle Move and Replace. switch (e.Action) { @@ -70,7 +77,7 @@ namespace Perspex.Controls foreach (var child in e.NewItems.OfType()) { - child.Parent = this; + child.Parent = logicalParent; } break; diff --git a/Perspex.Controls/Presenters/ItemsPresenter.cs b/Perspex.Controls/Presenters/ItemsPresenter.cs index 8830122696..b2de51a674 100644 --- a/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -26,7 +26,7 @@ namespace Perspex.Controls.Presenters public ItemsPresenter() { - this.GetObservableWithHistory(ItemsProperty).Skip(1).Subscribe(this.ItemsChanged); + this.GetObservableWithHistory(ItemsProperty).Subscribe(this.ItemsChanged); } public IEnumerable Items From 97c557512c388e0c328803612f13880e21489d72 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 26 Dec 2014 22:32:01 +0000 Subject: [PATCH 08/11] Oops, only commited half of last change. --- .../Collections/PerspexReadOnlyListView.cs | 93 ++++++ Perspex.Base/Perspex.Base.csproj | 1 + .../ItemsControlTests.cs | 294 ++++++++++++++++++ .../Perspex.Controls.UnitTests.csproj | 1 + 4 files changed, 389 insertions(+) create mode 100644 Perspex.Base/Collections/PerspexReadOnlyListView.cs create mode 100644 Perspex.Controls.UnitTests/ItemsControlTests.cs diff --git a/Perspex.Base/Collections/PerspexReadOnlyListView.cs b/Perspex.Base/Collections/PerspexReadOnlyListView.cs new file mode 100644 index 0000000000..dd5f8af61b --- /dev/null +++ b/Perspex.Base/Collections/PerspexReadOnlyListView.cs @@ -0,0 +1,93 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Collections +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; + using System.Linq; + + public class PerspexReadOnlyListView : IReadOnlyPerspexList, IDisposable + { + private IReadOnlyPerspexList inner; + + private Func convert; + + public PerspexReadOnlyListView( + IReadOnlyPerspexList inner, + Func convert) + { + this.inner = inner; + this.convert = convert; + this.inner.CollectionChanged += this.InnerCollectionChanged; + } + + public TOut this[int index] + { + get { return this.convert(this.inner[index]); } + } + + public int Count + { + get { return this.inner.Count; } + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + + public event PropertyChangedEventHandler PropertyChanged; + + public void Dispose() + { + this.inner.CollectionChanged -= this.InnerCollectionChanged; + } + + public IEnumerator GetEnumerator() + { + return this.inner.Select(convert).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + private IList ConvertList(IList list) + { + return list.Cast().Select(this.convert).ToList(); + } + + private void InnerCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (this.CollectionChanged != null) + { + NotifyCollectionChangedEventArgs ev; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + ev = new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + this.ConvertList(e.NewItems), + e.NewStartingIndex); + break; + case NotifyCollectionChangedAction.Remove: + ev = new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + this.ConvertList(e.OldItems), + e.NewStartingIndex); + break; + default: + throw new NotSupportedException("Action not yet implemented."); + } + + this.CollectionChanged(this, ev); + } + } + } +} diff --git a/Perspex.Base/Perspex.Base.csproj b/Perspex.Base/Perspex.Base.csproj index 1ddb527ce9..6959c65d9d 100644 --- a/Perspex.Base/Perspex.Base.csproj +++ b/Perspex.Base/Perspex.Base.csproj @@ -36,6 +36,7 @@ + diff --git a/Perspex.Controls.UnitTests/ItemsControlTests.cs b/Perspex.Controls.UnitTests/ItemsControlTests.cs new file mode 100644 index 0000000000..72a0f98a33 --- /dev/null +++ b/Perspex.Controls.UnitTests/ItemsControlTests.cs @@ -0,0 +1,294 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls.UnitTests +{ + using System; + using System.Collections.Specialized; + using System.Linq; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; + using Perspex.Collections; + using Perspex.Controls; + using Perspex.Controls.Presenters; + using Perspex.Layout; + using Perspex.Platform; + using Perspex.Styling; + using Perspex.VisualTree; + using Ploeh.AutoFixture; + using Ploeh.AutoFixture.AutoMoq; + using Splat; + + [TestClass] + public class ItemsControlTests + { + [TestMethod] + public void Template_Should_Be_Instantiated() + { + using (var ctx = this.RegisterServices()) + { + var target = new ItemsControl(); + target.Items = new[] { "Foo" }; + target.Template = this.GetTemplate(); + + target.Measure(new Size(100, 100)); + + var child = ((IVisual)target).VisualChildren.Single(); + Assert.IsInstanceOfType(child, typeof(Border)); + child = child.VisualChildren.Single(); + Assert.IsInstanceOfType(child, typeof(ItemsPresenter)); + child = child.VisualChildren.Single(); + Assert.IsInstanceOfType(child, typeof(StackPanel)); + child = child.VisualChildren.Single(); + Assert.IsInstanceOfType(child, typeof(TextBlock)); + } + } + + [TestMethod] + public void Templated_Children_Should_Be_Styled() + { + using (var ctx = this.RegisterServices()) + { + var root = new TestRoot(); + var target = new ItemsControl(); + var styler = new Mock(); + + Locator.CurrentMutable.Register(() => styler.Object, typeof(IStyler)); + target.Items = new[] { "Foo" }; + target.Template = this.GetTemplate(); + root.Content = target; + + target.ApplyTemplate(); + + styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once()); + styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once()); + styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once()); + styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once()); + styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once()); + } + } + + [TestMethod] + public void ItemsPresenter_And_Panel_Should_Have_TemplatedParent_Set() + { + var target = new ItemsControl(); + + target.Template = this.GetTemplate(); + target.Items = new[] { "Foo" }; + target.ApplyTemplate(); + + var presenter = target.GetTemplateControls().OfType().Single(); + var panel = target.GetTemplateControls().OfType().Single(); + + Assert.AreEqual(target, presenter.TemplatedParent); + Assert.AreEqual(target, panel.TemplatedParent); + } + + [TestMethod] + public void Item_Should_Have_TemplatedParent_Set_To_Null() + { + var target = new ItemsControl(); + + target.Template = this.GetTemplate(); + target.Items = new[] { "Foo" }; + target.ApplyTemplate(); + + var panel = target.GetTemplateControls().OfType().Single(); + var item = (TextBlock)panel.GetVisualChildren().First(); + + Assert.IsNull(item.TemplatedParent); + } + + [TestMethod] + public void Control_Item_Should_Set_Control_Parent() + { + var target = new ItemsControl(); + var child = new Control(); + + target.Template = this.GetTemplate(); + target.Items = new[] { child }; + target.ApplyTemplate(); + + Assert.AreEqual(target, child.Parent); + Assert.AreEqual(target, ((ILogical)child).LogicalParent); + } + + [TestMethod] + public void Clearing_Control_Item_Should_Clear_Child_Controls_Parent() + { + var target = new ItemsControl(); + var child = new Control(); + + target.Template = this.GetTemplate(); + target.Items = new[] { child }; + target.ApplyTemplate(); + target.Items = null; + + Assert.IsNull(child.Parent); + Assert.IsNull(((ILogical)child).LogicalParent); + } + + [TestMethod] + public void Control_Item_Should_Make_Control_Appear_In_LogicalChildren() + { + var target = new ItemsControl(); + var child = new Control(); + + target.Template = this.GetTemplate(); + target.Items = new[] { child }; + target.ApplyTemplate(); + + CollectionAssert.AreEqual(new[] { child }, ((ILogical)target).LogicalChildren.ToList()); + } + + [TestMethod] + public void String_Item_Should_Make_TextBlock_Appear_In_LogicalChildren() + { + var target = new ItemsControl(); + var child = new Control(); + + target.Template = this.GetTemplate(); + target.Items = new[] { "Foo" }; + target.ApplyTemplate(); + + var logical = (ILogical)target; + Assert.AreEqual(1, logical.LogicalChildren.Count); + Assert.IsInstanceOfType(logical.LogicalChildren[0], typeof(TextBlock)); + } + + [TestMethod] + public void Setting_Items_To_Null_Should_Remove_LogicalChildren() + { + var target = new ItemsControl(); + var child = new Control(); + + target.Template = this.GetTemplate(); + target.Items = new[] { "Foo" }; + target.ApplyTemplate(); + target.Items = null; + + CollectionAssert.AreEqual(new ILogical[0], ((ILogical)target).LogicalChildren.ToList()); + } + + [TestMethod] + public void Setting_Items_Should_Fire_LogicalChildren_CollectionChanged() + { + var target = new ItemsControl(); + var child = new Control(); + var called = false; + + target.Template = this.GetTemplate(); + target.ApplyTemplate(); + + ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => + called = e.Action == NotifyCollectionChangedAction.Add; + + target.Items = new[] { child }; + + Assert.IsTrue(called); + } + + [TestMethod] + public void Setting_Items_To_Null_Should_Fire_LogicalChildren_CollectionChanged() + { + var target = new ItemsControl(); + var child = new Control(); + var called = false; + + target.Template = this.GetTemplate(); + target.Items = new[] { child }; + target.ApplyTemplate(); + + ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => + called = e.Action == NotifyCollectionChangedAction.Remove; + + target.Items = null; + + Assert.IsTrue(called); + } + + [TestMethod] + public void Changing_Items_Should_Fire_LogicalChildren_CollectionChanged() + { + var target = new ItemsControl(); + var child = new Control(); + var called = false; + + target.Template = this.GetTemplate(); + target.Items = new[] { child }; + target.ApplyTemplate(); + + ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true; + + target.Items = new[] { "Foo" }; + + Assert.IsTrue(called); + } + + [TestMethod] + public void Adding_Items_Should_Fire_LogicalChildren_CollectionChanged() + { + var target = new ItemsControl(); + var items = new PerspexList { "Foo" }; + var called = false; + + target.Template = this.GetTemplate(); + target.Items = items; + target.ApplyTemplate(); + + ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => + called = e.Action == NotifyCollectionChangedAction.Add; + + items.Add("Bar"); + + Assert.IsTrue(called); + } + + [TestMethod] + public void Removing_Items_Should_Fire_LogicalChildren_CollectionChanged() + { + var target = new ItemsControl(); + var items = new PerspexList { "Foo", "Bar" }; + var called = false; + + target.Template = this.GetTemplate(); + target.Items = items; + target.ApplyTemplate(); + + ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => + called = e.Action == NotifyCollectionChangedAction.Remove; + + items.Remove("Bar"); + + Assert.IsTrue(called); + } + + private ControlTemplate GetTemplate() + { + return ControlTemplate.Create(parent => + { + return new Border + { + Background = new Perspex.Media.SolidColorBrush(0xffffffff), + Content = new ItemsPresenter + { + Id = "presenter", + [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + } + }; + }); + } + + private IDisposable RegisterServices() + { + var result = Locator.CurrentMutable.WithResolver(); + var fixture = new Fixture().Customize(new AutoMoqCustomization()); + var renderInterface = fixture.Create(); + Locator.CurrentMutable.RegisterConstant(renderInterface, typeof(IPlatformRenderInterface)); + return result; + } + } +} diff --git a/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index c16687597e..35214ff6b9 100644 --- a/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -64,6 +64,7 @@ + From 2c1d2cb7a8e404845fd5e4fa1d92b82235d4b147 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 27 Dec 2014 11:14:13 +0000 Subject: [PATCH 09/11] Display logical tree in DevTools. --- .../ContentControlTests.cs | 8 +-- .../ItemsControlTests.cs | 2 +- Perspex.Controls/ContentControl.cs | 2 +- Perspex.Controls/Control.cs | 9 +++ Perspex.Controls/ItemsControl.cs | 4 +- Perspex.Controls/Primitives/TabStrip.cs | 20 +++++- .../Primitives/TemplatedControl.cs | 4 +- Perspex.Controls/TabControl.cs | 21 ++++++ Perspex.Diagnostics/DevTools.cs | 69 +++++++++++++++---- .../Perspex.Diagnostics.csproj | 2 + .../ViewModels/ControlDetails.cs | 10 ++- .../ViewModels/LogicalTreeNode.cs | 29 ++++++++ Perspex.Diagnostics/ViewModels/TreeNode.cs | 63 +++++++++++++++++ .../ViewModels/VisualTreeNode.cs | 41 +++-------- Perspex.Themes.Default/ButtonStyle.cs | 2 +- Perspex.Themes.Default/CheckBoxStyle.cs | 2 +- Perspex.Themes.Default/ContentControlStyle.cs | 2 +- Perspex.Themes.Default/RadioButtonStyle.cs | 2 +- Perspex.Themes.Default/ScrollViewerStyle.cs | 2 +- Perspex.Themes.Default/TabControlStyle.cs | 1 + Perspex.Themes.Default/TabItemStyle.cs | 2 +- Perspex.Themes.Default/TabStripStyle.cs | 1 + Perspex.Themes.Default/ToggleButtonStyle.cs | 2 +- Perspex.Themes.Default/WindowStyle.cs | 2 +- 24 files changed, 227 insertions(+), 75 deletions(-) create mode 100644 Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs create mode 100644 Perspex.Diagnostics/ViewModels/TreeNode.cs diff --git a/Perspex.Controls.UnitTests/ContentControlTests.cs b/Perspex.Controls.UnitTests/ContentControlTests.cs index 5ef6f09a58..9eee0c3fd6 100644 --- a/Perspex.Controls.UnitTests/ContentControlTests.cs +++ b/Perspex.Controls.UnitTests/ContentControlTests.cs @@ -174,7 +174,7 @@ namespace Perspex.Controls.UnitTests contentControl.ApplyTemplate(); // Need to call ApplyTemplate on presenter for CollectionChanged to be called. - var presenter = contentControl.GetTemplateControls().Single(x => x.Id == "presenter"); + var presenter = contentControl.GetTemplateControls().Single(x => x.Id == "contentPresenter"); presenter.ApplyTemplate(); Assert.IsTrue(called); @@ -197,7 +197,7 @@ namespace Perspex.Controls.UnitTests contentControl.Content = null; // Need to call ApplyTemplate on presenter for CollectionChanged to be called. - var presenter = contentControl.GetTemplateControls().Single(x => x.Id == "presenter"); + var presenter = contentControl.GetTemplateControls().Single(x => x.Id == "contentPresenter"); presenter.ApplyTemplate(); Assert.IsTrue(called); @@ -221,7 +221,7 @@ namespace Perspex.Controls.UnitTests contentControl.Content = child2; // Need to call ApplyTemplate on presenter for CollectionChanged to be called. - var presenter = contentControl.GetTemplateControls().Single(x => x.Id == "presenter"); + var presenter = contentControl.GetTemplateControls().Single(x => x.Id == "contentPresenter"); presenter.ApplyTemplate(); Assert.IsTrue(called); @@ -236,7 +236,7 @@ namespace Perspex.Controls.UnitTests Background = new Perspex.Media.SolidColorBrush(0xffffffff), Content = new ContentPresenter { - Id = "presenter", + Id = "contentPresenter", [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], } }; diff --git a/Perspex.Controls.UnitTests/ItemsControlTests.cs b/Perspex.Controls.UnitTests/ItemsControlTests.cs index 72a0f98a33..494d226ee8 100644 --- a/Perspex.Controls.UnitTests/ItemsControlTests.cs +++ b/Perspex.Controls.UnitTests/ItemsControlTests.cs @@ -275,7 +275,7 @@ namespace Perspex.Controls.UnitTests Background = new Perspex.Media.SolidColorBrush(0xffffffff), Content = new ItemsPresenter { - Id = "presenter", + Id = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], } }; diff --git a/Perspex.Controls/ContentControl.cs b/Perspex.Controls/ContentControl.cs index d51c85176b..2beec51b93 100644 --- a/Perspex.Controls/ContentControl.cs +++ b/Perspex.Controls/ContentControl.cs @@ -79,7 +79,7 @@ namespace Perspex.Controls this.presenterSubscription = null; } - this.presenter = this.FindTemplateChild("presenter"); + this.presenter = this.FindTemplateChild("contentPresenter"); if (this.presenter != null) { diff --git a/Perspex.Controls/Control.cs b/Perspex.Controls/Control.cs index 1e4bff5d94..e364b7519d 100644 --- a/Perspex.Controls/Control.cs +++ b/Perspex.Controls/Control.cs @@ -32,6 +32,9 @@ namespace Perspex.Controls public static readonly PerspexProperty ParentProperty = PerspexProperty.Register("Parent"); + public static readonly PerspexProperty TagProperty = + PerspexProperty.Register("Tag"); + public static readonly PerspexProperty TemplatedParentProperty = PerspexProperty.Register("TemplatedParent", inherits: true); @@ -156,6 +159,12 @@ namespace Perspex.Controls internal set { this.SetValue(ParentProperty, value); } } + public object Tag + { + get { return this.GetValue(TagProperty); } + set { this.SetValue(TagProperty, value); } + } + public ITemplatedControl TemplatedParent { get { return this.GetValue(TemplatedParentProperty); } diff --git a/Perspex.Controls/ItemsControl.cs b/Perspex.Controls/ItemsControl.cs index 133fa41559..7767904df4 100644 --- a/Perspex.Controls/ItemsControl.cs +++ b/Perspex.Controls/ItemsControl.cs @@ -93,9 +93,9 @@ namespace Perspex.Controls this.logicalChildren = null; } - this.presenter = this.FindTemplateChild("presenter"); + this.presenter = this.FindTemplateChild("itemsPresenter"); - if (presenter != null) + if (this.presenter != null) { var panel = (IVisual)this.presenter.GetVisualChildren().Single(); this.logicalChildren = new PerspexReadOnlyListView( diff --git a/Perspex.Controls/Primitives/TabStrip.cs b/Perspex.Controls/Primitives/TabStrip.cs index 03d3709a75..91b13da1c6 100644 --- a/Perspex.Controls/Primitives/TabStrip.cs +++ b/Perspex.Controls/Primitives/TabStrip.cs @@ -7,14 +7,15 @@ namespace Perspex.Controls.Primitives { using System; - using System.Collections; using System.Linq; + using System.Reactive.Linq; using Perspex.Controls.Generators; - using Perspex.Controls.Presenters; - using Perspex.Input; public class TabStrip : SelectingItemsControl { + public static readonly PerspexProperty SelectedTabProperty = + TabControl.SelectedTabProperty.AddOwner(); + private static readonly ItemsPanelTemplate PanelTemplate = new ItemsPanelTemplate( () => new StackPanel()); @@ -23,6 +24,19 @@ namespace Perspex.Controls.Primitives ItemsPanelProperty.OverrideDefaultValue(typeof(TabStrip), PanelTemplate); } + public TabStrip() + { + this.Bind( + SelectedTabProperty, + this.GetObservable(SelectedItemProperty).Select(x => x as TabItem)); + } + + public TabItem SelectedTab + { + get { return this.GetValue(SelectedTabProperty); } + private set { this.SetValue(SelectedTabProperty, value); } + } + protected override ItemContainerGenerator CreateItemContainerGenerator() { TabControl tabControl = this.TemplatedParent as TabControl; diff --git a/Perspex.Controls/Primitives/TemplatedControl.cs b/Perspex.Controls/Primitives/TemplatedControl.cs index ffe27a7471..547d7cd605 100644 --- a/Perspex.Controls/Primitives/TemplatedControl.cs +++ b/Perspex.Controls/Primitives/TemplatedControl.cs @@ -138,9 +138,7 @@ namespace Perspex.Controls.Primitives protected T FindTemplateChild(string id) where T : Control { - return this.GetTemplateControls() - .Where(x => x.TemplatedParent == this) - .OfType().FirstOrDefault(x => x.Id == id); + return (T)this.GetTemplateControls().SingleOrDefault(x => x.Id == id); } protected T GetTemplateChild(string id) where T : Control diff --git a/Perspex.Controls/TabControl.cs b/Perspex.Controls/TabControl.cs index 5e71dd7b7f..2790cf034e 100644 --- a/Perspex.Controls/TabControl.cs +++ b/Perspex.Controls/TabControl.cs @@ -9,6 +9,7 @@ namespace Perspex.Controls using System; using System.Linq; using System.Reactive.Linq; + using Perspex.Collections; using Perspex.Controls.Generators; using Perspex.Controls.Primitives; @@ -17,6 +18,9 @@ namespace Perspex.Controls public static readonly PerspexProperty SelectedContentProperty = PerspexProperty.Register("SelectedContent"); + public static readonly PerspexProperty SelectedTabProperty = + PerspexProperty.Register("SelectedTab"); + private TabStrip tabStrip; public TabControl() @@ -27,6 +31,22 @@ namespace Perspex.Controls object content = (c != null) ? c.Content : null; this.SetValue(SelectedContentProperty, content); }); + + this.Bind( + SelectedTabProperty, + this.GetObservable(SelectedItemProperty).Select(x => x as TabItem)); + } + + public object SelectedContent + { + get { return this.GetValue(SelectedContentProperty); } + set { this.SetValue(SelectedContentProperty, value); } + } + + public TabItem SelectedTab + { + get { return this.GetValue(SelectedTabProperty); } + private set { this.SetValue(SelectedTabProperty, value); } } protected override ItemContainerGenerator CreateItemContainerGenerator() @@ -36,6 +56,7 @@ namespace Perspex.Controls protected override void OnTemplateApplied() { + base.OnTemplateApplied(); this.tabStrip = this.GetTemplateControls().OfType().FirstOrDefault(); this.BindTwoWay(TabControl.SelectedItemProperty, this.tabStrip, TabControl.SelectedItemProperty); } diff --git a/Perspex.Diagnostics/DevTools.cs b/Perspex.Diagnostics/DevTools.cs index 38abb63506..bf8576a2a8 100644 --- a/Perspex.Diagnostics/DevTools.cs +++ b/Perspex.Diagnostics/DevTools.cs @@ -10,6 +10,7 @@ namespace Perspex.Diagnostics using System.Reactive.Disposables; using System.Reactive.Linq; using Perspex.Controls; + using Perspex.Controls.Primitives; using Perspex.Diagnostics.ViewModels; using Perspex.Input; using ReactiveUI; @@ -21,23 +22,43 @@ namespace Perspex.Diagnostics public DevTools() { - var treeView = new TreeView + var treePane = new Grid { - DataTemplates = new DataTemplates + RowDefinitions = new RowDefinitions { - new TreeDataTemplate(GetHeader, x => x.Children), + new RowDefinition(GridLength.Auto), + new RowDefinition(new GridLength(1, GridUnitType.Star)), }, - [!TreeView.ItemsProperty] = this[!DevTools.RootProperty].Select(x => + Children = new Controls { - if (x != null) + (var tabStrip = new TabStrip { - return new[] { new VisualTreeNode((IVisual)x) }; - } - else + Items = new[] + { + new TabItem + { + Header = "Logical Tree", + IsSelected = true, + [!TabItem.TagProperty] = this[!RootProperty].Select(x => LogicalTreeNode.Create(x)), + }, + new TabItem + { + Header = "Visual Tree", + [!TabItem.TagProperty] = this[!RootProperty].Select(x => VisualTreeNode.Create(x)), + } + }, + }), + (var treeView = new TreeView { - return null; - } - }), + DataTemplates = new DataTemplates + { + new TreeDataTemplate(GetHeader, x => x.Children), + new TreeDataTemplate(GetHeader, x => x.Children), + }, + [!TreeView.ItemsProperty] = tabStrip.WhenAnyValue(x => x.SelectedTab.Tag), + [Grid.RowProperty] = 1, + }) + } }; var detailsView = new ContentControl @@ -48,8 +69,8 @@ namespace Perspex.Diagnostics }, [!ContentControl.ContentProperty] = treeView[!TreeView.SelectedItemProperty] .Where(x => x != null) - .Cast() - .Select(x => new ControlDetails(x.Visual)), + .Cast() + .Select(x => new ControlDetails(x.Control)), [Grid.ColumnProperty] = 2, }; @@ -69,7 +90,7 @@ namespace Perspex.Diagnostics }, Children = new Controls { - treeView, + treePane, splitter, detailsView, } @@ -127,6 +148,26 @@ namespace Perspex.Diagnostics }; } + private static Control GetHeader(LogicalTreeNode node) + { + return new StackPanel + { + Orientation = Orientation.Horizontal, + Gap = 8, + Children = new Controls + { + new TextBlock + { + Text = node.Type, + }, + new TextBlock + { + [!TextBlock.TextProperty] = node.WhenAnyValue(x => x.Classes), + } + } + }; + } + private static Control GetHeader(VisualTreeNode node) { return new StackPanel diff --git a/Perspex.Diagnostics/Perspex.Diagnostics.csproj b/Perspex.Diagnostics/Perspex.Diagnostics.csproj index c33de242fd..53948a659a 100644 --- a/Perspex.Diagnostics/Perspex.Diagnostics.csproj +++ b/Perspex.Diagnostics/Perspex.Diagnostics.csproj @@ -76,6 +76,8 @@ + + diff --git a/Perspex.Diagnostics/ViewModels/ControlDetails.cs b/Perspex.Diagnostics/ViewModels/ControlDetails.cs index f6fa82fdb4..7d1d9ff301 100644 --- a/Perspex.Diagnostics/ViewModels/ControlDetails.cs +++ b/Perspex.Diagnostics/ViewModels/ControlDetails.cs @@ -8,18 +8,16 @@ namespace Perspex.Diagnostics.ViewModels { using System.Collections.Generic; using System.Linq; - using Perspex.Styling; + using Perspex.Controls; using ReactiveUI; internal class ControlDetails : ReactiveObject { - public ControlDetails(IVisual visual) + public ControlDetails(Control control) { - PerspexObject po = visual as PerspexObject; - - if (po != null) + if (control != null) { - this.Properties = po.GetAllValues() + this.Properties = control.GetAllValues() .Select(x => new PropertyDetails(x)) .OrderBy(x => x.Name); } diff --git a/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs b/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs new file mode 100644 index 0000000000..cf77931eb2 --- /dev/null +++ b/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Diagnostics.ViewModels +{ + using System; + using Perspex.Controls; + using ReactiveUI; + + internal class LogicalTreeNode : TreeNode + { + private string classes; + + public LogicalTreeNode(ILogical logical) + : base((Control)logical) + { + this.Children = logical.LogicalChildren.CreateDerivedCollection(x => new LogicalTreeNode(x)); + } + + public static LogicalTreeNode[] Create(object control) + { + var logical = control as ILogical; + return logical != null ? new[] { new LogicalTreeNode(logical) } : null; + } + } +} diff --git a/Perspex.Diagnostics/ViewModels/TreeNode.cs b/Perspex.Diagnostics/ViewModels/TreeNode.cs new file mode 100644 index 0000000000..c0ecfbb395 --- /dev/null +++ b/Perspex.Diagnostics/ViewModels/TreeNode.cs @@ -0,0 +1,63 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Diagnostics.ViewModels +{ + using System; + using System.Reactive; + using System.Reactive.Linq; + using Perspex.Controls; + using ReactiveUI; + + internal class TreeNode : ReactiveObject + { + private string classes; + + public TreeNode(Control control) + { + this.Control = control; + this.Type = control.GetType().Name; + + control.Classes.Changed.Select(_ => Unit.Default) + .StartWith(Unit.Default) + .Subscribe(_ => + { + if (control.Classes.Count > 0) + { + this.Classes = "(" + string.Join(" ", control.Classes) + ")"; + } + else + { + this.Classes = string.Empty; + } + }); + } + + public IReactiveDerivedList Children + { + get; + protected set; + } + + public string Classes + { + get { return this.classes; } + private set { this.RaiseAndSetIfChanged(ref this.classes, value); } + } + + public string Type + { + get; + private set; + } + + public Control Control + { + get; + private set; + } + } +} diff --git a/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs b/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs index cd96c02f8a..e76983e2c0 100644 --- a/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs +++ b/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs @@ -6,55 +6,30 @@ namespace Perspex.Diagnostics.ViewModels { - using System; - using System.Reactive; - using System.Reactive.Linq; using Perspex.Controls; - using Perspex.Styling; using ReactiveUI; - internal class VisualTreeNode : ReactiveObject + internal class VisualTreeNode : TreeNode { private string classes; public VisualTreeNode(IVisual visual) + : base((Control)visual) { this.Children = visual.VisualChildren.CreateDerivedCollection(x => new VisualTreeNode(x)); - this.Type = visual.GetType().Name; - this.Visual = visual; - Control control = visual as Control; - - if (control != null) + if (this.Control != null) { - this.IsInTemplate = control.TemplatedParent != null; - - control.Classes.Changed.Select(_ => Unit.Default).StartWith(Unit.Default).Subscribe(_ => - { - if (control.Classes.Count > 0) - { - this.Classes = "(" + string.Join(" ", control.Classes) + ")"; - } - else - { - this.Classes = string.Empty; - } - }); + this.IsInTemplate = this.Control.TemplatedParent != null; } } - public IReactiveDerivedList Children { get; private set; } + public bool IsInTemplate { get; private set; } - public string Classes + public static VisualTreeNode[] Create(object control) { - get { return this.classes; } - private set { this.RaiseAndSetIfChanged(ref this.classes, value); } + var visual = control as IVisual; + return visual != null ? new[] { new VisualTreeNode(visual) } : null; } - - public bool IsInTemplate { get; private set; } - - public string Type { get; private set; } - - public IVisual Visual { get; private set; } } } diff --git a/Perspex.Themes.Default/ButtonStyle.cs b/Perspex.Themes.Default/ButtonStyle.cs index e32d4517ff..bc64c8f5f1 100644 --- a/Perspex.Themes.Default/ButtonStyle.cs +++ b/Perspex.Themes.Default/ButtonStyle.cs @@ -78,7 +78,7 @@ namespace Perspex.Themes.Default Padding = new Thickness(3), Content = new ContentPresenter { - Id = "presenter", + Id = "contentPresenter", [~ContentPresenter.ContentProperty] = control[~Button.ContentProperty], [~ContentPresenter.HorizontalAlignmentProperty] = control[~Button.HorizontalContentAlignmentProperty], [~ContentPresenter.VerticalAlignmentProperty] = control[~Button.VerticalContentAlignmentProperty], diff --git a/Perspex.Themes.Default/CheckBoxStyle.cs b/Perspex.Themes.Default/CheckBoxStyle.cs index 2ee0b0105c..2c8c9ed48b 100644 --- a/Perspex.Themes.Default/CheckBoxStyle.cs +++ b/Perspex.Themes.Default/CheckBoxStyle.cs @@ -83,7 +83,7 @@ namespace Perspex.Themes.Default }, new ContentPresenter { - Id = "presenter", + Id = "contentPresenter", Margin = new Thickness(4, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, [~ContentPresenter.ContentProperty] = control[~CheckBox.ContentProperty], diff --git a/Perspex.Themes.Default/ContentControlStyle.cs b/Perspex.Themes.Default/ContentControlStyle.cs index 1dac4417f9..4060c3bd44 100644 --- a/Perspex.Themes.Default/ContentControlStyle.cs +++ b/Perspex.Themes.Default/ContentControlStyle.cs @@ -32,7 +32,7 @@ namespace Perspex.Themes.Default { return new ContentPresenter { - Id = "presenter", + Id = "contentPresenter", [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], }; } diff --git a/Perspex.Themes.Default/RadioButtonStyle.cs b/Perspex.Themes.Default/RadioButtonStyle.cs index ac56bdcd61..0393f4ea86 100644 --- a/Perspex.Themes.Default/RadioButtonStyle.cs +++ b/Perspex.Themes.Default/RadioButtonStyle.cs @@ -82,7 +82,7 @@ namespace Perspex.Themes.Default }, new ContentPresenter { - Id = "presenter", + Id = "contentPresenter", Margin = new Thickness(4, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, [~ContentPresenter.ContentProperty] = control[~RadioButton.ContentProperty], diff --git a/Perspex.Themes.Default/ScrollViewerStyle.cs b/Perspex.Themes.Default/ScrollViewerStyle.cs index a1d51c9024..e87a802503 100644 --- a/Perspex.Themes.Default/ScrollViewerStyle.cs +++ b/Perspex.Themes.Default/ScrollViewerStyle.cs @@ -47,7 +47,7 @@ namespace Perspex.Themes.Default { new ScrollContentPresenter { - Id = "presenter", + Id = "scrollContentPresenter", [~ScrollContentPresenter.ContentProperty] = control[~ScrollViewer.ContentProperty], [~~ScrollContentPresenter.ExtentProperty] = control[~~ScrollViewer.ExtentProperty], [~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty], diff --git a/Perspex.Themes.Default/TabControlStyle.cs b/Perspex.Themes.Default/TabControlStyle.cs index b902e46a75..b3b919374a 100644 --- a/Perspex.Themes.Default/TabControlStyle.cs +++ b/Perspex.Themes.Default/TabControlStyle.cs @@ -46,6 +46,7 @@ namespace Perspex.Themes.Default }, new ContentPresenter { + Id = "contentPresenter", [~ContentPresenter.ContentProperty] = control[~TabControl.SelectedContentProperty], [Grid.RowProperty] = 1, } diff --git a/Perspex.Themes.Default/TabItemStyle.cs b/Perspex.Themes.Default/TabItemStyle.cs index 93c38f03a4..459e0df884 100644 --- a/Perspex.Themes.Default/TabItemStyle.cs +++ b/Perspex.Themes.Default/TabItemStyle.cs @@ -41,7 +41,7 @@ namespace Perspex.Themes.Default { return new ContentPresenter { - Id = "presenter", + Id = "contentPresenter", [~ContentPresenter.ContentProperty] = control[~TabItem.HeaderProperty], }; } diff --git a/Perspex.Themes.Default/TabStripStyle.cs b/Perspex.Themes.Default/TabStripStyle.cs index 6a46bee554..eed4683886 100644 --- a/Perspex.Themes.Default/TabStripStyle.cs +++ b/Perspex.Themes.Default/TabStripStyle.cs @@ -40,6 +40,7 @@ namespace Perspex.Themes.Default { return new ItemsPresenter { + Id = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], }; diff --git a/Perspex.Themes.Default/ToggleButtonStyle.cs b/Perspex.Themes.Default/ToggleButtonStyle.cs index 038c934143..1106030ea2 100644 --- a/Perspex.Themes.Default/ToggleButtonStyle.cs +++ b/Perspex.Themes.Default/ToggleButtonStyle.cs @@ -93,7 +93,7 @@ namespace Perspex.Themes.Default Padding = new Thickness(3), Content = new ContentPresenter { - Id = "presenter", + Id = "contentPresenter", [~ContentPresenter.ContentProperty] = control[~ToggleButton.ContentProperty], [~ContentPresenter.HorizontalAlignmentProperty] = control[~ToggleButton.HorizontalContentAlignmentProperty], [~ContentPresenter.VerticalAlignmentProperty] = control[~ToggleButton.VerticalContentAlignmentProperty], diff --git a/Perspex.Themes.Default/WindowStyle.cs b/Perspex.Themes.Default/WindowStyle.cs index 0e163d55aa..a6380b5017 100644 --- a/Perspex.Themes.Default/WindowStyle.cs +++ b/Perspex.Themes.Default/WindowStyle.cs @@ -37,7 +37,7 @@ namespace Perspex.Themes.Default [~Border.BackgroundProperty] = control[~Window.BackgroundProperty], Content = new ContentPresenter { - Id = "presenter", + Id = "contentPresenter", [~ContentPresenter.ContentProperty] = control[~Window.ContentProperty], } }; From 7c0935193972cb90922a58fd2ee4f28f043e8d18 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 27 Dec 2014 22:46:19 +0000 Subject: [PATCH 10/11] Make logical tree work for TabControl. It only contains the currently visible tab - I'm not so happy about this... --- .../Collections/SingleItemPerspexList.cs | 2 +- .../Presenters/ContentPresenter.cs | 2 +- Perspex.Controls/TabControl.cs | 29 +++++++++++++++++-- Perspex.Themes.Default/TabItemStyle.cs | 2 +- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/Perspex.Base/Collections/SingleItemPerspexList.cs b/Perspex.Base/Collections/SingleItemPerspexList.cs index 96a629d556..5af1e68caa 100644 --- a/Perspex.Base/Collections/SingleItemPerspexList.cs +++ b/Perspex.Base/Collections/SingleItemPerspexList.cs @@ -80,7 +80,7 @@ namespace Perspex.Collections } else { - e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, this.item); + e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, this.item, 0); this.item = value; } diff --git a/Perspex.Controls/Presenters/ContentPresenter.cs b/Perspex.Controls/Presenters/ContentPresenter.cs index 4646348df4..c0340567c3 100644 --- a/Perspex.Controls/Presenters/ContentPresenter.cs +++ b/Perspex.Controls/Presenters/ContentPresenter.cs @@ -32,7 +32,7 @@ namespace Perspex.Controls.Presenters { get { - return this.Child; + return this.child; } private set diff --git a/Perspex.Controls/TabControl.cs b/Perspex.Controls/TabControl.cs index 2790cf034e..eb9cb0eb11 100644 --- a/Perspex.Controls/TabControl.cs +++ b/Perspex.Controls/TabControl.cs @@ -11,9 +11,10 @@ namespace Perspex.Controls using System.Reactive.Linq; using Perspex.Collections; using Perspex.Controls.Generators; + using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; - public class TabControl : SelectingItemsControl + public class TabControl : SelectingItemsControl, ILogical { public static readonly PerspexProperty SelectedContentProperty = PerspexProperty.Register("SelectedContent"); @@ -23,6 +24,12 @@ namespace Perspex.Controls private TabStrip tabStrip; + private ContentPresenter presenter; + + private IDisposable presenterSubscription; + + private SingleItemPerspexList logicalChild = new SingleItemPerspexList(); + public TabControl() { this.GetObservable(SelectedItemProperty).Subscribe(x => @@ -49,6 +56,11 @@ namespace Perspex.Controls private set { this.SetValue(SelectedTabProperty, value); } } + IReadOnlyPerspexList ILogical.LogicalChildren + { + get { return this.logicalChild; } + } + protected override ItemContainerGenerator CreateItemContainerGenerator() { return new TypedItemContainerGenerator(this); @@ -56,7 +68,20 @@ namespace Perspex.Controls protected override void OnTemplateApplied() { - base.OnTemplateApplied(); + if (this.presenterSubscription != null) + { + this.presenterSubscription.Dispose(); + this.presenterSubscription = null; + } + + this.presenter = this.FindTemplateChild("contentPresenter"); + + if (this.presenter != null) + { + this.presenterSubscription = this.presenter.ChildObservable + .Subscribe(x => this.logicalChild.SingleItem = x); + } + this.tabStrip = this.GetTemplateControls().OfType().FirstOrDefault(); this.BindTwoWay(TabControl.SelectedItemProperty, this.tabStrip, TabControl.SelectedItemProperty); } diff --git a/Perspex.Themes.Default/TabItemStyle.cs b/Perspex.Themes.Default/TabItemStyle.cs index 459e0df884..b3dcba8f76 100644 --- a/Perspex.Themes.Default/TabItemStyle.cs +++ b/Perspex.Themes.Default/TabItemStyle.cs @@ -41,7 +41,7 @@ namespace Perspex.Themes.Default { return new ContentPresenter { - Id = "contentPresenter", + Id = "headerPresenter", [~ContentPresenter.ContentProperty] = control[~TabItem.HeaderProperty], }; } From c4b3c9ffc1753c8104df27fe86986d3e1ac0231e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 27 Dec 2014 22:47:36 +0000 Subject: [PATCH 11/11] Make logical tree work for ScrollViewer. --- Perspex.Themes.Default/ScrollViewerStyle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Perspex.Themes.Default/ScrollViewerStyle.cs b/Perspex.Themes.Default/ScrollViewerStyle.cs index e87a802503..7a14e1cd65 100644 --- a/Perspex.Themes.Default/ScrollViewerStyle.cs +++ b/Perspex.Themes.Default/ScrollViewerStyle.cs @@ -47,7 +47,7 @@ namespace Perspex.Themes.Default { new ScrollContentPresenter { - Id = "scrollContentPresenter", + Id = "contentPresenter", [~ScrollContentPresenter.ContentProperty] = control[~ScrollViewer.ContentProperty], [~~ScrollContentPresenter.ExtentProperty] = control[~~ScrollViewer.ExtentProperty], [~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty],