Browse Source

Merge branch 'master' into fixes/2203-stream-binding-value-types

pull/2257/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
bd7212303b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      Avalonia.sln
  2. 2
      src/Avalonia.Base/AvaloniaProperty.cs
  3. 16
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  4. 145
      src/Avalonia.Base/ValueStore.cs
  5. 11
      src/Avalonia.Controls/TopLevel.cs
  6. 2
      src/Avalonia.Controls/Window.cs
  7. 3
      src/Avalonia.Controls/WindowBase.cs
  8. 6
      src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs
  9. 72
      src/Avalonia.Styling/Styling/NotSelector.cs
  10. 11
      src/Avalonia.Styling/Styling/Selectors.cs
  11. 51
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  12. 13
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  13. 16
      tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
  14. 21
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  15. 73
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  16. 28
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  17. 35
      tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
  18. 114
      tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs

31
Avalonia.sln

@ -196,9 +196,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "nukebuild\_build.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Animation.UnitTests", "tests\Avalonia.Animation.UnitTests\Avalonia.Animation.UnitTests.csproj", "{AF227847-E65C-4BE9-BCE9-B551357788E0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
@ -1819,6 +1821,30 @@ Global
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhone.Build.0 = Release|Any CPU
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhone.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhone.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.Build.0 = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhone.ActiveCfg = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhone.Build.0 = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1875,6 +1901,7 @@ Global
{AF227847-E65C-4BE9-BCE9-B551357788E0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

2
src/Avalonia.Base/AvaloniaProperty.cs

@ -21,7 +21,7 @@ namespace Avalonia
/// </summary>
public static readonly object UnsetValue = new Unset();
private static int s_nextId = 1;
private static int s_nextId;
private readonly Subject<AvaloniaPropertyChangedEventArgs> _initialized;
private readonly Subject<AvaloniaPropertyChangedEventArgs> _changed;
private readonly PropertyMetadata _defaultMetadata;

16
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -13,6 +13,8 @@ namespace Avalonia
/// </summary>
public class AvaloniaPropertyRegistry
{
private readonly IList<AvaloniaProperty> _properties =
new List<AvaloniaProperty>();
private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _registered =
new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _attached =
@ -148,6 +150,16 @@ namespace Avalonia
return FindRegistered(o.GetType(), name);
}
/// <summary>
/// Finds a registered property by Id.
/// </summary>
/// <param name="id">The property Id.</param>
/// <returns>The registered property or null if no matching property found.</returns>
internal AvaloniaProperty FindRegistered(int id)
{
return id < _properties.Count ? _properties[id] : null;
}
/// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is registered on a type.
/// </summary>
@ -202,7 +214,8 @@ namespace Avalonia
{
inner.Add(property.Id, property);
}
_properties.Add(property);
_registeredCache.Clear();
}
@ -238,6 +251,7 @@ namespace Avalonia
inner.Add(property.Id, property);
}
_properties.Add(property);
_attachedCache.Clear();
}
}

145
src/Avalonia.Base/ValueStore.cs

