Browse Source

Only allow pseudoclasses to be set by control.

Limit adding/removing pseudoclasses to IPseudoClasses interface.
pull/366/head
Steven Kirk 10 years ago
parent
commit
db1e20acc8
  1. 4
      samples/TestApplicationShared/GalleryStyle.cs
  2. 2
      samples/TestApplicationShared/MainWindow.cs
  3. 4
      samples/XamlTestApplicationPcl/Views/MainWindow.paml
  4. 10
      src/Perspex.Base/Collections/PerspexList.cs
  5. 4
      src/Perspex.Controls/Button.cs
  6. 228
      src/Perspex.Controls/Classes.cs
  7. 13
      src/Perspex.Controls/Control.cs
  8. 30
      src/Perspex.Controls/ControlExtensions.cs
  9. 25
      src/Perspex.Controls/IPseudoClasses.cs
  10. 21
      src/Perspex.Controls/ItemsControl.cs
  11. 4
      src/Perspex.Controls/Mixins/SelectableMixin.cs
  12. 1
      src/Perspex.Controls/Perspex.Controls.csproj
  13. 9
      src/Perspex.Controls/Primitives/SelectingItemsControl.cs
  14. 9
      src/Perspex.Controls/TreeView.cs
  15. 153
      tests/Perspex.Controls.UnitTests/ClassesTests.cs
  16. 1
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

4
samples/TestApplicationShared/GalleryStyle.cs

