Browse Source

Merge pull request #2345 from AvaloniaUI/feature/1742-multiple-selectors

Allow multiple comma-separated style selectors
pull/2351/head
Jumar Macato 7 years ago
committed by GitHub
parent
commit
7dc7710876
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 131
      src/Avalonia.Styling/Styling/OrSelector.cs
  2. 22
      src/Avalonia.Styling/Styling/Selectors.cs
  3. 8
      src/Avalonia.Themes.Default/Separator.xaml
  4. 30
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  5. 20
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  6. 16
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  7. 7
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs
  8. 106
      tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs

131
src/Avalonia.Styling/Styling/OrSelector.cs

@ -0,0 +1,131 @@
// Copyright (c) The Avalonia 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;
namespace Avalonia.Styling
{
/// <summary>
/// The OR style selector.
/// </summary>
internal class OrSelector : Selector
{
private readonly IReadOnlyList<Selector> _selectors;
private string _selectorString;
private Type _targetType;
/// <summary>
/// Initializes a new instance of the <see cref="OrSelector"/> class.
/// </summary>
/// <param name="selectors">The selectors to OR.</param>
public OrSelector(IReadOnlyList<Selector> selectors)
{
Contract.Requires<ArgumentNullException>(selectors != null);
Contract.Requires<ArgumentException>(selectors.Count > 1);
_selectors = selectors;
}
/// <inheritdoc/>
public override bool InTemplate => false;
/// <inheritdoc/>
public override bool IsCombinator => false;
/// <inheritdoc/>
public override Type TargetType
{
get
{
if (_targetType == null)
{
_targetType = EvaluateTargetType();
}
return _targetType;
}
}
/// <inheritdoc/>
public override string ToString()
{
if (_selectorString == null)
{
_selectorString = string.Join(", ", _selectors);
}
return _selectorString;
}
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
{
var activators = new List<IObservable<bool>>();
var neverThisInstance = false;
foreach (var selector in _selectors)
{
var match = selector.Match(control, subscribe);
switch (match.Result)
{
case SelectorMatchResult.AlwaysThisType:
case SelectorMatchResult.AlwaysThisInstance:
return match;
case SelectorMatchResult.NeverThisInstance:
neverThisInstance = true;
break;
case SelectorMatchResult.Sometimes:
activators.Add(match.Activator);
break;
}
}
if (activators.Count > 1)
{
return new SelectorMatch(StyleActivator.Or(activators));
}
else if (activators.Count == 1)
{
return new SelectorMatch(activators[0]);
}
else if (neverThisInstance)
{
return SelectorMatch.NeverThisInstance;
}
else
{
return SelectorMatch.NeverThisType;
}
}
protected override Selector MovePrevious() => null;
private Type EvaluateTargetType()
{
var result = default(Type);
foreach (var selector in _selectors)
{
if (selector.TargetType == null)
{
return null;
}
else if (result == null)
{
result = selector.TargetType;
}
else
{
while (!result.IsAssignableFrom(selector.TargetType))
{
result = result.BaseType;
}
}
}
return result;
}
}
}

22
src/Avalonia.Styling/Styling/Selectors.cs

@ -2,6 +2,8 @@
// 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;
namespace Avalonia.Styling
{
@ -137,6 +139,26 @@ namespace Avalonia.Styling
return previous.OfType(typeof(T));
}
/// <summary>
/// Returns a selector which ORs selectors.
/// </summary>
/// <param name="selectors">The selectors to be OR'd.</param>
/// <returns>The selector.</returns>
public static Selector Or(params Selector[] selectors)
{
return new OrSelector(selectors);
}
/// <summary>
/// Returns a selector which ORs selectors.
/// </summary>
/// <param name="selectors">The selectors to be OR'd.</param>
/// <returns>The selector.</returns>
public static Selector Or(IReadOnlyList<Selector> selectors)
{
return new OrSelector(selectors);
}
/// <summary>
/// Returns a selector which matches a control with the specified property value.
/// </summary>

8
src/Avalonia.Themes.Default/Separator.xaml

@ -11,13 +11,7 @@
</Setter>
</Style>
<Style Selector="MenuItem > Separator">
<Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
<Setter Property="Margin" Value="29,1,0,1"/>
<Setter Property="Height" Value="1"/>
</Style>
<Style Selector="ContextMenu > Separator">
<Style Selector="MenuItem > Separator, ContextMenu > Separator">
<Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
<Setter Property="Margin" Value="29,1,0,1"/>
<Setter Property="Height" Value="1"/>

30
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs

@ -49,7 +49,7 @@ namespace Avalonia.Markup.Parsers
state = ParseStart(ref r);
break;
case State.Middle:
state = ParseMiddle(ref r, end);
(state, syntax) = ParseMiddle(ref r, end);
break;
case State.CanHaveType:
state = ParseCanHaveType(ref r);
@ -113,33 +113,37 @@ namespace Avalonia.Markup.Parsers
return State.TypeName;
}
private static State ParseMiddle(ref CharacterReader r, char? end)
private static (State, ISyntax) ParseMiddle(ref CharacterReader r, char? end)
{
if (r.TakeIf(':'))
{
return State.Colon;
return (State.Colon, null);
}
else if (r.TakeIf('.'))
{
return State.Class;
return (State.Class, null);
}
else if (r.TakeIf(char.IsWhiteSpace) || r.Peek == '>')
{
return State.Traversal;
return (State.Traversal, null);
}
else if (r.TakeIf('/'))
{
return State.Template;
return (State.Template, null);
}
else if (r.TakeIf('#'))
{
return State.Name;
return (State.Name, null);
}
else if (r.TakeIf(','))
{
return (State.Start, new CommaSyntax());
}
else if (end.HasValue && !r.End && r.Peek == end.Value)
{
return State.End;
return (State.End, null);
}
return State.TypeName;
return (State.TypeName, null);
}
private static State ParseCanHaveType(ref CharacterReader r)
@ -415,5 +419,13 @@ namespace Avalonia.Markup.Parsers
return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument);
}
}
public class CommaSyntax : ISyntax
{
public override bool Equals(object obj)
{
return obj is CommaSyntax or;
}
}
}
}

20
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs

@ -43,6 +43,7 @@ namespace Avalonia.Markup.Parsers
private Selector Create(IEnumerable<SelectorGrammar.ISyntax> syntax)
{
var result = default(Selector);
var results = default(List<Selector>);
foreach (var i in syntax)
{
@ -106,11 +107,30 @@ namespace Avalonia.Markup.Parsers
case SelectorGrammar.NotSyntax not:
result = result.Not(x => Create(not.Argument));
break;
case SelectorGrammar.CommaSyntax comma:
if (results == null)
{
results = new List<Selector>();
}
results.Add(result);
result = null;
break;
default:
throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'.");
}
}
if (results != null)
{
if (result != null)
{
results.Add(result);
}
result = results.Count > 1 ? Selectors.Or(results) : results[0];
}
return result;
}

16
tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

@ -261,6 +261,22 @@ namespace Avalonia.Markup.UnitTests.Parsers
result);
}
[Fact]
public void OfType_Comma_Is_Class()
{
var result = SelectorGrammar.Parse("TextBlock, :is(Button).foo");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "TextBlock" },
new SelectorGrammar.CommaSyntax(),
new SelectorGrammar.IsSyntax { TypeName = "Button" },
new SelectorGrammar.ClassSyntax { Class = "foo" },
},
result);
}
[Fact]
public void Namespace_Alone_Fails()
{

7
tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs

@ -14,6 +14,13 @@ namespace Avalonia.Markup.UnitTests.Parsers
var result = target.Parse("TextBlock[IsPointerOver=True]");
}
[Fact]
public void Parses_Comma_Separated_Selectors()
{
var target = new SelectorParser((ns, type) => typeof(TextBlock));
var result = target.Parse("TextBlock, TextBlock:foo");
}
[Fact]
public void Throws_If_OfType_Type_Not_Found()
{

106
tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs

@ -0,0 +1,106 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class SelectorTests_Or
{
[Fact]
public void Or_Selector_Should_Have_Correct_String_Representation()
{
var target = Selectors.Or(
default(Selector).OfType<Control1>().Class("foo"),
default(Selector).OfType<Control2>().Class("bar"));
Assert.Equal("Control1.foo, Control2.bar", target.ToString());
}
[Fact]
public void Or_Selector_Matches_Control_Of_Correct_Type()
{
var target = Selectors.Or(
default(Selector).OfType<Control1>(),
default(Selector).OfType<Control2>().Class("bar"));
var control = new Control1();
Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result);
}
[Fact]
public void Or_Selector_Matches_Control_Of_Correct_Type_With_Class()
{
var target = Selectors.Or(
default(Selector).OfType<Control1>(),
default(Selector).OfType<Control2>().Class("bar"));
var control = new Control2();
Assert.Equal(SelectorMatchResult.Sometimes, target.Match(control).Result);
}
[Fact]
public void Or_Selector_Doesnt_Match_Control_Of_Incorrect_Type()
{
var target = Selectors.Or(
default(Selector).OfType<Control1>(),
default(Selector).OfType<Control2>().Class("bar"));
var control = new Control3();
Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(control).Result);
}
[Fact]
public void Or_Selector_Doesnt_Match_Control_With_Incorrect_Name()
{
var target = Selectors.Or(
default(Selector).OfType<Control1>().Name("foo"),
default(Selector).OfType<Control2>().Name("foo"));
var control = new Control1 { Name = "bar" };
Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(control).Result);
}
[Fact]
public void Returns_Correct_TargetType_When_Types_Same()
{
var target = Selectors.Or(
default(Selector).OfType<Control1>().Class("foo"),
default(Selector).OfType<Control1>().Class("bar"));
Assert.Equal(typeof(Control1), target.TargetType);
}
[Fact]
public void Returns_Common_TargetType()
{
var target = Selectors.Or(
default(Selector).OfType<Control1>().Class("foo"),
default(Selector).OfType<Control2>().Class("bar"));
Assert.Equal(typeof(TestControlBase), target.TargetType);
}
[Fact]
public void Returns_Null_TargetType_When_A_Selector_Has_No_TargetType()
{
var target = Selectors.Or(
default(Selector).OfType<Control1>().Class("foo"),
default(Selector).Class("bar"));
Assert.Equal(null, target.TargetType);
}
public class Control1 : TestControlBase
{
}
public class Control2 : TestControlBase
{
}
public class Control3 : TestControlBase
{
}
}
}
Loading…
Cancel
Save