@ -7,13 +7,21 @@ namespace Avalonia
{
internal class ValueStore : IPriorityValueOwner
{
private struct Entry
{
internal int PropertyId;
internal object Value;
}
private readonly AvaloniaObject _owner;
private readonly Dictionary<AvaloniaProperty, object> _values =
new Dictionary<AvaloniaProperty, object>();
private Entry[] _entries;
public ValueStore(AvaloniaObject owner)
{
_owner = owner;
// The last item in the list is always int.MaxValue
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } };
}
public IDisposable AddBinding(
@ -23,7 +31,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
if (_values.TryGetValue(property, out var v))
if (TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@ -31,13 +39,13 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
_values[property] = priorityValue;
SetValueInternal(property, priorityValue);
}
}
else
{
priorityValue = CreatePriorityValue(property);
_values.Add(property, priorityValue);
AddValueInternal(property, priorityValue);
}
return priorityValue.Add(source, (int)priority);
@ -47,7 +55,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
if (_values.TryGetValue(property, out var v))
if (TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@ -55,7 +63,7 @@ namespace Avalonia
{
if (priority == (int)BindingPriority.LocalValue)
{
_values[property] = Validate(property, value);
SetValueInternal(property, Validate(property, value));
Changed(property, priority, v, value);
return;
}
@ -63,7 +71,7 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
_values[property] = priorityValue;
SetValueInternal(property, priorityValue);
}
}
}
@ -76,14 +84,14 @@ namespace Avalonia
if (priority == (int)BindingPriority.LocalValue)
{
_values.Add(property, Validate(property, value));
AddValueInternal(property, Validate(property, value));
Changed(property, priority, AvaloniaProperty.UnsetValue, value);
return;
}
else
{
priorityValue = CreatePriorityValue(property);
_values.Add(property, priorityValue);
AddValueInternal(property, priorityValue);
}
}
@ -100,13 +108,22 @@ namespace Avalonia
_owner.PriorityValueChanged(property, priority, oldValue, newValue);
}
public IDictionary<AvaloniaProperty, object> GetSetValues() => _values;
public IDictionary<AvaloniaProperty, object> GetSetValues()
{
var dict = new Dictionary<AvaloniaProperty, object>(_entries.Length - 1);
for (int i = 0; i < _entries.Length - 1; ++i)
{
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
}
return dict;
}
public object GetValue(AvaloniaProperty property)
{
var result = AvaloniaProperty.UnsetValue;
if (_values.TryGetValue(property, out var value))
if (TryGetValue(property, out var value))
{
result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
}
@ -116,12 +133,12 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property)
{
return _values.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
}
public bool IsSet(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var value))
if (TryGetValue(property, out var value))
{
return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
}
@ -131,7 +148,7 @@ namespace Avalonia
public void Revalidate(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var value))
if (TryGetValue(property, out var value))
{
(value as PriorityValue)?.Revalidate();
}
@ -178,5 +195,103 @@ namespace Avalonia
(_deferredSetter = new DeferredSetter<object>());
}
}
private bool TryGetValue(AvaloniaProperty property, out object value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
{
value = null;
return false;
}
value = _entries[index].Value;
return true;
}
private void AddValueInternal(AvaloniaProperty property, object value)
{
Entry[] entries = new Entry[_entries.Length + 1];
for (int i = 0; i < _entries.Length; ++i)
{
if (_entries[i].PropertyId > property.Id)
{
if (i > 0)
{
Array.Copy(_entries, 0, entries, 0, i);
}
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
}
}
_entries = entries;
}
private void SetValueInternal(AvaloniaProperty property, object value)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
}
private (int, bool) TryFindEntry(int propertyId)
{
if (_entries.Length <= 12)
{
// For small lists, we use an optimized linear search. Since the last item in the list
// is always int.MaxValue, we can skip a conditional branch in each iteration.
// By unrolling the loop, we can skip another unconditional branch in each iteration.
if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId);
if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId);
if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId);
if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId);
if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId);
if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId);
if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId);
if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId);
if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId);
if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId);
if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId);
}
else
{
int low = 0;
int high = _entries.Length;
int id;
while (high - low > 3)
{
int pivot = (high + low) / 2;
id = _entries[pivot].PropertyId;
if (propertyId == id)
return (pivot, true);
if (propertyId <= id)
high = pivot;
else
low = pivot + 1;
}
do
{
id = _entries[low].PropertyId;
if (id == propertyId)
return (low, true);
if (id > propertyId)
break;
++low;
}
while (low < high);
}
return (0, false);
}
}
}

11
src/Avalonia.Controls/TopLevel.cs

@ -136,6 +136,11 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Fired when the window is opened.
/// </summary>
public event EventHandler Opened;
/// <summary>
/// Fired when the window is closed.
/// </summary>
@ -311,6 +316,12 @@ namespace Avalonia.Controls
$"Control '{GetType().Name}' is a top level control and cannot be added as a child.");
}
/// <summary>
/// Raises the <see cref="Opened"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnOpened(EventArgs e) => Opened?.Invoke(this, e);
/// <summary>
/// Tries to get a service from an <see cref="IAvaloniaDependencyResolver"/>, logging a
/// warning if not found.

2
src/Avalonia.Controls/Window.cs

@ -389,6 +389,7 @@ namespace Avalonia.Controls
Renderer?.Start();
}
SetWindowStartupLocation(Owner?.PlatformImpl);
OnOpened(EventArgs.Empty);
}
/// <summary>
@ -458,6 +459,7 @@ namespace Avalonia.Controls
owner.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult)));
});
OnOpened(EventArgs.Empty);
}
SetWindowStartupLocation(owner);

3
src/Avalonia.Controls/WindowBase.cs

@ -163,7 +163,7 @@ namespace Avalonia.Controls
}
/// <summary>
/// Shows the popup.
/// Shows the window.
/// </summary>
public virtual void Show()
{
@ -181,6 +181,7 @@ namespace Avalonia.Controls
}
PlatformImpl?.Show();
Renderer?.Start();
OnOpened(EventArgs.Empty);
}
finally
{

6
src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs

@ -29,8 +29,8 @@ namespace Avalonia
{
var windowLoaded = Observable
.FromEventPattern(
x => window.Initialized += x,
x => window.Initialized -= x)
x => window.Opened += x,
x => window.Opened -= x)
.Select(args => true);
var windowUnloaded = Observable
.FromEventPattern(
@ -59,4 +59,4 @@ namespace Avalonia
.DistinctUntilChanged();
}
}
}
}

72
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
{
/// <summary>
/// The `:not()` style selector.
/// </summary>
internal class NotSelector : Selector
{
private readonly Selector _previous;
private readonly Selector _argument;
private string _selectorString;
/// <summary>
/// Initializes a new instance of the <see cref="NotSelector"/> class.
/// </summary>
/// <param name="previous">The previous selector.</param>
/// <param name="argument">The selector to be not-ed.</param>
public NotSelector(Selector previous, Selector argument)
{
_previous = previous;
_argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument.");
}
/// <inheritdoc/>
public override bool InTemplate => _argument.InTemplate;
/// <inheritdoc/>
public override bool IsCombinator => false;
/// <inheritdoc/>
public override Type TargetType => _previous?.TargetType;
/// <inheritdoc/>
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;
}
}