@ -19,7 +19,7 @@ namespace TestApplication
{
this.AddRange(new[]
{
new Style (s => s.Class(":container").OfType<TabControl> ())
new Style (s => s.Class("container").OfType<TabControl> ())
{
Setters = new[]
{
@ -27,7 +27,7 @@ namespace TestApplication
}
},
new Style(s => s.Class(":container").OfType<TabControl>().Child().Child().Child().Child().Child().OfType<TabStripItem>())
new Style(s => s.Class("container").OfType<TabControl>().Child().Child().Child().Child().Child().OfType<TabStripItem>())
{
Setters = new[]
{

2
samples/TestApplicationShared/MainWindow.cs

@ -104,7 +104,7 @@ namespace TestApplication
};
container.Classes.Add(":container");
container.Classes.Add("container");
window.Show();
return window;

4
samples/XamlTestApplicationPcl/Views/MainWindow.paml

@ -5,11 +5,11 @@
Title="Perspex Test Application" Width="800" Height="600">
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,*">
<Menu Grid.ColumnSpan="2">
<MenuItem Header="_File">
<MenuItem Header="_File" Background="Red">
<MenuItem Header="_Hello">
<MenuItem Header="_Goodbye"/>
<Separator/>
<MenuItem Header="_World"/>
<MenuItem Header="_World" Background="Red"/>
</MenuItem>
<Separator Background="Red"/>
<MenuItem Header="_Test"/>

10
src/Perspex.Base/Collections/PerspexList.cs

@ -248,6 +248,16 @@ namespace Perspex.Collections
return _inner.GetEnumerator();
}
/// <summary>
/// Gets a range of items from the collection.
/// </summary>
/// <param name="index">The first index to remove.</param>
/// <param name="count">The number of items to remove.</param>
public IEnumerable<T> GetRange(int index, int count)
{
return _inner.GetRange(index, count);
}
/// <summary>
/// Gets the index of the specified item in the collection.
/// </summary>

4
src/Perspex.Controls/Button.cs

@ -215,7 +215,7 @@ namespace Perspex.Controls
{
base.OnPointerPressed(e);
Classes.Add(":pressed");
PseudoClasses.Add(":pressed");
e.Device.Capture(this);
e.Handled = true;
@ -231,7 +231,7 @@ namespace Perspex.Controls
base.OnPointerReleased(e);
e.Device.Capture(null);
Classes.Remove(":pressed");
PseudoClasses.Remove(":pressed");
e.Handled = true;
if (ClickMode == ClickMode.Release && Classes.Contains(":pointerover"))

228
src/Perspex.Controls/Classes.cs

@ -1,39 +1,253 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Perspex.Collections;
namespace Perspex.Controls
{
public class Classes : PerspexList<string>
/// <summary>
/// Holds a collection of style classes for an <see cref="IControl"/>.
/// </summary>
/// <remarks>
/// Similar to CSS, each control may have any number of styling classes applied.
/// </remarks>
public class Classes : PerspexList<string>, IPseudoClasses
{
/// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class.
/// </summary>
public Classes()
{
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class.
/// </summary>
/// <param name="items">The initial items.</param>
public Classes(IEnumerable<string> items)
: base(items)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class.
/// </summary>
/// <param name="items">The initial items.</param>
public Classes(params string[] items)
: base(items)
{
}
public override void Add(string item)
/// <summary>
/// Adds a style class to the collection.
/// </summary>
/// <param name="name">The class name.</param>
/// <remarks>
/// Only standard classes may be added via this method. To add pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override void Add(string name)
{
ThrowIfPseudoclass(name, "added");
if (!Contains(name))
{
base.Add(name);
}
}
/// <summary>
/// Adds a style classes to the collection.
/// </summary>
/// <param name="names">The class names.</param>
/// <remarks>
/// Only standard classes may be added via this method. To add pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override void AddRange(IEnumerable<string> names)
{
var c = new List<string>();
foreach (var name in names)
{
ThrowIfPseudoclass(name, "added");
if (!Contains(name))
{
c.Add(name);
}
}
base.AddRange(c);
}
/// <summary>
/// Inserts a style class into the collection.
/// </summary>
/// <param name="index">The index to insert the class at.</param>
/// <param name="name">The class name.</param>
/// <remarks>
/// Only standard classes may be added via this method. To add pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override void Insert(int index, string name)
{
ThrowIfPseudoclass(name, "added");
if (!Contains(name))
{
base.Insert(index, name);
}
}
/// <summary>
/// Inserts style classes into the collection.
/// </summary>
/// <param name="index">The index to insert the class at.</param>
/// <param name="names">The class names.</param>
/// <remarks>
/// Only standard classes may be added via this method. To add pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override void InsertRange(int index, IEnumerable<string> names)
{
var c = new List<string>();
foreach (var name in names)
{
ThrowIfPseudoclass(name, "added");
if (!Contains(name))
{
c.Add(name);
}
}
base.InsertRange(index, c);
}
/// <summary>
/// Removes a style class from the collection.
/// </summary>
/// <param name="name">The class name.</param>
/// <remarks>
/// Only standard classes may be removed via this method. To remove pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override bool Remove(string name)
{
ThrowIfPseudoclass(name, "removed");
return base.Remove(name);
}
/// <summary>
/// Removes style classes from the collection.
/// </summary>
/// <param name="names">The class name.</param>
/// <remarks>
/// Only standard classes may be removed via this method. To remove pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override void RemoveAll(IEnumerable<string> names)
{
if (!Contains(item))
var c = new List<string>();
foreach (var name in names)
{
base.Add(item);
ThrowIfPseudoclass(name, "removed");
if (!Contains(name))
{
c.Add(name);
}
}
base.RemoveAll(c);
}
/// <summary>
/// Removes a style class from the collection.
/// </summary>
/// <param name="index">The index of the class in the collection.</param>
/// <remarks>
/// Only standard classes may be removed via this method. To remove pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override void RemoveAt(int index)
{
var name = this[index];
ThrowIfPseudoclass(name, "removed");
base.RemoveAt(index);
}
public override void AddRange(IEnumerable<string> items)
/// <summary>
/// Removes style classes from the collection.
/// </summary>
/// <param name="index">The first index to remove.</param>
/// <param name="count">The number of items to remove.</param>
public override void RemoveRange(int index, int count)
{
base.AddRange(items.Where(x => !Contains(x)));
var names = GetRange(index, count);
base.RemoveRange(index, count);
}
/// <summary>
/// Removes all non-pseudoclasses in the collection and adds a new set.
/// </summary>
/// <param name="source">The new contents of the collection.</param>
public void Replace(IList<string> source)
{
var toRemove = new List<string>();
foreach (var name in source)
{
ThrowIfPseudoclass(name, "added");
}
foreach (var name in this)
{
if (!name.StartsWith(":"))
{
toRemove.Add(name);
}
}
base.RemoveAll(toRemove);
base.AddRange(source);
}
/// <inheritdoc/>
void IPseudoClasses.Add(string name)
{
if (!Contains(name))
{
base.Add(name);
}
}
/// <inheritdoc/>
bool IPseudoClasses.Remove(string name)
{
return base.Remove(name);
}
private void ThrowIfPseudoclass(string name, string operation)
{
if (name.StartsWith(":"))
{
throw new ArgumentException(
$"The pseudoclass '{name}' may only be {operation} by the control itself.");
}
}
}
}

13
src/Perspex.Controls/Control.cs

@ -146,8 +146,7 @@ namespace Perspex.Controls
{
if (_classes != value)
{
_classes.Clear();
_classes.AddRange(value);
_classes.Replace(value);
}
}
}
@ -307,6 +306,12 @@ namespace Perspex.Controls
}
}
/// <summary>
/// Gets the <see cref="Classes"/> collection in a form that allows adding and removing
/// pseudoclasses.
/// </summary>
protected IPseudoClasses PseudoClasses => Classes;
/// <summary>
/// Sets the control's logical parent.
/// </summary>
@ -382,11 +387,11 @@ namespace Perspex.Controls
{
if (selector((T)e.NewValue))
{
((Control)e.Sender).Classes.Add(className);
((Control)e.Sender).PseudoClasses.Add(className);
}
else
{
((Control)e.Sender).Classes.Remove(className);
((Control)e.Sender).PseudoClasses.Remove(className);
}
});
}

