From 9bb1bcce22a6c3cbece519d1216755a13b1c6124 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 26 Feb 2024 23:10:02 -0800 Subject: [PATCH] Improve flexibility with build-in controls localization (#13773) * Avoid hardcoding strings in DateTimePicker cs files * Add invariant resources * Implement ResourceProvider for flexibility of localization with custom resource provider * Fix these weird tests * Seal some ResourceDictionary extension points * Replace "Locale" with "String" --- .../Controls/ResourceDictionary.cs | 77 ++++--------- .../Controls/ResourceProvider.cs | 102 ++++++++++++++++++ .../DateTimePickers/DatePicker.cs | 7 +- .../DateTimePickers/TimePicker.cs | 20 +++- .../Accents/SystemAccentColors.cs | 40 +++---- .../Controls/DatePicker.xaml | 12 +-- .../Controls/ManagedFileChooser.xaml | 35 +++--- .../Controls/SelectableTextBlock.xaml | 2 +- .../Controls/TextBox.xaml | 12 +-- .../Controls/TimePicker.xaml | 2 + src/Avalonia.Themes.Fluent/FluentTheme.xaml | 1 + .../Strings/InvariantResources.xaml | 28 +++++ .../Avalonia.Themes.Simple.csproj | 3 + .../Controls/DatePicker.xaml | 6 +- .../Controls/ManagedFileChooser.xaml | 35 +++--- .../Controls/SelectableTextBlock.xaml | 2 +- .../Controls/TextBox.xaml | 6 +- .../Controls/TimePicker.xaml | 2 + src/Avalonia.Themes.Simple/SimpleTheme.xaml | 1 + .../DatePickerTests.cs | 12 +-- .../TimePickerTests.cs | 8 +- 21 files changed, 268 insertions(+), 145 deletions(-) create mode 100644 src/Avalonia.Base/Controls/ResourceProvider.cs create mode 100644 src/Avalonia.Themes.Fluent/Strings/InvariantResources.xaml diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index b928cf0672..285031e256 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Templates; @@ -13,11 +14,10 @@ namespace Avalonia.Controls /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : IResourceDictionary, IThemeVariantProvider + public class ResourceDictionary : ResourceProvider, IResourceDictionary, IThemeVariantProvider { private object? lastDeferredItemKey; private Dictionary? _inner; - private IResourceHost? _owner; private AvaloniaList? _mergedDictionaries; private AvaloniaDictionary? _themeDictionary; @@ -29,7 +29,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public ResourceDictionary(IResourceHost owner) => Owner = owner; + public ResourceDictionary(IResourceHost owner) : base(owner) { } public int Count => _inner?.Count ?? 0; @@ -50,19 +50,6 @@ namespace Avalonia.Controls public ICollection Keys => (ICollection?)_inner?.Keys ?? Array.Empty(); public ICollection Values => (ICollection?)_inner?.Values ?? Array.Empty(); - public IResourceHost? Owner - { - get => _owner; - private set - { - if (_owner != value) - { - _owner = value; - OwnerChanged?.Invoke(this, EventArgs.Empty); - } - } - } - public IList MergedDictionaries { get @@ -123,7 +110,7 @@ namespace Avalonia.Controls ThemeVariant? IThemeVariantProvider.Key { get; set; } - bool IResourceNode.HasResources + public sealed override bool HasResources { get { @@ -150,9 +137,7 @@ namespace Avalonia.Controls bool ICollection>.IsReadOnly => false; private Dictionary Inner => _inner ??= new(); - - public event EventHandler? OwnerChanged; - + public void Add(object key, object? value) { Inner.Add(key, value); @@ -187,7 +172,7 @@ namespace Avalonia.Controls return false; } - public bool TryGetResource(object key, ThemeVariant? theme, out object? value) + public sealed override bool TryGetResource(object key, ThemeVariant? theme, out object? value) { if (TryGetValue(key, out value)) return true; @@ -316,17 +301,8 @@ namespace Avalonia.Controls return false; } - void IResourceProvider.AddOwner(IResourceHost owner) + protected sealed override void OnAddOwner(IResourceHost owner) { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); - - if (Owner != null) - { - throw new InvalidOperationException("The ResourceDictionary already has a parent."); - } - - Owner = owner; - var hasResources = _inner?.Count > 0; if (_mergedDictionaries is not null) @@ -352,37 +328,30 @@ namespace Avalonia.Controls } } - void IResourceProvider.RemoveOwner(IResourceHost owner) + protected sealed override void OnRemoveOwner(IResourceHost owner) { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); + var hasResources = _inner?.Count > 0; - if (Owner == owner) + if (_mergedDictionaries is not null) { - Owner = null; - - var hasResources = _inner?.Count > 0; - - if (_mergedDictionaries is not null) + foreach (var i in _mergedDictionaries) { - foreach (var i in _mergedDictionaries) - { - i.RemoveOwner(owner); - hasResources |= i.HasResources; - } + i.RemoveOwner(owner); + hasResources |= i.HasResources; } - if (_themeDictionary is not null) + } + if (_themeDictionary is not null) + { + foreach (var i in _themeDictionary.Values) { - foreach (var i in _themeDictionary.Values) - { - i.RemoveOwner(owner); - hasResources |= i.HasResources; - } + i.RemoveOwner(owner); + hasResources |= i.HasResources; } + } - if (hasResources) - { - owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); - } + if (hasResources) + { + owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); } } diff --git a/src/Avalonia.Base/Controls/ResourceProvider.cs b/src/Avalonia.Base/Controls/ResourceProvider.cs new file mode 100644 index 0000000000..f10a816845 --- /dev/null +++ b/src/Avalonia.Base/Controls/ResourceProvider.cs @@ -0,0 +1,102 @@ +using System; +using Avalonia.Styling; + +namespace Avalonia.Controls; + +/// +/// Base implementation for IResourceProvider interface. +/// Includes Owner property management. +/// +public abstract class ResourceProvider : IResourceProvider +{ + private IResourceHost? _owner; + + public ResourceProvider() + { + } + + public ResourceProvider(IResourceHost owner) + { + _owner = owner; + } + + /// + public abstract bool HasResources { get; } + + /// + public abstract bool TryGetResource(object key, ThemeVariant? theme, out object? value); + + /// + public IResourceHost? Owner + { + get => _owner; + private set + { + if (_owner != value) + { + _owner = value; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + /// + public event EventHandler? OwnerChanged; + + protected void RaiseResourcesChanged() + { + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + + /// + /// Handles when owner was added. + /// Base method implementation raises , if this provider has any resources. + /// + /// New owner. + protected virtual void OnAddOwner(IResourceHost owner) + { + if (HasResources) + { + owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + } + + /// + /// Handles when owner was removed. + /// Base method implementation raises , if this provider has any resources. + /// + /// Old owner. + protected virtual void OnRemoveOwner(IResourceHost owner) + { + if (HasResources) + { + owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + } + + void IResourceProvider.AddOwner(IResourceHost owner) + { + owner = owner ?? throw new ArgumentNullException(nameof(owner)); + + if (Owner != null) + { + throw new InvalidOperationException("The ResourceDictionary already has a parent."); + } + + Owner = owner; + + OnAddOwner(owner); + } + + void IResourceProvider.RemoveOwner(IResourceHost owner) + { + owner = owner ?? throw new ArgumentNullException(nameof(owner)); + + if (Owner == owner) + { + Owner = null; + + OnRemoveOwner(owner); + } + } +} diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index c1f2a63f84..212cb64b13 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -373,10 +373,11 @@ namespace Avalonia.Controls } else { + // By clearing local value, we reset text property to the value from the template. + _monthText!.ClearValue(TextBlock.TextProperty); + _yearText!.ClearValue(TextBlock.TextProperty); + _dayText!.ClearValue(TextBlock.TextProperty); PseudoClasses.Set(":hasnodate", true); - _monthText!.Text = "month"; - _yearText!.Text = "year"; - _dayText!.Text = "day"; } } diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index 8eda9e1f0b..62ac76e71c 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -190,10 +190,19 @@ namespace Avalonia.Controls if (_contentGrid == null) return; - bool use24HourClock = ClockIdentifier == "24HourClock"; + var use24HourClock = ClockIdentifier == "24HourClock"; - var columnsD = use24HourClock ? "*, Auto, *" : "*, Auto, *, Auto, *"; - _contentGrid.ColumnDefinitions = new ColumnDefinitions(columnsD); + var columnsD = new ColumnDefinitions(); + columnsD.Add(new ColumnDefinition(GridLength.Star)); + columnsD.Add(new ColumnDefinition(GridLength.Auto)); + columnsD.Add(new ColumnDefinition(GridLength.Star)); + if (!use24HourClock) + { + columnsD.Add(new ColumnDefinition(GridLength.Auto)); + columnsD.Add(new ColumnDefinition(GridLength.Star)); + } + + _contentGrid.ColumnDefinitions = columnsD; _thirdPickerHost!.IsVisible = !use24HourClock; _secondSplitter!.IsVisible = !use24HourClock; @@ -232,8 +241,9 @@ namespace Avalonia.Controls } else { - _hourText.Text = "hour"; - _minuteText.Text = "minute"; + // By clearing local value, we reset text property to the value from the template. + _hourText.ClearValue(TextBlock.TextProperty); + _minuteText.ClearValue(TextBlock.TextProperty); PseudoClasses.Set(":hasnotime", true); _periodText.Text = DateTime.Now.Hour >= 12 ? TimeUtils.GetPMDesignator() : TimeUtils.GetAMDesignator(); diff --git a/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs b/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs index a796a5704d..95f135a40a 100644 --- a/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs +++ b/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs @@ -6,7 +6,7 @@ using Avalonia.Styling; namespace Avalonia.Themes.Fluent.Accents; -internal class SystemAccentColors : IResourceProvider +internal sealed class SystemAccentColors : ResourceProvider { public const string AccentKey = "SystemAccentColor"; public const string AccentDark1Key = "SystemAccentColorDark1"; @@ -22,8 +22,8 @@ internal class SystemAccentColors : IResourceProvider private Color _systemAccentColorDark1, _systemAccentColorDark2, _systemAccentColorDark3; private Color _systemAccentColorLight1, _systemAccentColorLight2, _systemAccentColorLight3; - public bool HasResources => true; - public bool TryGetResource(object key, ThemeVariant? theme, out object? value) + public override bool HasResources => true; + public override bool TryGetResource(object key, ThemeVariant? theme, out object? value) { if (key is string strKey) { @@ -81,38 +81,24 @@ internal class SystemAccentColors : IResourceProvider return false; } - public IResourceHost? Owner { get; private set; } - public event EventHandler? OwnerChanged; - public void AddOwner(IResourceHost owner) + protected override void OnAddOwner(IResourceHost owner) { - if (Owner != owner) + if (GetFromOwner(owner) is { } platformSettings) { - Owner = owner; - OwnerChanged?.Invoke(this, EventArgs.Empty); - - if (GetFromOwner(owner) is { } platformSettings) - { - platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; - } - - _invalidateColors = true; + platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; } + + _invalidateColors = true; } - public void RemoveOwner(IResourceHost owner) + protected override void OnRemoveOwner(IResourceHost owner) { - if (Owner == owner) + if (GetFromOwner(owner) is { } platformSettings) { - Owner = null; - OwnerChanged?.Invoke(this, EventArgs.Empty); - - if (GetFromOwner(owner) is { } platformSettings) - { - platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged; - } - - _invalidateColors = true; + platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged; } + + _invalidateColors = true; } private void EnsureColors() diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index 49f8174791..e7be603a16 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -97,12 +97,12 @@ VerticalAlignment="Stretch" TemplatedControl.IsTemplateFocusTarget="True"> - - - + + + @@ -183,12 +184,12 @@ IsVisible="{Binding ShowFilters}" ItemsSource="{Binding Filters}" SelectedItem="{Binding SelectedFilter}" /> - + - + - - + - +