11
src/Avalonia.Styling/Styling/Selectors.cs

@ -94,6 +94,17 @@ namespace Avalonia.Styling
}
}
/// <summary>
/// Returns a selector which inverts the results of selector argument.
/// </summary>
/// <param name="previous">The previous selector.</param>
/// <param name="argument">The selector to be not-ed.</param>
/// <returns>The selector.</returns>
public static Selector Not(this Selector previous, Func<Selector, Selector> argument)
{
return new NotSelector(previous, argument(null));
}
/// <summary>
/// Returns a selector which matches a type.
/// </summary>

51
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<ISyntax> Parse(string s)
{
var r = new CharacterReader(s.AsSpan());
return Parse(ref r, null);
}
private static IEnumerable<ISyntax> Parse(ref CharacterReader r, char? end)
{
var state = State.Start;
var selector = new List<ISyntax>();
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<ISyntax> Argument { get; set; }
public override bool Equals(object obj)
{
return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument);
}
}
}
}

13
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
/// </param>
public SelectorParser(Func<string, string, Type> typeResolver)
{
this._typeResolver = typeResolver;
_typeResolver = typeResolver;
}
/// <summary>
@ -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<SelectorGrammar.ISyntax> 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()}'.");
}
}

16
tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs

@ -199,6 +199,22 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Showing_Should_Raise_Opened()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = new TestWindowBase();
var raised = false;
target.Opened += (s, e) => raised = true;
target.Show();
Assert.True(raised);
}
}
[Fact]
public void Hiding_Should_Stop_Renderer()
{

21
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -228,18 +228,35 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void ShowDialog_Should_Start_Renderer()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var parent = Mock.Of<IWindowImpl>();
var renderer = new Mock<IRenderer>();
var target = new Window(CreateImpl(renderer));
target.Show();
target.ShowDialog<object>(parent);
renderer.Verify(x => x.Start(), Times.Once);
}
}
[Fact]
public void ShowDialog_Should_Raise_Opened()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var parent = Mock.Of<IWindowImpl>();
var target = new Window();
var raised = false;
target.Opened += (s, e) => raised = true;
target.ShowDialog<object>(parent);
Assert.True(raised);
}
}
[Fact]
public void Hiding_Should_Stop_Renderer()
{

73
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<ExpressionParseException>(() => SelectorGrammar.Parse(".%foo"));
}
[Fact]
public void Not_Without_Argument_Fails()
{
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(":not()"));
}
[Fact]
public void Not_Without_Closing_Parenthesis_Fails()
{
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(":not(Button"));
}
}
}

28
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 = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border:not(.foo)'>
<Setter Property='Background' Value='Red'/>
</Style>
</Window.Styles>
<StackPanel>
<Border Name='foo' Classes='foo bar'/>
<Border Name='notFoo' Classes='bar'/>
</StackPanel>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var foo = window.FindControl<Border>("foo");
var notFoo = window.FindControl<Border>("notFoo");
Assert.Null(foo.Background);
Assert.Equal(Colors.Red, ((ISolidColorBrush)notFoo.Background).Color);
}
}
}
}

35
tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs

@ -10,6 +10,7 @@ using ReactiveUI;
using DynamicData;
using Xunit;
using Splat;
using Avalonia.Markup.Xaml;
namespace Avalonia
{
@ -70,12 +71,40 @@ namespace Avalonia
public class ActivatableWindow : ReactiveWindow<ActivatableViewModel>
{
public ActivatableWindow() => this.WhenActivated(disposables => { });
public ActivatableWindow()
{
InitializeComponent();
Assert.IsType<Border>(Content);
this.WhenActivated(disposables => { });
}
private void InitializeComponent()
{
var loader = new AvaloniaXamlLoader();
loader.Load(@"
<Window xmlns='https://github.com/avaloniaui'>
<Border/>
</Window>", null, this);
}
}
public class ActivatableUserControl : ReactiveUserControl<ActivatableViewModel>
{
public ActivatableUserControl() => this.WhenActivated(disposables => { });
public ActivatableUserControl()
{
InitializeComponent();
Assert.IsType<Border>(Content);
this.WhenActivated(disposables => { });
}
private void InitializeComponent()
{
var loader = new AvaloniaXamlLoader();
loader.Load(@"
<UserControl xmlns='https://github.com/avaloniaui'>
<Border/>
</UserControl>", null, this);
}
}
public AvaloniaActivationForViewFetcherTest()
@ -183,4 +212,4 @@ namespace Avalonia
Assert.False(viewModel.IsActivated);
}
}
}
}

114
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<Control1>());
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<Control1>());
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<Control1>().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<Control1>().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<Control1>().Not(x => x.Class("foo"));
Assert.Equal(typeof(Control1), target.TargetType);
}
public class Control1 : TestControlBase
{
}
public class Control2 : TestControlBase
{
}
}
}
Loading…
Cancel
Save