diff --git a/Perspex.UnitTests/Perspex.UnitTests.csproj b/Perspex.UnitTests/Perspex.UnitTests.csproj index b07df6a6e8..2996a1fe0b 100644 --- a/Perspex.UnitTests/Perspex.UnitTests.csproj +++ b/Perspex.UnitTests/Perspex.UnitTests.csproj @@ -69,6 +69,8 @@ + + diff --git a/Perspex.UnitTests/StyleTests.cs b/Perspex.UnitTests/StyleTests.cs index be88fa4f97..f842c2563a 100644 --- a/Perspex.UnitTests/StyleTests.cs +++ b/Perspex.UnitTests/StyleTests.cs @@ -18,7 +18,7 @@ namespace Perspex.UnitTests [TestMethod] public void Style_With_Only_Type_Selector_Should_Update_Value() { - Style style = new Style(x => x.Select()) + Style style = new Style(x => x.Select().OfType()) { Setters = new[] { @@ -38,7 +38,7 @@ namespace Perspex.UnitTests [TestMethod] public void Style_With_Class_Selector_Should_Update_And_Restore_Value() { - Style style = new Style(x => x.Select().Class("foo")) + Style style = new Style(x => x.Select().OfType().Class("foo")) { Setters = new[] { @@ -62,7 +62,7 @@ namespace Perspex.UnitTests [TestMethod] public void Later_Styles_Should_Override_Earlier() { - Style style1 = new Style(x => x.Select().Class("foo")) + Style style1 = new Style(x => x.Select().OfType().Class("foo")) { Setters = new[] { @@ -70,7 +70,7 @@ namespace Perspex.UnitTests }, }; - Style style2 = new Style(x => x.Select().Class("foo")) + Style style2 = new Style(x => x.Select().OfType().Class("foo")) { Setters = new[] { diff --git a/Perspex.UnitTests/Styling/SelectorTests.cs b/Perspex.UnitTests/Styling/SelectorTests.cs new file mode 100644 index 0000000000..45ec6294a1 --- /dev/null +++ b/Perspex.UnitTests/Styling/SelectorTests.cs @@ -0,0 +1,68 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.UnitTests.Styling +{ + using System; + using System.Linq; + using System.Reactive; + using System.Reactive.Linq; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Perspex.Styling; + using Match = Perspex.Styling.Match; + + [TestClass] + public class SelectorTests + { + [TestMethod] + public void OfType_Matches_Control_Of_Correct_Type() + { + var control = new Class1(); + var target = control.Select().OfType(); + + CollectionAssert.AreEqual(new[] { true }, target.GetActivator().Take(1).ToEnumerable().ToArray()); + } + + [TestMethod] + public void OfType_Doesnt_Match_Control_Of_Wrong_Type() + { + var control = new Class2(); + var target = control.Select().OfType(); + + CollectionAssert.AreEqual(new[] { false }, target.GetActivator().Take(1).ToEnumerable().ToArray()); + } + + [TestMethod] + public void When_OfType_Matches_Control_Other_Selectors_Are_Subscribed() + { + var control = new Class1(); + var target = control.Select().OfType().SubscribeCheck(); + + var result = target.GetActivator().ToEnumerable().Take(1).ToArray(); + + Assert.AreEqual(1, control.SubscribeCheckObservable.SubscribedCount); + } + + [TestMethod] + public void When_OfType_Doesnt_Match_Control_Other_Selectors_Are_Not_Subscribed() + { + var control = new Class1(); + var target = control.Select().OfType().SubscribeCheck(); + + var result = target.GetActivator().ToEnumerable().Take(1).ToArray(); + + Assert.AreEqual(0, control.SubscribeCheckObservable.SubscribedCount); + } + + public class Class1 : SubscribeCheck + { + } + + public class Class2 : SubscribeCheck + { + } + } +} diff --git a/Perspex.UnitTests/Styling/SubscribeCheck.cs b/Perspex.UnitTests/Styling/SubscribeCheck.cs new file mode 100644 index 0000000000..e6cd0cde76 --- /dev/null +++ b/Perspex.UnitTests/Styling/SubscribeCheck.cs @@ -0,0 +1,59 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.UnitTests.Styling +{ + using System; + using System.Reactive.Disposables; + using Perspex.Styling; + using Match = Perspex.Styling.Match; + + public class TestObservable : IObservable + { + public int SubscribedCount { get; private set; } + + public IDisposable Subscribe(IObserver observer) + { + ++this.SubscribedCount; + observer.OnNext(true); + return Disposable.Create(() => --this.SubscribedCount); + } + } + + public class SubscribeCheck : IStyleable + { + public SubscribeCheck() + { + this.Classes = new PerspexList(); + this.SubscribeCheckObservable = new TestObservable(); + } + + public PerspexList Classes + { + get; + private set; + } + + public TestObservable SubscribeCheckObservable + { + get; + private set; + } + + public virtual void SetValue(PerspexProperty property, object value, IObservable activator) + { + } + } + + public static class TestSelectors + { + public static Match SubscribeCheck(this Match match) + { + match.Observables.Add(((SubscribeCheck)match.Control).SubscribeCheckObservable); + return match; + } + } +} diff --git a/Perspex/Perspex.csproj b/Perspex/Perspex.csproj index ed8045927f..8417ba0679 100644 --- a/Perspex/Perspex.csproj +++ b/Perspex/Perspex.csproj @@ -79,6 +79,7 @@ + diff --git a/Perspex/Styling/Activator.cs b/Perspex/Styling/Activator.cs new file mode 100644 index 0000000000..d8739ae709 --- /dev/null +++ b/Perspex/Styling/Activator.cs @@ -0,0 +1,89 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Styling +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Reactive.Disposables; + + public class Activator : IObservable + { + List values = new List(); + + List subscriptions = new List(); + + List> observers = new List>(); + + bool last = false; + + public Activator(IList> observables) + { + int i = 0; + + foreach (IObservable o in observables.Reverse()) + { + int iCaptured = i; + + this.values.Add(false); + + IDisposable subscription = o.Subscribe( + x => this.Update(iCaptured, x), + x => this.Finish(iCaptured), + () => this.Finish(iCaptured)); + + this.subscriptions.Add(subscription); + + ++i; + } + } + + public IDisposable Subscribe(IObserver observer) + { + Contract.Requires(observer != null); + + this.observers.Add(observer); + observer.OnNext(last); + return Disposable.Create(() => this.observers.Remove(observer)); + } + + private void Update(int index, bool value) + { + this.values[index] = value; + + bool current = this.values.All(x => x); + + if (current != last) + { + this.Push(current); + last = current; + } + } + + private void Finish(int i) + { + if (!this.values[i]) + { + // If the observable has finished on 'false' then it will never go back to true + // so we can unsubscribe from all the other subscriptions now. + foreach (IDisposable subscription in this.subscriptions) + { + subscription.Dispose(); + } + } + } + + private void Push(bool value) + { + foreach (IObserver observer in this.observers) + { + observer.OnNext(value); + } + } + } +} diff --git a/Perspex/Styling/Match.cs b/Perspex/Styling/Match.cs index fe74a97576..a05cf66010 100644 --- a/Perspex/Styling/Match.cs +++ b/Perspex/Styling/Match.cs @@ -14,54 +14,38 @@ namespace Perspex.Styling public class Match { - public IStyleable Control + public Match(IStyleable control) { - get; - set; + this.Control = control; + this.Observables = new List>(); } - public IObservable Observable + public IStyleable Control { get; - set; + private set; } - public Match Previous + public List> Observables { get; set; } - public string Token + public string SelectorString { get; set; } - public IObservable GetActivator() + public Activator GetActivator() { - List> observables = new List>(); - Match match = this; - - do - { - if (match.Observable != null) - { - observables.Add(match.Observable); - } - - match = match.Previous; - } - while (match != null); - - return System.Reactive.Linq.Observable.CombineLatest(observables).Select(x => x.All(b => b)); + return new Activator(this.Observables); } public override string ToString() { - string result = (this.Previous != null) ? this.Previous.ToString() : string.Empty; - result += this.Token; - return result; + return this.SelectorString; } } } diff --git a/Perspex/Styling/Selectors.cs b/Perspex/Styling/Selectors.cs index bc8e071a22..f7994dde95 100644 --- a/Perspex/Styling/Selectors.cs +++ b/Perspex/Styling/Selectors.cs @@ -18,47 +18,33 @@ namespace Perspex.Styling public static class Selectors { - public static Match Select(this IStyleable control) + public static Match Select(this IStyleable control) { Contract.Requires(control != null); - if (control is T) - { - return new Match - { - Control = control, - Observable = Observable.Return(true), - Token = typeof(T).Name, - }; - } - else - { - return null; - } + return new Match(control); } - public static Match Class(this Match selector, string name) + public static Match Class(this Match match, string name) { + Contract.Requires(match != null); Contract.Requires(name != null); - if (selector != null) - { - IObservable match = Observable - .Return(selector.Control.Classes.Contains(name)) - .Concat(selector.Control.Classes.Changed.Select(e => selector.Control.Classes.Contains(name))); + match.Observables.Add(Observable + .Return(match.Control.Classes.Contains(name)) + .Concat(match.Control.Classes.Changed.Select(e => match.Control.Classes.Contains(name)))); + match.SelectorString += (name[0] == ':') ? name : '.' + name; - return new Match - { - Control = selector.Control, - Observable = match, - Previous = selector, - Token = (name[0] == ':') ? name : '.' + name, - }; - } - else - { - return null; - } + return match; + } + + public static Match OfType(this Match match) + { + Contract.Requires(match != null); + + match.Observables.Add(Observable.Return(match.Control is T)); + match.SelectorString += typeof(T).Name; + return match; } } } diff --git a/Perspex/Themes/Default/ButtonStyle.cs b/Perspex/Themes/Default/ButtonStyle.cs index f04990d3e8..923dfd010a 100644 --- a/Perspex/Themes/Default/ButtonStyle.cs +++ b/Perspex/Themes/Default/ButtonStyle.cs @@ -15,7 +15,7 @@ namespace Perspex.Themes.Default { this.AddRange(new[] { - new Style(x => x.Select