diff --git a/src/Avalonia.Styling/Styling/NotSelector.cs b/src/Avalonia.Styling/Styling/NotSelector.cs
new file mode 100644
index 0000000000..bcf76620be
--- /dev/null
+++ b/src/Avalonia.Styling/Styling/NotSelector.cs
@@ -0,0 +1,72 @@
+// 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.Reactive.Linq;
+
+namespace Avalonia.Styling
+{
+ ///
+ /// The `:not()` style selector.
+ ///
+ internal class NotSelector : Selector
+ {
+ private readonly Selector _previous;
+ private readonly Selector _argument;
+ private string _selectorString;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The previous selector.
+ /// The selector to be not-ed.
+ public NotSelector(Selector previous, Selector argument)
+ {
+ _previous = previous;
+ _argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument.");
+ }
+
+ ///
+ public override bool InTemplate => _argument.InTemplate;
+
+ ///
+ public override bool IsCombinator => false;
+
+ ///
+ public override Type TargetType => _previous?.TargetType;
+
+ ///
+ public override string ToString()
+ {
+ if (_selectorString == null)
+ {
+ _selectorString = ":not(" + _argument.ToString() + ")";
+ }
+
+ return _selectorString;
+ }
+
+ protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+ {
+ var innerResult = _argument.Match(control, subscribe);
+
+ switch (innerResult.Result)
+ {
+ case SelectorMatchResult.AlwaysThisInstance:
+ return SelectorMatch.NeverThisInstance;
+ case SelectorMatchResult.AlwaysThisType:
+ return SelectorMatch.NeverThisType;
+ case SelectorMatchResult.NeverThisInstance:
+ return SelectorMatch.AlwaysThisInstance;
+ case SelectorMatchResult.NeverThisType:
+ return SelectorMatch.AlwaysThisType;
+ case SelectorMatchResult.Sometimes:
+ return new SelectorMatch(innerResult.Activator.Select(x => !x));
+ default:
+ throw new InvalidOperationException("Invalid SelectorMatchResult.");
+ }
+ }
+
+ protected override Selector MovePrevious() => _previous;
+ }
+}
diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs
index c91cc7af04..4284c7e798 100644
--- a/src/Avalonia.Styling/Styling/Selectors.cs
+++ b/src/Avalonia.Styling/Styling/Selectors.cs
@@ -94,6 +94,17 @@ namespace Avalonia.Styling
}
}
+ ///
+ /// Returns a selector which inverts the results of selector argument.
+ ///
+ /// The previous selector.
+ /// The selector to be not-ed.
+ /// The selector.
+ public static Selector Not(this Selector previous, Func argument)
+ {
+ return new NotSelector(previous, argument(null));
+ }
+
///
/// Returns a selector which matches a type.
///
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
index f66d3e51fc..55c3aab81f 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using Avalonia.Data.Core;
using Avalonia.Utilities;
@@ -32,6 +33,11 @@ namespace Avalonia.Markup.Parsers
public static IEnumerable Parse(string s)
{
var r = new CharacterReader(s.AsSpan());
+ return Parse(ref r, null);
+ }
+
+ private static IEnumerable Parse(ref CharacterReader r, char? end)
+ {
var state = State.Start;
var selector = new List();
while (!r.End && state != State.End)
@@ -43,7 +49,7 @@ namespace Avalonia.Markup.Parsers
state = ParseStart(ref r);
break;
case State.Middle:
- state = ParseMiddle(ref r);
+ state = ParseMiddle(ref r, end);
break;
case State.CanHaveType:
state = ParseCanHaveType(ref r);
@@ -107,7 +113,7 @@ namespace Avalonia.Markup.Parsers
return State.TypeName;
}
- private static State ParseMiddle(ref CharacterReader r)
+ private static State ParseMiddle(ref CharacterReader r, char? end)
{
if (r.TakeIf(':'))
{
@@ -129,6 +135,10 @@ namespace Avalonia.Markup.Parsers
{
return State.Name;
}
+ else if (end.HasValue && !r.End && r.Peek == end.Value)
+ {
+ return State.End;
+ }
return State.TypeName;
}
@@ -151,16 +161,23 @@ namespace Avalonia.Markup.Parsers
}
const string IsKeyword = "is";
+ const string NotKeyword = "not";
+
if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('('))
{
var syntax = ParseType(ref r, new IsSyntax());
- if (r.End || !r.TakeIf(')'))
- {
- throw new ExpressionParseException(r.Position, $"Expected ')', got {r.Peek}");
- }
+ Expect(ref r, ')');
return (State.CanHaveType, syntax);
}
+ if (identifier.SequenceEqual(NotKeyword.AsSpan()) && r.TakeIf('('))
+ {
+ var argument = Parse(ref r, ')');
+ Expect(ref r, ')');
+
+ var syntax = new NotSyntax { Argument = argument };
+ return (State.Middle, syntax);
+ }
else
{
return (
@@ -282,6 +299,18 @@ namespace Avalonia.Markup.Parsers
return syntax;
}
+ private static void Expect(ref CharacterReader r, char c)
+ {
+ if (r.End)
+ {
+ throw new ExpressionParseException(r.Position, $"Expected '{c}', got end of selector.");
+ }
+ else if (!r.TakeIf(')'))
+ {
+ throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.Peek}'.");
+ }
+ }
+
public interface ISyntax
{
}
@@ -376,5 +405,15 @@ namespace Avalonia.Markup.Parsers
return obj is TemplateSyntax;
}
}
+
+ public class NotSyntax : ISyntax
+ {
+ public IEnumerable Argument { get; set; }
+
+ public override bool Equals(object obj)
+ {
+ return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument);
+ }
+ }
}
}
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
index bf5b396bec..8d1216e1dc 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
@@ -2,6 +2,7 @@
// 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.Globalization;
using Avalonia.Styling;
using Avalonia.Utilities;
@@ -25,7 +26,7 @@ namespace Avalonia.Markup.Parsers
///
public SelectorParser(Func typeResolver)
{
- this._typeResolver = typeResolver;
+ _typeResolver = typeResolver;
}
///
@@ -36,6 +37,11 @@ namespace Avalonia.Markup.Parsers
public Selector Parse(string s)
{
var syntax = SelectorGrammar.Parse(s);
+ return Create(syntax);
+ }
+
+ private Selector Create(IEnumerable syntax)
+ {
var result = default(Selector);
foreach (var i in syntax)
@@ -97,6 +103,11 @@ namespace Avalonia.Markup.Parsers
case SelectorGrammar.TemplateSyntax template:
result = result.Template();
break;
+ case SelectorGrammar.NotSyntax not:
+ result = result.Not(x => Create(not.Argument));
+ break;
+ default:
+ throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'.");
}
}
diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
index 88fe5a2a12..e3ce4b0968 100644
--- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
+++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
@@ -200,6 +200,67 @@ namespace Avalonia.Markup.UnitTests.Parsers
result);
}
+ [Fact]
+ public void Not_OfType()
+ {
+ var result = SelectorGrammar.Parse(":not(Button)");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.NotSyntax
+ {
+ Argument = new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
+ },
+ }
+ },
+ result);
+ }
+
+ [Fact]
+ public void OfType_Not_Class()
+ {
+ var result = SelectorGrammar.Parse("Button:not(.foo)");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
+ new SelectorGrammar.NotSyntax
+ {
+ Argument = new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.ClassSyntax { Class = "foo" },
+ },
+ }
+ },
+ result);
+ }
+
+ [Fact]
+ public void Is_Descendent_Not_OfType_Class()
+ {
+ var result = SelectorGrammar.Parse(":is(Control) :not(Button.foo)");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.IsSyntax { TypeName = "Control" },
+ new SelectorGrammar.DescendantSyntax { },
+ new SelectorGrammar.NotSyntax
+ {
+ Argument = new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
+ new SelectorGrammar.ClassSyntax { Class = "foo" },
+ },
+ }
+ },
+ result);
+ }
+
[Fact]
public void Namespace_Alone_Fails()
{
@@ -223,5 +284,17 @@ namespace Avalonia.Markup.UnitTests.Parsers
{
Assert.Throws(() => SelectorGrammar.Parse(".%foo"));
}
+
+ [Fact]
+ public void Not_Without_Argument_Fails()
+ {
+ Assert.Throws(() => SelectorGrammar.Parse(":not()"));
+ }
+
+ [Fact]
+ public void Not_Without_Closing_Parenthesis_Fails()
+ {
+ Assert.Throws(() => SelectorGrammar.Parse(":not(Button"));
+ }
}
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
index beaf7477d0..a84ce74a88 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
@@ -198,5 +198,33 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
ex.InnerException.Message);
}
}
+
+ [Fact]
+ public void Style_Can_Use_Not_Selector()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+
+";
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var foo = window.FindControl("foo");
+ var notFoo = window.FindControl("notFoo");
+
+ Assert.Null(foo.Background);
+ Assert.Equal(Colors.Red, ((ISolidColorBrush)notFoo.Background).Color);
+ }
+ }
}
}
diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs
new file mode 100644
index 0000000000..2f3e2b8f34
--- /dev/null
+++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs
@@ -0,0 +1,114 @@
+// 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.Reactive.Linq;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Xunit;
+
+namespace Avalonia.Styling.UnitTests
+{
+ public class SelectorTests_Not
+ {
+ [Fact]
+ public void Not_Selector_Should_Have_Correct_String_Representation()
+ {
+ var target = default(Selector).Not(x => x.Class("foo"));
+
+ Assert.Equal(":not(.foo)", target.ToString());
+ }
+
+ [Fact]
+ public void Not_OfType_Matches_Control_Of_Incorrect_Type()
+ {
+ var control = new Control1();
+ var target = default(Selector).Not(x => x.OfType());
+
+ Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(control).Result);
+ }
+
+ [Fact]
+ public void Not_OfType_Doesnt_Match_Control_Of_Correct_Type()
+ {
+ var control = new Control2();
+ var target = default(Selector).Not(x => x.OfType());
+
+ Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result);
+ }
+
+ [Fact]
+ public async Task Not_Class_Doesnt_Match_Control_With_Class()
+ {
+ var control = new Control1
+ {
+ Classes = new Classes { "foo" },
+ };
+
+ var target = default(Selector).Not(x => x.Class("foo"));
+ var match = target.Match(control);
+
+ Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
+ Assert.False(await match.Activator.Take(1));
+ }
+
+ [Fact]
+ public async Task Not_Class_Matches_Control_Without_Class()
+ {
+ var control = new Control1
+ {
+ Classes = new Classes { "bar" },
+ };
+
+ var target = default(Selector).Not(x => x.Class("foo"));
+ var match = target.Match(control);
+
+ Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
+ Assert.True(await match.Activator.Take(1));
+ }
+
+ [Fact]
+ public async Task OfType_Not_Class_Matches_Control_Without_Class()
+ {
+ var control = new Control1
+ {
+ Classes = new Classes { "bar" },
+ };
+
+ var target = default(Selector).OfType().Not(x => x.Class("foo"));
+ var match = target.Match(control);
+
+ Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
+ Assert.True(await match.Activator.Take(1));
+ }
+
+ [Fact]
+ public void OfType_Not_Class_Doesnt_Match_Control_Of_Wrong_Type()
+ {
+ var control = new Control2
+ {
+ Classes = new Classes { "foo" },
+ };
+
+ var target = default(Selector).OfType().Not(x => x.Class("foo"));
+ var match = target.Match(control);
+
+ Assert.Equal(SelectorMatchResult.NeverThisType, match.Result);
+ }
+
+ [Fact]
+ public void Returns_Correct_TargetType()
+ {
+ var target = default(Selector).OfType().Not(x => x.Class("foo"));
+
+ Assert.Equal(typeof(Control1), target.TargetType);
+ }
+
+ public class Control1 : TestControlBase
+ {
+ }
+
+ public class Control2 : TestControlBase
+ {
+ }
+ }
+}