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? _previewWith; + public static bool IsDesignMode { get; internal set; } public static readonly AttachedProperty HeightProperty = AvaloniaProperty @@ -47,19 +50,30 @@ namespace Avalonia.Controls return control.GetValue(DataContextProperty); } - public static readonly AttachedProperty PreviewWithProperty = AvaloniaProperty - .RegisterAttached("PreviewWith", typeof (Design)); + public static readonly AttachedProperty PreviewWithProperty = AvaloniaProperty + .RegisterAttached("PreviewWith", typeof (Design)); - public static void SetPreviewWith(AvaloniaObject target, Control control) + public static void SetPreviewWith(AvaloniaObject target, Control? control) { target.SetValue(PreviewWithProperty, control); } - public static Control GetPreviewWith(AvaloniaObject target) + public static void SetPreviewWith(ResourceDictionary target, Control? control) + { + _previewWith ??= new(); + _previewWith[target] = control; + } + + public static Control? GetPreviewWith(AvaloniaObject target) { return target.GetValue(PreviewWithProperty); } + public static Control? GetPreviewWith(ResourceDictionary target) + { + return _previewWith?[target]; + } + public static readonly AttachedProperty DesignStyleProperty = AvaloniaProperty .RegisterAttached("DesignStyle", typeof(Design)); diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index 70d9dbec08..f9772cb399 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Controls.Templates; +using Avalonia.Styling; namespace Avalonia.Controls.Generators { @@ -14,6 +15,11 @@ namespace Avalonia.Controls.Generators /// 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