30
src/Perspex.Controls/ControlExtensions.cs

@ -70,5 +70,35 @@ namespace Perspex.Controls
.Select(x => (x as INameScope) ?? NameScope.GetNameScope(x))
.FirstOrDefault(x => x != null);
}
/// <summary>
/// Adds or removes a pseudoclass depending on a boolean value.
/// </summary>
/// <param name="classes">The pseudoclasses collection.</param>
/// <param name="name">The name of the pseudoclass to set.</param>
/// <param name="value">True to add the pseudoclass or false to remove.</param>
public static void Set(this IPseudoClasses classes, string name, bool value)
{
if (value)
{
classes.Add(name);
}
else
{
classes.Remove(name);
}
}
/// <summary>
/// Sets a pseudoclass depending on an observable trigger.
/// </summary>
/// <param name="classes">The pseudoclasses collection.</param>
/// <param name="name">The name of the pseudoclass to set.</param>
/// <param name="trigger">The trigger: true adds the pseudoclass, false removes.</param>
/// <returns>A disposable used to cancel the subscription.</returns>
public static IDisposable Set(this IPseudoClasses classes, string name, IObservable<bool> trigger)
{
return trigger.Subscribe(x => classes.Set(name, x));
}
}
}

25
src/Perspex.Controls/IPseudoClasses.cs

@ -0,0 +1,25 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Perspex.Controls
{
/// <summary>
/// Exposes an interface for setting pseudoclasses on a <see cref="Classes"/> collection.
/// </summary>
public interface IPseudoClasses
{
/// <summary>
/// Adds a pseudoclass to the collection.
/// </summary>
/// <param name="name">The pseudoclass name.</param>
void Add(string name);
/// <summary>
/// Removes a pseudoclass from the collection.
/// </summary>
/// <param name="name">The pseudoclass name.</param>
bool Remove(string name);
}
}

21
src/Perspex.Controls/ItemsControl.cs

@ -64,7 +64,7 @@ namespace Perspex.Controls
/// </summary>
public ItemsControl()
{
Classes.Add(":empty");
PseudoClasses.Add(":empty");
SubscribeToItems(_items);
}
@ -302,15 +302,7 @@ namespace Perspex.Controls
}
var collection = sender as ICollection;
if (collection.Count == 0)
{
Classes.Add(":empty");
}
else
{
Classes.Remove(":empty");
}
PseudoClasses.Set(":empty", collection.Count == 0);
}
/// <summary>
@ -367,14 +359,7 @@ namespace Perspex.Controls
/// <param name="items"></param>
private void SubscribeToItems(IEnumerable items)
{
if (items == null || items.Count() == 0)
{
Classes.Add(":empty");
}
else
{
Classes.Remove(":empty");
}
PseudoClasses.Set(":empty", items == null || items.Count() == 0);
var incc = items as INotifyCollectionChanged;

4
src/Perspex.Controls/Mixins/SelectableMixin.cs

@ -52,7 +52,7 @@ namespace Perspex.Controls.Mixins
{
if ((bool)x.NewValue)
{
sender.Classes.Add(":selected");
((IPseudoClasses)sender.Classes).Add(":selected");
if (((IVisual)sender).IsAttachedToVisualTree)
{
@ -61,7 +61,7 @@ namespace Perspex.Controls.Mixins
}
else
{
sender.Classes.Remove(":selected");
((IPseudoClasses)sender.Classes).Remove(":selected");
}
sender.RaiseEvent(new RoutedEventArgs

1
src/Perspex.Controls/Perspex.Controls.csproj

@ -47,6 +47,7 @@
<Compile Include="Generators\ItemContainer.cs" />
<Compile Include="HotkeyManager.cs" />
<Compile Include="INameScope.cs" />
<Compile Include="IPseudoClasses.cs" />
<Compile Include="Mixins\ContentControlMixin.cs" />
<Compile Include="NameScope.cs" />
<Compile Include="NameScopeEventArgs.cs" />

9
src/Perspex.Controls/Primitives/SelectingItemsControl.cs

@ -546,14 +546,7 @@ namespace Perspex.Controls.Primitives
}
else
{
if (selected)
{
container.Classes.Add(":selected");
}
else
{
container.Classes.Remove(":selected");
}
((IPseudoClasses)container.Classes).Set(":selected", selected);
}
}
finally

9
src/Perspex.Controls/TreeView.cs

@ -186,14 +186,7 @@ namespace Perspex.Controls
}
else
{
if (selected)
{
container.Classes.Add(":selected");
}
else
{
container.Classes.Remove(":selected");
}
((IPseudoClasses)container.Classes).Set(":selected", selected);
}
}
}

