diff --git a/Avalonia.sln b/Avalonia.sln
index 071d0457b8..4999719676 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -559,6 +559,7 @@ Global
{2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml b/samples/ControlCatalog/Pages/ButtonsPage.xaml
index 059b4d9788..8a474a203d 100644
--- a/samples/ControlCatalog/Pages/ButtonsPage.xaml
+++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml
@@ -90,6 +90,7 @@
+
@@ -55,7 +53,14 @@ namespace Avalonia
nameof(TemplatedParent),
o => o.TemplatedParent,
(o ,v) => o.TemplatedParent = v);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ThemeProperty =
+ AvaloniaProperty.Register(nameof(Theme));
+ private static readonly ControlTheme s_invalidTheme = new ControlTheme();
private int _initCount;
private string? _name;
private readonly Classes _classes = new Classes();
@@ -67,6 +72,8 @@ namespace Avalonia
private List? _appliedStyles;
private ITemplatedControl? _templatedParent;
private bool _dataContextUpdating;
+ private bool _hasPromotedTheme;
+ private ControlTheme? _implicitTheme;
///
/// Initializes static members of the class.
@@ -230,6 +237,15 @@ namespace Avalonia
internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value);
}
+ ///
+ /// Gets or sets the theme to be applied to the element.
+ ///
+ public ControlTheme? Theme
+ {
+ get => GetValue(ThemeProperty);
+ set => SetValue(ThemeProperty, value);
+ }
+
///
/// Gets the styled element's logical children.
///
@@ -302,6 +318,7 @@ namespace Avalonia
///
IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent;
+
///
public virtual void BeginInit()
{
@@ -341,10 +358,15 @@ namespace Avalonia
}
finally
{
+ _styled = true;
EndBatchUpdate();
}
- _styled = true;
+ if (_hasPromotedTheme)
+ {
+ _hasPromotedTheme = false;
+ ClearValue(ThemeProperty);
+ }
}
return _styled;
@@ -475,6 +497,31 @@ namespace Avalonia
};
}
+ ControlTheme? IStyleable.GetEffectiveTheme()
+ {
+ var theme = Theme;
+
+ // Explitly set Theme property takes precedence.
+ if (theme is not null)
+ return theme;
+
+ // If the Theme property is not set, try to find a ControlTheme resource with our StyleKey.
+ if (_implicitTheme is null)
+ {
+ var key = ((IStyleable)this).StyleKey;
+
+ if (this.TryFindResource(key, out var value) && value is ControlTheme t)
+ _implicitTheme = t;
+ else
+ _implicitTheme = s_invalidTheme;
+ }
+
+ if (_implicitTheme != s_invalidTheme)
+ return _implicitTheme;
+
+ return null;
+ }
+
void IStyleable.StyleApplied(IStyleInstance instance)
{
instance = instance ?? throw new ArgumentNullException(nameof(instance));
@@ -590,6 +637,30 @@ namespace Avalonia
{
}
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == ThemeProperty)
+ {
+ // Changing the theme detaches all styles, meaning that if the theme property was
+ // set via a style, it will get cleared! To work around this, if the value was
+ // applied at less than local value priority then promote the value to local value
+ // priority until styling is re-applied.
+ if (change.Priority > BindingPriority.LocalValue)
+ {
+ Theme = change.GetNewValue();
+ _hasPromotedTheme = true;
+ }
+ else if (_hasPromotedTheme && change.Priority == BindingPriority.LocalValue)
+ {
+ _hasPromotedTheme = false;
+ }
+
+ InvalidateStyles();
+ }
+ }
+
private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted)
{
if (o is StyledElement element)
@@ -696,6 +767,7 @@ namespace Avalonia
if (_logicalRoot != null)
{
_logicalRoot = null;
+ _implicitTheme = null;
DetachStyles();
OnDetachedFromLogicalTree(e);
DetachedFromLogicalTree?.Invoke(this, e);
@@ -760,7 +832,7 @@ namespace Avalonia
private void DetachStyles()
{
- if (_appliedStyles is object)
+ if (_appliedStyles?.Count > 0)
{
BeginBatchUpdate();
diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs
index 34f3a76b61..9512dc34df 100644
--- a/src/Avalonia.Base/Styling/ChildSelector.cs
+++ b/src/Avalonia.Base/Styling/ChildSelector.cs
@@ -65,6 +65,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
- internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector();
+ protected override Selector? MovePreviousOrParent() => _parent;
}
}
diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs
new file mode 100644
index 0000000000..644e8b32d4
--- /dev/null
+++ b/src/Avalonia.Base/Styling/ControlTheme.cs
@@ -0,0 +1,67 @@
+using System;
+
+namespace Avalonia.Styling
+{
+ ///
+ /// Defines a switchable theme for a control.
+ ///
+ public class ControlTheme : StyleBase
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ControlTheme() { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value for .
+ public ControlTheme(Type targetType) => TargetType = targetType;
+
+ ///
+ /// Gets or sets the type for which this control theme is intended.
+ ///
+ public Type? TargetType { get; set; }
+
+ ///
+ /// Gets or sets a control theme that is the basis of the current theme.
+ ///
+ public ControlTheme? BasedOn { get; set; }
+
+ public override SelectorMatchResult TryAttach(IStyleable target, object? host)
+ {
+ _ = target ?? throw new ArgumentNullException(nameof(target));
+
+ if (TargetType is null)
+ throw new InvalidOperationException("ControlTheme has no TargetType.");
+
+ var result = BasedOn?.TryAttach(target, host) ?? SelectorMatchResult.NeverThisType;
+
+ if (HasSettersOrAnimations && TargetType.IsAssignableFrom(target.StyleKey))
+ {
+ Attach(target, null);
+ result = SelectorMatchResult.AlwaysThisType;
+ }
+
+ var childResult = TryAttachChildren(target, host);
+
+ if (childResult > result)
+ result = childResult;
+
+ return result;
+ }
+
+ public override string ToString()
+ {
+ if (TargetType is not null)
+ return "ControlTheme: " + TargetType.Name;
+ else
+ return "ControlTheme";
+ }
+
+ internal override void SetParent(StyleBase? parent)
+ {
+ throw new InvalidOperationException("ControlThemes cannot be added as a nested style.");
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs
index 4ffaff6861..677a924189 100644
--- a/src/Avalonia.Base/Styling/DescendentSelector.cs
+++ b/src/Avalonia.Base/Styling/DescendentSelector.cs
@@ -70,6 +70,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
- internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector();
+ protected override Selector? MovePreviousOrParent() => _parent;
}
}
diff --git a/src/Avalonia.Base/Styling/IStyle.cs b/src/Avalonia.Base/Styling/IStyle.cs
index e9faf82c07..417739fb28 100644
--- a/src/Avalonia.Base/Styling/IStyle.cs
+++ b/src/Avalonia.Base/Styling/IStyle.cs
@@ -23,6 +23,6 @@ namespace Avalonia.Styling
///
/// A describing how the style matches the control.
///
- SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host);
+ SelectorMatchResult TryAttach(IStyleable target, object? host);
}
}
diff --git a/src/Avalonia.Base/Styling/IStyleable.cs b/src/Avalonia.Base/Styling/IStyleable.cs
index 5bc972e7ab..254da4d85c 100644
--- a/src/Avalonia.Base/Styling/IStyleable.cs
+++ b/src/Avalonia.Base/Styling/IStyleable.cs
@@ -3,8 +3,6 @@ using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Metadata;
-#nullable enable
-
namespace Avalonia.Styling
{
///
@@ -28,6 +26,11 @@ namespace Avalonia.Styling
///
ITemplatedControl? TemplatedParent { get; }
+ ///
+ /// Gets the effective theme for the control as used by the syling system.
+ ///
+ ControlTheme? GetEffectiveTheme();
+
///
/// Notifies the element that a style has been applied.
///
diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs
index 481a937867..77c5b719c6 100644
--- a/src/Avalonia.Base/Styling/NestingSelector.cs
+++ b/src/Avalonia.Base/Styling/NestingSelector.cs
@@ -15,9 +15,17 @@ namespace Avalonia.Styling
protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
- if (parent is Style s && s.Selector is Selector selector)
+ if (parent is Style s && s.Selector is not null)
{
- return selector.Match(control, (parent as Style)?.Parent, subscribe);
+ return s.Selector.Match(control, s.Parent, subscribe);
+ }
+ else if (parent is ControlTheme theme)
+ {
+ if (theme.TargetType is null)
+ throw new InvalidOperationException("ControlTheme has no TargetType.");
+ return theme.TargetType.IsAssignableFrom(control.StyleKey) ?
+ SelectorMatch.AlwaysThisType :
+ SelectorMatch.NeverThisType;
}
throw new InvalidOperationException(
@@ -25,6 +33,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
- internal override bool HasValidNestingSelector() => true;
+ protected override Selector? MovePreviousOrParent() => null;
}
}
diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs
index cdc3254d38..c7727bb6b8 100644
--- a/src/Avalonia.Base/Styling/NotSelector.cs
+++ b/src/Avalonia.Base/Styling/NotSelector.cs
@@ -67,6 +67,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
- internal override bool HasValidNestingSelector() => _argument.HasValidNestingSelector();
+ protected override Selector? MovePreviousOrParent() => _previous;
}
}
diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs
index 047bf434da..f473791664 100644
--- a/src/Avalonia.Base/Styling/NthChildSelector.cs
+++ b/src/Avalonia.Base/Styling/NthChildSelector.cs
@@ -105,7 +105,7 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
- internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
+ protected override Selector? MovePreviousOrParent() => _previous;
public override string ToString()
{
diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs
index 913c27bf0c..af9249864f 100644
--- a/src/Avalonia.Base/Styling/OrSelector.cs
+++ b/src/Avalonia.Base/Styling/OrSelector.cs
@@ -103,18 +103,12 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
+ protected override Selector? MovePreviousOrParent() => null;
- internal override bool HasValidNestingSelector()
+ internal override void ValidateNestingSelector(bool inControlTheme)
{
foreach (var selector in _selectors)
- {
- if (!selector.HasValidNestingSelector())
- {
- return false;
- }
- }
-
- return true;
+ selector.ValidateNestingSelector(inControlTheme);
}
private Type? EvaluateTargetType()
diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
index 7a37daf087..48136ba2de 100644
--- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
+++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
@@ -90,7 +90,7 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
- internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
+ protected override Selector? MovePreviousOrParent() => _previous;
internal static bool Compare(Type propertyType, object? propertyValue, object? value)
{
diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs
index 1e06f3d375..7ce17518dd 100644
--- a/src/Avalonia.Base/Styling/Selector.cs
+++ b/src/Avalonia.Base/Styling/Selector.cs
@@ -86,7 +86,36 @@ namespace Avalonia.Styling
///
protected abstract Selector? MovePrevious();
- internal abstract bool HasValidNestingSelector();
+ ///
+ /// Moves to the previous selector or the parent selector.
+ ///
+ protected abstract Selector? MovePreviousOrParent();
+
+ internal virtual void ValidateNestingSelector(bool inControlTheme)
+ {
+ var s = this;
+ var templateCount = 0;
+
+ do
+ {
+ if (inControlTheme)
+ {
+ if (!s.InTemplate && s.IsCombinator)
+ throw new InvalidOperationException(
+ "ControlTheme style may not directly contain a child or descendent selector.");
+ if (s is TemplateSelector && templateCount++ > 0)
+ throw new InvalidOperationException(
+ "ControlTemplate styles cannot contain multiple template selectors.");
+ }
+
+ var previous = s.MovePreviousOrParent();
+
+ if (previous is null && s is not NestingSelector)
+ throw new InvalidOperationException("Child styles must have a nesting selector.");
+
+ s = previous;
+ } while (s is not null);
+ }
private static SelectorMatch MatchUntilCombinator(
IStyleable control,
diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs
index 000e588bad..c61b08b2a1 100644
--- a/src/Avalonia.Base/Styling/Style.cs
+++ b/src/Avalonia.Base/Styling/Style.cs
@@ -1,22 +1,13 @@
using System;
-using System.Collections.Generic;
-using Avalonia.Animation;
-using Avalonia.Controls;
-using Avalonia.Metadata;
namespace Avalonia.Styling
{
///
/// Defines a style.
///
- public class Style : AvaloniaObject, IStyle, IResourceProvider
+ public class Style : StyleBase
{
- private IResourceHost? _owner;
- private StyleChildren? _children;
- private IResourceDictionary? _resources;
- private List? _setters;
- private List? _animations;
- private StyleCache? _childCache;
+ private Selector? _selector;
///
/// Initializes a new instance of the class.
@@ -35,113 +26,41 @@ namespace Avalonia.Styling
}
///
- /// Gets the children of the style.
- ///
- public IList Children => _children ??= new(this);
-
- ///
- /// Gets the or Application that hosts the style.
+ /// Gets or sets the style's selector.
///
- public IResourceHost? Owner
+ public Selector? Selector
{
- get => _owner;
- private set
- {
- if (_owner != value)
- {
- _owner = value;
- OwnerChanged?.Invoke(this, EventArgs.Empty);
- }
- }
+ get => _selector;
+ set => _selector = ValidateSelector(value);
}
- ///
- /// Gets the parent style if this style is hosted in a collection.
- ///
- public Style? Parent { get; private set; }
-
- ///
- /// Gets or sets a dictionary of style resources.
- ///
- public IResourceDictionary Resources
+ public override SelectorMatchResult TryAttach(IStyleable target, object? host)
{
- get => _resources ?? (Resources = new ResourceDictionary());
- set
- {
- value = value ?? throw new ArgumentNullException(nameof(value));
-
- var hadResources = _resources?.HasResources ?? false;
-
- _resources = value;
-
- if (Owner is object)
- {
- _resources.AddOwner(Owner);
-
- if (hadResources || _resources.HasResources)
- {
- Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
- }
- }
- }
- }
-
- ///
- /// Gets or sets the style's selector.
- ///
- public Selector? Selector { get; set; }
-
- ///
- /// Gets the style's setters.
- ///
- public IList Setters => _setters ??= new List();
-
- ///
- /// Gets the style's animations.
- ///
- public IList Animations => _animations ??= new List();
-
- bool IResourceNode.HasResources => _resources?.Count > 0;
- IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty();
+ _ = target ?? throw new ArgumentNullException(nameof(target));
- public event EventHandler? OwnerChanged;
+ var result = SelectorMatchResult.NeverThisType;
- public void Add(ISetter setter) => Setters.Add(setter);
- public void Add(IStyle style) => Children.Add(style);
-
- public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
- {
- target = target ?? throw new ArgumentNullException(nameof(target));
+ if (HasSettersOrAnimations)
+ {
+ var match = Selector?.Match(target, Parent, true) ??
+ (target == host ?
+ SelectorMatch.AlwaysThisInstance :
+ SelectorMatch.NeverThisInstance);
- var match = Selector is object ? Selector.Match(target, Parent) :
- target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
+ if (match.IsMatch)
+ Attach(target, match.Activator);
- if (match.IsMatch && (_setters is object || _animations is object))
- {
- var instance = new StyleInstance(this, target, _setters, _animations, match.Activator);
- target.StyleApplied(instance);
- instance.Start();
+ result = match.Result;
}
- var result = match.Result;
+ var childResult = TryAttachChildren(target, host);
- if (_children is not null)
- {
- _childCache ??= new StyleCache();
- var childResult = _childCache.TryAttach(_children, target, host);
- if (childResult > result)
- result = childResult;
- }
+ if (childResult > result)
+ result = childResult;
return result;
}
- public bool TryGetResource(object key, out object? result)
- {
- result = null;
- return _resources?.TryGetResource(key, out result) ?? false;
- }
-
///
/// Returns a string representation of the style.
///
@@ -158,41 +77,30 @@ namespace Avalonia.Styling
}
}
- void IResourceProvider.AddOwner(IResourceHost owner)
+ internal override void SetParent(StyleBase? parent)
{
- owner = owner ?? throw new ArgumentNullException(nameof(owner));
-
- if (Owner != null)
+ if (parent is Style parentStyle && parentStyle.Selector is not null)
{
- throw new InvalidOperationException("The Style already has a parent.");
- }
-
- Owner = owner;
- _resources?.AddOwner(owner);
- }
-
- void IResourceProvider.RemoveOwner(IResourceHost owner)
- {
- owner = owner ?? throw new ArgumentNullException(nameof(owner));
-
- if (Owner == owner)
- {
- Owner = null;
- _resources?.RemoveOwner(owner);
+ if (Selector is null)
+ throw new InvalidOperationException("Child styles must have a selector.");
+ Selector.ValidateNestingSelector(false);
}
- }
-
- internal void SetParent(Style? parent)
- {
- if (parent?.Selector is not null)
+ else if (parent is ControlTheme)
{
if (Selector is null)
throw new InvalidOperationException("Child styles must have a selector.");
- if (!Selector.HasValidNestingSelector())
- throw new InvalidOperationException("Child styles must have a nesting selector.");
+ Selector.ValidateNestingSelector(true);
}
- Parent = parent;
+ base.SetParent(parent);
+ }
+
+ private static Selector? ValidateSelector(Selector? selector)
+ {
+ if (selector is TemplateSelector)
+ throw new InvalidOperationException(
+ "Invalid selector: Template selector must be followed by control selector.");
+ return selector;
}
}
}
diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs
new file mode 100644
index 0000000000..306a4cf010
--- /dev/null
+++ b/src/Avalonia.Base/Styling/StyleBase.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Metadata;
+using Avalonia.Styling.Activators;
+
+namespace Avalonia.Styling
+{
+ ///
+ /// Base class for and .
+ ///
+ public abstract class StyleBase : AvaloniaObject, IStyle, IResourceProvider
+ {
+ private IResourceHost? _owner;
+ private StyleChildren? _children;
+ private IResourceDictionary? _resources;
+ private List? _setters;
+ private List? _animations;
+ private StyleCache? _childCache;
+
+ public IList Children => _children ??= new(this);
+
+ public IResourceHost? Owner
+ {
+ get => _owner;
+ private set
+ {
+ if (_owner != value)
+ {
+ _owner = value;
+ OwnerChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public IStyle? Parent { get; private set; }
+
+ public IResourceDictionary Resources
+ {
+ get => _resources ?? (Resources = new ResourceDictionary());
+ set
+ {
+ value = value ?? throw new ArgumentNullException(nameof(value));
+
+ var hadResources = _resources?.HasResources ?? false;
+
+ _resources = value;
+
+ if (Owner is object)
+ {
+ _resources.AddOwner(Owner);
+
+ if (hadResources || _resources.HasResources)
+ {
+ Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
+ }
+ }
+ }
+ }
+
+ public IList Setters => _setters ??= new List();
+ public IList Animations => _animations ??= new List();
+
+ bool IResourceNode.HasResources => _resources?.Count > 0;
+ IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty();
+
+ internal bool HasSettersOrAnimations => _setters?.Count > 0 || _animations?.Count > 0;
+
+ public void Add(ISetter setter) => Setters.Add(setter);
+ public void Add(IStyle style) => Children.Add(style);
+
+ public event EventHandler? OwnerChanged;
+
+ public abstract SelectorMatchResult TryAttach(IStyleable target, object? host);
+
+ public bool TryGetResource(object key, out object? result)
+ {
+ result = null;
+ return _resources?.TryGetResource(key, out result) ?? false;
+ }
+
+ internal void Attach(IStyleable target, IStyleActivator? activator)
+ {
+ var instance = new StyleInstance(this, target, _setters, _animations, activator);
+ target.StyleApplied(instance);
+ instance.Start();
+ }
+
+ internal SelectorMatchResult TryAttachChildren(IStyleable target, object? host)
+ {
+ if (_children is null || _children.Count == 0)
+ return SelectorMatchResult.NeverThisType;
+ _childCache ??= new StyleCache();
+ return _childCache.TryAttach(_children, target, host);
+ }
+
+ internal virtual void SetParent(StyleBase? parent) => Parent = parent;
+
+ void IResourceProvider.AddOwner(IResourceHost owner)
+ {
+ owner = owner ?? throw new ArgumentNullException(nameof(owner));
+
+ if (Owner != null)
+ {
+ throw new InvalidOperationException("The Style already has a parent.");
+ }
+
+ Owner = owner;
+ _resources?.AddOwner(owner);
+ }
+
+ void IResourceProvider.RemoveOwner(IResourceHost owner)
+ {
+ owner = owner ?? throw new ArgumentNullException(nameof(owner));
+
+ if (Owner == owner)
+ {
+ Owner = null;
+ _resources?.RemoveOwner(owner);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/StyleCache.cs b/src/Avalonia.Base/Styling/StyleCache.cs
index 3285476880..81196f6a27 100644
--- a/src/Avalonia.Base/Styling/StyleCache.cs
+++ b/src/Avalonia.Base/Styling/StyleCache.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Styling
///
internal class StyleCache : Dictionary?>
{
- public SelectorMatchResult TryAttach(IList styles, IStyleable target, IStyleHost? host)
+ public SelectorMatchResult TryAttach(IList styles, IStyleable target, object? host)
{
if (TryGetValue(target.StyleKey, out var cached))
{
diff --git a/src/Avalonia.Base/Styling/StyleChildren.cs b/src/Avalonia.Base/Styling/StyleChildren.cs
index 5f8635f155..42b0a331ee 100644
--- a/src/Avalonia.Base/Styling/StyleChildren.cs
+++ b/src/Avalonia.Base/Styling/StyleChildren.cs
@@ -5,20 +5,20 @@ namespace Avalonia.Styling
{
internal class StyleChildren : Collection
{
- private readonly Style _owner;
+ private readonly StyleBase _owner;
- public StyleChildren(Style owner) => _owner = owner;
+ public StyleChildren(StyleBase owner) => _owner = owner;
protected override void InsertItem(int index, IStyle item)
{
- (item as Style)?.SetParent(_owner);
+ (item as StyleBase)?.SetParent(_owner);
base.InsertItem(index, item);
}
protected override void RemoveItem(int index)
{
var item = Items[index];
- (item as Style)?.SetParent(null);
+ (item as StyleBase)?.SetParent(null);
if (_owner.Owner is IResourceHost host)
(item as IResourceProvider)?.RemoveOwner(host);
base.RemoveItem(index);
@@ -26,7 +26,7 @@ namespace Avalonia.Styling
protected override void SetItem(int index, IStyle item)
{
- (item as Style)?.SetParent(_owner);
+ (item as StyleBase)?.SetParent(_owner);
base.SetItem(index, item);
if (_owner.Owner is IResourceHost host)
(item as IResourceProvider)?.AddOwner(host);
diff --git a/src/Avalonia.Base/Styling/Styler.cs b/src/Avalonia.Base/Styling/Styler.cs
index 74cf77ea40..ad5c1cd102 100644
--- a/src/Avalonia.Base/Styling/Styler.cs
+++ b/src/Avalonia.Base/Styling/Styler.cs
@@ -1,19 +1,24 @@
using System;
-#nullable enable
-
namespace Avalonia.Styling
{
public class Styler : IStyler
{
public void ApplyStyles(IStyleable target)
{
- target = target ?? throw new ArgumentNullException(nameof(target));
+ _ = target ?? throw new ArgumentNullException(nameof(target));
+
+ // Apply the control theme.
+ target.GetEffectiveTheme()?.TryAttach(target, target);
+
+ // If the control has a themed templated parent then apply the styles from the
+ // templated parent theme.
+ if (target.TemplatedParent is IStyleable styleableParent)
+ styleableParent.GetEffectiveTheme()?.TryAttach(target, styleableParent);
+ // Apply styles from the rest of the tree.
if (target is IStyleHost styleHost)
- {
ApplyStyles(target, styleHost);
- }
}
private void ApplyStyles(IStyleable target, IStyleHost host)
@@ -21,14 +26,10 @@ namespace Avalonia.Styling
var parent = host.StylingParent;
if (parent != null)
- {
ApplyStyles(target, parent);
- }
if (host.IsStylesInitialized)
- {
host.Styles.TryAttach(target, host);
- }
}
}
}
diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs
index e4c3371007..c213475bb7 100644
--- a/src/Avalonia.Base/Styling/Styles.cs
+++ b/src/Avalonia.Base/Styling/Styles.cs
@@ -26,6 +26,11 @@ namespace Avalonia.Styling
{
_styles.ResetBehavior = ResetBehavior.Remove;
_styles.CollectionChanged += OnCollectionChanged;
+ _styles.Validate = i =>
+ {
+ if (i is ControlTheme)
+ throw new InvalidOperationException("ControlThemes cannot be added to a Styles collection.");
+ };
}
public Styles(IResourceHost owner)
@@ -111,7 +116,7 @@ namespace Avalonia.Styling
set => _styles[index] = value;
}
- public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
+ public SelectorMatchResult TryAttach(IStyleable target, object? host)
{
_cache ??= new StyleCache();
return _cache.TryAttach(this, target, host);
diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs
index b0a2dae8d6..278e24a203 100644
--- a/src/Avalonia.Base/Styling/TemplateSelector.cs
+++ b/src/Avalonia.Base/Styling/TemplateSelector.cs
@@ -49,6 +49,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
- internal override bool HasValidNestingSelector() => _parent?.HasValidNestingSelector() ?? false;
+ protected override Selector? MovePreviousOrParent() => _parent;
}
}
diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
index d52c8c7d5c..94a6db41f6 100644
--- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
+++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
@@ -141,7 +141,7 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
- internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
+ protected override Selector? MovePreviousOrParent() => _previous;
private string BuildSelectorString()
{
diff --git a/src/Avalonia.Controls/Design.cs b/src/Avalonia.Controls/Design.cs
index 07d2918a88..80600b2276 100644
--- a/src/Avalonia.Controls/Design.cs
+++ b/src/Avalonia.Controls/Design.cs
@@ -1,4 +1,5 @@
+using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Styling;
@@ -6,6 +7,8 @@ namespace Avalonia.Controls
{
public static class Design
{
+ private static Dictionary
IEnumerable Containers { get; }
+ ///
+ /// Gets or sets the theme to be applied to the items in the control.
+ ///
+ ControlTheme? ItemContainerTheme { get; set; }
+
///
/// Gets or sets the data template used to display the items in the control.
///
diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
index a76dcbe9c8..8b36b07cec 100644
--- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
+++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
@@ -4,6 +4,7 @@ using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
+using Avalonia.Styling;
namespace Avalonia.Controls.Generators
{
@@ -35,6 +36,11 @@ namespace Avalonia.Controls.Generators
///
public event EventHandler? Recycled;
+ ///
+ /// Gets or sets the theme to be applied to the items in the control.
+ ///
+ public ControlTheme? ItemContainerTheme { get; set; }
+
///
/// Gets or sets the data template used to display the items in the control.
///
@@ -190,10 +196,18 @@ namespace Avalonia.Controls.Generators
result.SetValue(
ContentPresenter.ContentTemplateProperty,
ItemTemplate,
- BindingPriority.TemplatedParent);
+ BindingPriority.Style);
}
}
+ if (ItemContainerTheme != null)
+ {
+ result.SetValue(
+ StyledElement.ThemeProperty,
+ ItemContainerTheme,
+ BindingPriority.TemplatedParent);
+ }
+
return result;
}
diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
index 635f3a7d37..3ff1b0702d 100644
--- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
+++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
@@ -44,28 +44,29 @@ namespace Avalonia.Controls.Generators
{
var container = item as T;
- if (container != null)
+ if (container is null)
{
- return container;
- }
- else
- {
- var result = new T();
+ container = new T();
if (ContentTemplateProperty != null)
{
- result.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style);
+ container.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style);
}
- result.SetValue(ContentProperty, item, BindingPriority.Style);
+ container.SetValue(ContentProperty, item, BindingPriority.Style);
if (!(item is IControl))
{
- result.DataContext = item;
+ container.DataContext = item;
}
+ }
- return result;
+ if (ItemContainerTheme != null)
+ {
+ container.SetValue(StyledElement.ThemeProperty, ItemContainerTheme, BindingPriority.Style);
}
+
+ return container;
}
///
diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
index 536a5fdd06..4e3deb5552 100644
--- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
+++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
@@ -71,6 +71,11 @@ namespace Avalonia.Controls.Generators
var template = GetTreeDataTemplate(item, ItemTemplate);
var result = new T();
+ if (ItemContainerTheme != null)
+ {
+ result.SetValue(Control.ThemeProperty, ItemContainerTheme, BindingPriority.Style);
+ }
+
result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style);
var itemsSelector = template.ItemsSelector(item);
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 56b0014c05..345e7fcac8 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -15,6 +15,7 @@ using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.VisualTree;
+using Avalonia.Styling;
namespace Avalonia.Controls
{
@@ -36,6 +37,12 @@ namespace Avalonia.Controls
public static readonly DirectProperty ItemsProperty =
AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ItemContainerThemeProperty =
+ AvaloniaProperty.Register(nameof(ItemContainerTheme));
+
///
/// Defines the property.
///
@@ -88,6 +95,7 @@ namespace Avalonia.Controls
{
_itemContainerGenerator = CreateItemContainerGenerator();
+ _itemContainerGenerator.ItemContainerTheme = ItemContainerTheme;
_itemContainerGenerator.ItemTemplate = ItemTemplate;
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
@@ -108,6 +116,15 @@ namespace Avalonia.Controls
set { SetAndRaise(ItemsProperty, ref _items, value); }
}
+ ///
+ /// Gets or sets the that is applied to the container element generated for each item.
+ ///
+ public ControlTheme? ItemContainerTheme
+ {
+ get { return GetValue(ItemContainerThemeProperty); }
+ set { SetValue(ItemContainerThemeProperty, value); }
+ }
+
///
/// Gets the number of items in .
///
@@ -349,6 +366,10 @@ namespace Avalonia.Controls
{
UpdatePseudoClasses(change.GetNewValue());
}
+ else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
+ {
+ _itemContainerGenerator.ItemContainerTheme = change.GetNewValue();
+ }
}
///
@@ -502,25 +523,47 @@ namespace Avalonia.Controls
IInputElement? from,
bool wrap)
{
- IInputElement? result;
- var c = from;
+ var current = from;
- do
+ for (;;)
{
- result = container.GetControl(direction, c, wrap);
+ var result = container.GetControl(direction, current, wrap);
- if (result != null &&
- result.Focusable &&
+ if (result is null)
+ {
+ return null;
+ }
+
+ if (result.Focusable &&
result.IsEffectivelyEnabled &&
result.IsEffectivelyVisible)
{
return result;
}
- c = result;
- } while (c != null && c != from && direction != NavigationDirection.First && direction != NavigationDirection.Last);
+ current = result;
+
+ if (current == from)
+ {
+ return null;
+ }
- return null;
+ switch (direction)
+ {
+ //We did not find an enabled first item. Move downwards until we find one.
+ case NavigationDirection.First:
+ direction = NavigationDirection.Down;
+ from = result;
+ break;
+
+ //We did not find an enabled last item. Move upwards until we find one.
+ case NavigationDirection.Last:
+ direction = NavigationDirection.Up;
+ from = result;
+ break;
+
+ }
+ }
}
private void PresenterChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)
diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
index 868cce879a..16aeb2f559 100644
--- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
+++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
@@ -53,7 +53,7 @@ namespace Avalonia.Controls.Platform
Menu.PointerPressed += PointerPressed;
Menu.PointerReleased += PointerReleased;
Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
- Menu.AddHandler(Avalonia.Controls.Menu.MenuOpenedEvent, MenuOpened);
+ Menu.AddHandler(MenuBase.MenuOpenedEvent, MenuOpened);
Menu.AddHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
Menu.AddHandler(MenuItem.PointerExitedItemEvent, PointerExited);
Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved);
@@ -89,7 +89,7 @@ namespace Avalonia.Controls.Platform
Menu.PointerPressed -= PointerPressed;
Menu.PointerReleased -= PointerReleased;
Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
- Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, MenuOpened);
+ Menu.RemoveHandler(MenuBase.MenuOpenedEvent, MenuOpened);
Menu.RemoveHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
Menu.RemoveHandler(MenuItem.PointerExitedItemEvent, PointerExited);
Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved);
@@ -175,7 +175,11 @@ namespace Avalonia.Controls.Platform
case Key.Left:
{
- if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen)
+ if (item is { IsSubMenuOpen: true, SelectedItem: null })
+ {
+ item.Close();
+ }
+ else if (item?.Parent is IMenuItem { IsTopLevel: false, IsSubMenuOpen: true } parent)
{
parent.Close();
parent.Focus();
diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
index 35033c58f0..dc52cc3ae2 100644
--- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs
+++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
@@ -367,6 +367,17 @@ namespace Avalonia.Controls.Primitives
{
}
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == ThemeProperty)
+ {
+ foreach (var child in this.GetTemplateChildren())
+ child.InvalidateStyles();
+ }
+ }
+
///
/// Called when the control's template is applied.
///
diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs
index 490b0b3ce3..2e3aa037c2 100644
--- a/src/Avalonia.Controls/TreeViewItem.cs
+++ b/src/Avalonia.Controls/TreeViewItem.cs
@@ -121,6 +121,11 @@ namespace Avalonia.Controls
{
ItemTemplate = _treeView.ItemTemplate;
}
+
+ if (ItemContainerTheme == null && _treeView?.ItemContainerTheme != null)
+ {
+ ItemContainerTheme = _treeView.ItemContainerTheme;
+ }
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs
index b009778f97..811f9c7baa 100644
--- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs
+++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs
@@ -37,6 +37,7 @@ namespace Avalonia.DesignerSupport
var localAsm = assemblyPath != null ? Assembly.LoadFile(Path.GetFullPath(assemblyPath)) : null;
var loaded = loader.Load(stream, localAsm, null, baseUri, true);
var style = loaded as IStyle;
+ var resources = loaded as ResourceDictionary;
if (style != null)
{
var substitute = Design.GetPreviewWith((AvaloniaObject)style);
@@ -58,6 +59,27 @@ namespace Avalonia.DesignerSupport
}
};
}
+ else if (resources != null)
+ {
+ var substitute = Design.GetPreviewWith(resources);
+ if (substitute != null)
+ {
+ substitute.Resources.MergedDictionaries.Add(resources);
+ control = substitute;
+ }
+ else
+ control = new StackPanel
+ {
+ Children =
+ {
+ new TextBlock {Text = "ResourceDictionaries can't be previewed without Design.PreviewWith. Add"},
+ new TextBlock {Text = ""},
+ new TextBlock {Text = " "},
+ new TextBlock {Text = ""},
+ new TextBlock {Text = "in your resource dictionary"}
+ }
+ };
+ }
else if (loaded is Application)
control = new TextBlock {Text = "Application can't be previewed in design view"};
else
diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json
index 403bb5a59a..2fbcbfdb6a 100644
--- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json
+++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json
@@ -10,6 +10,55 @@
"integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==",
"dev": true
},
+ "@jridgewell/gen-mapping": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
+ "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
+ "@jridgewell/resolve-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+ "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+ "dev": true
+ },
+ "@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true
+ },
+ "@jridgewell/source-map": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
+ "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/gen-mapping": "^0.3.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
+ "@jridgewell/sourcemap-codec": {
+ "version": "1.4.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+ "dev": true
+ },
+ "@jridgewell/trace-mapping": {
+ "version": "0.3.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
+ "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
"@types/eslint": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz",
@@ -2136,6 +2185,12 @@
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==",
"dev": true
},
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@@ -2153,6 +2208,16 @@
"source-map-js": "^1.0.1"
}
},
+ "source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
"string-template": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
@@ -2208,13 +2273,14 @@
"dev": true
},
"terser": {
- "version": "5.10.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
- "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
+ "version": "5.14.2",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
+ "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"dev": true,
"requires": {
+ "@jridgewell/source-map": "^0.3.2",
+ "acorn": "^8.5.0",
"commander": "^2.20.0",
- "source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
@@ -2223,30 +2289,6 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
- },
- "source-map": {
- "version": "0.7.3",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
- "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
- "dev": true
- },
- "source-map-support": {
- "version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "dev": true,
- "requires": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- }
- }
}
}
},
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
index f8e2e0544f..631da80d8b 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
@@ -67,8 +67,15 @@ namespace Avalonia.Diagnostics.ViewModels
var setters = new List();
- if (styleSource is Style style)
+ if (styleSource is StyleBase style)
{
+ var selector = style switch
+ {
+ Style s => s.Selector?.ToString(),
+ ControlTheme t => t.TargetType?.Name.ToString(),
+ _ => null,
+ };
+
foreach (var setter in style.Setters)
{
if (setter is Setter regularSetter
@@ -105,7 +112,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters));
+ AppliedStyles.Add(new StyleViewModel(appliedStyle, selector ?? "No selector", setters));
}
}
diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs
index 9f9d75b411..ef99838208 100644
--- a/src/Avalonia.FreeDesktop/DBusHelper.cs
+++ b/src/Avalonia.FreeDesktop/DBusHelper.cs
@@ -24,8 +24,7 @@ namespace Avalonia.FreeDesktop
if (_ctx is not null)
_ctx?.Post(d, state);
else
- lock (_lock)
- d(state);
+ d(state);
}
}
diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs
index c17d5b993c..7974069184 100644
--- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs
+++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs
@@ -15,27 +15,26 @@ namespace Avalonia.FreeDesktop
{
internal class DBusSystemDialog : BclStorageProvider
{
- private static readonly Lazy s_fileChooser = new(() =>
+ private static readonly Lazy s_fileChooser = new(() => DBusHelper.Connection?
+ .CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"));
+
+ internal static async Task TryCreate(IPlatformHandle handle)
{
- var fileChooser = DBusHelper.Connection?.CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
- if (fileChooser is null)
- return null;
- try
+ if (handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser)
{
- _ = fileChooser.GetVersionAsync();
- return fileChooser;
- }
- catch (Exception e)
- {
- Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}");
- return null;
+ try
+ {
+ await fileChooser.GetVersionAsync();
+ return new DBusSystemDialog(fileChooser, handle);
+ }
+ catch (Exception e)
+ {
+ Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}");
+ return null;
+ }
}
- });
- internal static DBusSystemDialog? TryCreate(IPlatformHandle handle)
- {
- return handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser
- ? new DBusSystemDialog(fileChooser, handle) : null;
+ return null;
}
private readonly IFileChooser _fileChooser;
diff --git a/src/Avalonia.Themes.Default/SimpleTheme.cs b/src/Avalonia.Themes.Default/SimpleTheme.cs
index 664c95644f..98e35355c8 100644
--- a/src/Avalonia.Themes.Default/SimpleTheme.cs
+++ b/src/Avalonia.Themes.Default/SimpleTheme.cs
@@ -103,7 +103,7 @@ namespace Avalonia.Themes.Default
void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner);
- public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
+ public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host);
public bool TryGetResource(object key, out object? value)
{
diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs
index 81601f72a1..79dd81a068 100644
--- a/src/Avalonia.Themes.Fluent/FluentTheme.cs
+++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs
@@ -174,7 +174,7 @@ namespace Avalonia.Themes.Fluent
}
}
- public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
+ public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host);
public bool TryGetResource(object key, out object? value)
{
diff --git a/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs b/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs
new file mode 100644
index 0000000000..5fe0f46b14
--- /dev/null
+++ b/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs
@@ -0,0 +1,64 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Platform.Storage;
+
+namespace Avalonia.X11.NativeDialogs;
+
+internal class CompositeStorageProvider : IStorageProvider
+{
+ private readonly IEnumerable>> _factories;
+ public CompositeStorageProvider(IEnumerable>> factories)
+ {
+ _factories = factories;
+ }
+
+ public bool CanOpen => true;
+ public bool CanSave => true;
+ public bool CanPickFolder => true;
+
+ private async Task EnsureStorageProvider()
+ {
+ foreach (var factory in _factories)
+ {
+ var provider = await factory();
+ if (provider is not null)
+ {
+ return provider;
+ }
+ }
+
+ throw new InvalidOperationException("Neither DBus nor GTK are available on the system");
+ }
+
+ public async Task> OpenFilePickerAsync(FilePickerOpenOptions options)
+ {
+ var provider = await EnsureStorageProvider().ConfigureAwait(false);
+ return await provider.OpenFilePickerAsync(options).ConfigureAwait(false);
+ }
+
+ public async Task SaveFilePickerAsync(FilePickerSaveOptions options)
+ {
+ var provider = await EnsureStorageProvider().ConfigureAwait(false);
+ return await provider.SaveFilePickerAsync(options).ConfigureAwait(false);
+ }
+
+ public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options)
+ {
+ var provider = await EnsureStorageProvider().ConfigureAwait(false);
+ return await provider.OpenFolderPickerAsync(options).ConfigureAwait(false);
+ }
+
+ public async Task OpenFileBookmarkAsync(string bookmark)
+ {
+ var provider = await EnsureStorageProvider().ConfigureAwait(false);
+ return await provider.OpenFileBookmarkAsync(bookmark).ConfigureAwait(false);
+ }
+
+ public async Task OpenFolderBookmarkAsync(string bookmark)
+ {
+ var provider = await EnsureStorageProvider().ConfigureAwait(false);
+ return await provider.OpenFolderBookmarkAsync(bookmark).ConfigureAwait(false);
+ }
+}
diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs
index c9e482db86..ae04c072a5 100644
--- a/src/Avalonia.X11/NativeDialogs/Gtk.cs
+++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs
@@ -264,8 +264,6 @@ namespace Avalonia.X11.NativeDialogs
public static Task StartGtk()
{
return StartGtkCore();
- lock (s_startGtkLock)
- return s_startGtkTask ??= StartGtkCore();
}
private static void GtkThread(TaskCompletionSource tcs)
diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
index 89d08a3974..89aa0340b5 100644
--- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
+++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
@@ -17,10 +17,10 @@ namespace Avalonia.X11.NativeDialogs
{
internal class GtkSystemDialog : BclStorageProvider
{
- private Task? _initialized;
+ private static Task? _initialized;
private readonly X11Window _window;
- public GtkSystemDialog(X11Window window)
+ private GtkSystemDialog(X11Window window)
{
_window = window;
}
@@ -31,10 +31,15 @@ namespace Avalonia.X11.NativeDialogs
public override bool CanPickFolder => true;
- public override async Task> OpenFilePickerAsync(FilePickerOpenOptions options)
+ internal static async Task TryCreate(X11Window window)
{
- await EnsureInitialized();
+ _initialized ??= StartGtk();
+
+ return await _initialized ? new GtkSystemDialog(window) : null;
+ }
+ public override async Task> OpenFilePickerAsync(FilePickerOpenOptions options)
+ {
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.Open,
@@ -46,8 +51,6 @@ namespace Avalonia.X11.NativeDialogs
public override async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
- await EnsureInitialized();
-
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.SelectFolder,
@@ -59,8 +62,6 @@ namespace Avalonia.X11.NativeDialogs
public override async Task SaveFilePickerAsync(FilePickerSaveOptions options)
{
- await EnsureInitialized();
-
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save,
@@ -225,19 +226,6 @@ namespace Avalonia.X11.NativeDialogs
return tcs.Task;
}
- private async Task EnsureInitialized()
- {
- if (_initialized == null)
- {
- _initialized = StartGtk();
- }
-
- if (!(await _initialized))
- {
- throw new Exception("Unable to initialize GTK on separate thread");
- }
- }
-
private static void UpdateParent(IntPtr chooser, IWindowImpl parentWindow)
{
var xid = parentWindow.Handle.Handle;
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index edb320d4f0..7043c60ae7 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -216,16 +216,16 @@ namespace Avalonia
public bool OverlayPopups { get; set; }
///
- /// Enables native file dialogs as well as global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc).
+ /// Enables global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc).
/// The default value is true.
///
public bool UseDBusMenu { get; set; } = true;
///
- /// Enables GTK file picker instead of default FreeDesktop.
- /// The default value is true. And FreeDesktop file picker is used instead if available.
+ /// Enables DBus file picker instead of GTK.
+ /// The default value is true.
///
- public bool UseGtkFilePicker { get; set; } = false;
+ public bool UseDBusFilePicker { get; set; } = true;
///
/// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 2f92448f4b..009ccb6159 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -22,6 +22,7 @@ using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.X11.Glx;
+using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.XLib;
// ReSharper disable IdentifierTypo
// ReSharper disable StringLiteralTypo
@@ -215,9 +216,11 @@ namespace Avalonia.X11
_x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1);
}
- var canUseFreeDekstopPicker = !platform.Options.UseGtkFilePicker && platform.Options.UseDBusMenu;
- StorageProvider = canUseFreeDekstopPicker && DBusSystemDialog.TryCreate(Handle) is {} dBusStorage
- ? dBusStorage : new NativeDialogs.GtkSystemDialog(this);
+ StorageProvider = new CompositeStorageProvider(new Func>[]
+ {
+ () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreate(Handle) : Task.FromResult(null),
+ () => GtkSystemDialog.TryCreate(this),
+ });
}
class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index e3a55feac9..0d41ec93b4 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
@@ -46,6 +46,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
);
InsertBefore(
+ new AvaloniaXamlIlControlThemeTransformer(),
new AvaloniaXamlIlSelectorTransformer(),
new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
new AvaloniaXamlIlBindingPathParser(),
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs
new file mode 100644
index 0000000000..1338dc7248
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs
@@ -0,0 +1,39 @@
+using System.Linq;
+using XamlX;
+using XamlX.Ast;
+using XamlX.Transform;
+using XamlX.Transform.Transformers;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
+{
+ class AvaloniaXamlIlControlThemeTransformer : IXamlAstTransformer
+ {
+ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+ {
+ if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.ControlTheme"))
+ return node;
+
+ // Check if we've already transformed this node.
+ if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlTargetTypeMetadataNode)
+ return node;
+
+ var targetTypeNode = on.Children.OfType()
+ .FirstOrDefault(p => p.Property.GetClrProperty().Name == "TargetType") ??
+ throw new XamlParseException("ControlTheme must have a TargetType.", node);
+
+ IXamlType targetType;
+
+ if (targetTypeNode.Values[0] is XamlTypeExtensionNode extension)
+ targetType = extension.Value.GetClrType();
+ else if (targetTypeNode.Values[0] is XamlAstTextNode text)
+ targetType = TypeReferenceResolver.ResolveType(context, text.Text, false, text, true).GetClrType();
+ else
+ throw new XamlParseException("Could not determine TargetType for ControlTheme.", targetTypeNode);
+
+ return new AvaloniaXamlIlTargetTypeMetadataNode(on,
+ new XamlAstClrTypeReference(targetTypeNode, targetType, false),
+ AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style);
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs
index e816265422..6da95be1c1 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs
@@ -1,19 +1,14 @@
-using System;
using System.Collections.Generic;
using System.Linq;
-using Avalonia.Data.Core;
-using XamlX;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Transform;
-using XamlX.Transform.Transformers;
using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
using XamlParseException = XamlX.XamlParseException;
- using XamlLoadException = XamlX.XamlLoadException;
class AvaloniaXamlIlSetterTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
@@ -22,35 +17,23 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
&& on.Type.GetClrType().FullName == "Avalonia.Styling.Setter"))
return node;
- var parent = context.ParentNodes().OfType()
- .FirstOrDefault(p => p.Type.GetClrType().FullName == "Avalonia.Styling.Style");
-
- if (parent == null)
- throw new XamlParseException(
- "Avalonia.Styling.Setter is only valid inside Avalonia.Styling.Style", node);
- var selectorProperty = parent.Children.OfType()
- .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector");
- if (selectorProperty == null)
- throw new XamlParseException(
- "Can not find parent Style Selector", node);
- var selector = selectorProperty.Values.FirstOrDefault() as XamlIlSelectorNode;
- if (selector?.TargetType == null)
- throw new XamlParseException(
- "Can not resolve parent Style Selector type", node);
+ var targetTypeNode = context.ParentNodes()
+ .OfType()
+ .FirstOrDefault(x => x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) ??
+ throw new XamlParseException("Can not find parent Style Selector or ControlTemplate TargetType", node);
IXamlType propType = null;
var property = @on.Children.OfType()
.FirstOrDefault(x => x.Property.GetClrProperty().Name == "Property");
if (property != null)
{
-
var propertyName = property.Values.OfType().FirstOrDefault()?.Text;
if (propertyName == null)
throw new XamlParseException("Setter.Property must be a string", node);
var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName,
- new XamlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]);
+ new XamlAstClrTypeReference(targetTypeNode, targetTypeNode.TargetType.GetClrType(), false), property.Values[0]);
property.Values = new List {avaloniaPropertyNode};
propType = avaloniaPropertyNode.AvaloniaPropertyType;
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
index 46b5bc0c40..d92003ad9f 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
@@ -82,7 +82,7 @@ namespace Avalonia.Markup.Xaml.Styling
}
}
- public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
+ public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host);
public bool TryGetResource(object key, out object? value)
{
diff --git a/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs b/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs
new file mode 100644
index 0000000000..7a27a02fc4
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs
@@ -0,0 +1,92 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Styling;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Styling
+{
+ public class ControlThemeTests
+ {
+ [Fact]
+ public void ControlTheme_Cannot_Be_Added_To_Styles()
+ {
+ var target = new ControlTheme(typeof(Button));
+ var styles = new Styles();
+
+ Assert.Throws(() => styles.Add(target));
+ }
+
+ [Fact]
+ public void ControlTheme_Cannot_Be_Added_To_Style_Children()
+ {
+ var target = new ControlTheme(typeof(Button));
+ var style = new Style();
+
+ Assert.Throws(() => style.Children.Add(target));
+ }
+
+ [Fact]
+ public void ControlTheme_Cannot_Be_Added_To_ControlTheme_Children()
+ {
+ var target = new ControlTheme(typeof(Button));
+ var other = new ControlTheme(typeof(CheckBox));
+
+ Assert.Throws(() => other.Children.Add(target));
+ }
+
+ [Fact]
+ public void Style_Without_Selector_Cannot_Be_Added_To_Children()
+ {
+ var target = new ControlTheme(typeof(Button));
+ var child = new Style();
+
+ Assert.Throws(() => target.Children.Add(child));
+ }
+
+ [Fact]
+ public void Style_Without_Nesting_Selector_Cannot_Be_Added_To_Children()
+ {
+ var target = new ControlTheme(typeof(Button));
+ var child = new Style(x => x.OfType