153
tests/Perspex.Controls.UnitTests/ClassesTests.cs

@ -0,0 +1,153 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Xunit;
namespace Perspex.Controls.UnitTests
{
public class ClassesTests
{
[Fact]
public void Duplicates_Should_Not_Be_Added()
{
var target = new Classes();
target.Add("foo");
target.Add("foo");
Assert.Equal(new[] { "foo" }, target);
}
[Fact]
public void Duplicates_Should_Not_Be_Added_Via_AddRange()
{
var target = new Classes();
target.Add("foo");
target.AddRange(new[] { "foo", "bar" });
Assert.Equal(new[] { "foo", "bar" }, target);
}
[Fact]
public void Duplicates_Should_Not_Be_Added_Via_Pseudoclasses()
{
var target = new Classes();
var ps = (IPseudoClasses)target;
ps.Add(":foo");
ps.Add(":foo");
Assert.Equal(new[] { ":foo" }, target);
}
[Fact]
public void Duplicates_Should_Not_Be_Inserted()
{
var target = new Classes();
target.Add("foo");
target.Insert(0, "foo");
Assert.Equal(new[] { "foo" }, target);
}
[Fact]
public void Duplicates_Should_Not_Be_Inserted_Via_InsertRange()
{
var target = new Classes();
target.Add("foo");
target.InsertRange(1, new[] { "foo", "bar" });
Assert.Equal(new[] { "foo", "bar" }, target);
}
[Fact]
public void Should_Not_Be_Able_To_Add_Pseudoclass()
{
var target = new Classes();
Assert.Throws<ArgumentException>(() => target.Add(":foo"));
}
[Fact]
public void Should_Not_Be_Able_To_Add_Pseudoclasses_Via_AddRange()
{
var target = new Classes();
Assert.Throws<ArgumentException>(() => target.AddRange(new[] { "foo", ":bar" }));
}
[Fact]
public void Should_Not_Be_Able_To_Insert_Pseudoclass()
{
var target = new Classes();
Assert.Throws<ArgumentException>(() => target.Insert(0, ":foo"));
}
[Fact]
public void Should_Not_Be_Able_To_Insert_Pseudoclasses_Via_InsertRange()
{
var target = new Classes();
Assert.Throws<ArgumentException>(() => target.InsertRange(0, new[] { "foo", ":bar" }));
}
[Fact]
public void Should_Not_Be_Able_To_Remove_Pseudoclass()
{
var target = new Classes();
Assert.Throws<ArgumentException>(() => target.Remove(":foo"));
}
[Fact]
public void Should_Not_Be_Able_To_Remove_Pseudoclasses_Via_RemoveAll()
{
var target = new Classes();
Assert.Throws<ArgumentException>(() => target.RemoveAll(new[] { "foo", ":bar" }));
}
[Fact]
public void Should_Not_Be_Able_To_Remove_Pseudoclasses_Via_RemoveRange()
{
var target = new Classes();
Assert.Throws<ArgumentException>(() => target.RemoveRange(0, 1));
}
[Fact]
public void Should_Not_Be_Able_To_Remove_Pseudoclass_Via_RemoveAt()
{
var target = new Classes();
((IPseudoClasses)target).Add(":foo");
Assert.Throws<ArgumentException>(() => target.RemoveAt(0));
}
[Fact]
public void Replace_Should_Not_Replace_Pseudoclasses()
{
var target = new Classes("foo", "bar");
((IPseudoClasses)target).Add(":baz");
target.Replace(new[] { "qux" });
Assert.Equal(new[] { ":baz", "qux" }, target);
}
[Fact]
public void Replace_Should_Not_Accept_Pseudoclasses()
{
var target = new Classes();
Assert.Throws<ArgumentException>(() => target.Replace(new[] { ":qux" }));
}
}
}

1
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@ -81,6 +81,7 @@
<Otherwise />
</Choose>
<ItemGroup>
<Compile Include="ClassesTests.cs" />
<Compile Include="EnumerableExtensions.cs" />
<Compile Include="HeaderedItemsControlTests .cs" />
<Compile Include="ControlTests_NameScope.cs" />

Loading…
Cancel
Save