Skip to content

Commit

Permalink
Improve flexibility with build-in controls localization (#13773)
Browse files Browse the repository at this point in the history
* 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"
  • Loading branch information
maxkatz6 authored Feb 27, 2024
1 parent b6ee7b9 commit 9bb1bcc
Show file tree
Hide file tree
Showing 21 changed files with 268 additions and 145 deletions.
77 changes: 23 additions & 54 deletions src/Avalonia.Base/Controls/ResourceDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,11 +14,10 @@ namespace Avalonia.Controls
/// <summary>
/// An indexed dictionary of resources.
/// </summary>
public class ResourceDictionary : IResourceDictionary, IThemeVariantProvider
public class ResourceDictionary : ResourceProvider, IResourceDictionary, IThemeVariantProvider
{
private object? lastDeferredItemKey;
private Dictionary<object, object?>? _inner;
private IResourceHost? _owner;
private AvaloniaList<IResourceProvider>? _mergedDictionaries;
private AvaloniaDictionary<ThemeVariant, IThemeVariantProvider>? _themeDictionary;

Expand All @@ -29,7 +29,7 @@ public ResourceDictionary() { }
/// <summary>
/// Initializes a new instance of the <see cref="ResourceDictionary"/> class.
/// </summary>
public ResourceDictionary(IResourceHost owner) => Owner = owner;
public ResourceDictionary(IResourceHost owner) : base(owner) { }

public int Count => _inner?.Count ?? 0;

Expand All @@ -50,19 +50,6 @@ public object? this[object key]
public ICollection<object> Keys => (ICollection<object>?)_inner?.Keys ?? Array.Empty<object>();
public ICollection<object?> Values => (ICollection<object?>?)_inner?.Values ?? Array.Empty<object?>();

public IResourceHost? Owner
{
get => _owner;
private set
{
if (_owner != value)
{
_owner = value;
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
}
}

public IList<IResourceProvider> MergedDictionaries
{
get
Expand Down Expand Up @@ -123,7 +110,7 @@ public IDictionary<ThemeVariant, IThemeVariantProvider> ThemeDictionaries

ThemeVariant? IThemeVariantProvider.Key { get; set; }

bool IResourceNode.HasResources
public sealed override bool HasResources
{
get
{
Expand All @@ -150,9 +137,7 @@ bool IResourceNode.HasResources
bool ICollection<KeyValuePair<object, object?>>.IsReadOnly => false;

private Dictionary<object, object?> Inner => _inner ??= new();

public event EventHandler? OwnerChanged;


public void Add(object key, object? value)
{
Inner.Add(key, value);
Expand Down Expand Up @@ -187,7 +172,7 @@ public bool Remove(object key)
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;
Expand Down Expand Up @@ -316,17 +301,8 @@ internal bool ContainsDeferredKey(object key)
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)
Expand All @@ -352,37 +328,30 @@ void IResourceProvider.AddOwner(IResourceHost owner)
}
}

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);
}
}

Expand Down
102 changes: 102 additions & 0 deletions src/Avalonia.Base/Controls/ResourceProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System;
using Avalonia.Styling;

namespace Avalonia.Controls;

/// <summary>
/// Base implementation for IResourceProvider interface.
/// Includes Owner property management.
/// </summary>
public abstract class ResourceProvider : IResourceProvider
{
private IResourceHost? _owner;

public ResourceProvider()
{
}

public ResourceProvider(IResourceHost owner)
{
_owner = owner;
}

/// <inheritdoc/>
public abstract bool HasResources { get; }

/// <inheritdoc/>
public abstract bool TryGetResource(object key, ThemeVariant? theme, out object? value);

/// <inheritdoc/>
public IResourceHost? Owner
{
get => _owner;
private set
{
if (_owner != value)
{
_owner = value;
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
}
}

/// <inheritdoc/>
public event EventHandler? OwnerChanged;

protected void RaiseResourcesChanged()
{
Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
}

/// <summary>
/// Handles when owner was added.
/// Base method implementation raises <see cref="IResourceHost.NotifyHostedResourcesChanged"/>, if this provider has any resources.
/// </summary>
/// <param name="owner">New owner.</param>
protected virtual void OnAddOwner(IResourceHost owner)
{
if (HasResources)
{
owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
}
}

/// <summary>
/// Handles when owner was removed.
/// Base method implementation raises <see cref="IResourceHost.NotifyHostedResourcesChanged"/>, if this provider has any resources.
/// </summary>
/// <param name="owner">Old owner.</param>
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);
}
}
}
7 changes: 4 additions & 3 deletions src/Avalonia.Controls/DateTimePickers/DatePicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,10 +373,11 @@ private void SetSelectedDateText()
}
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";
}
}

Expand Down
20 changes: 15 additions & 5 deletions src/Avalonia.Controls/DateTimePickers/TimePicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,19 @@ private void SetGrid()
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;
Expand Down Expand Up @@ -232,8 +241,9 @@ private void SetSelectedTimeText()
}
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();
Expand Down
40 changes: 13 additions & 27 deletions src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Avalonia.Themes.Fluent.Accents;

internal class SystemAccentColors : IResourceProvider
internal sealed class SystemAccentColors : ResourceProvider
{
public const string AccentKey = "SystemAccentColor";
public const string AccentDark1Key = "SystemAccentColorDark1";
Expand All @@ -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)
{
Expand Down Expand Up @@ -81,38 +81,24 @@ public bool TryGetResource(object key, ThemeVariant? theme, out object? value)
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()
Expand Down
12 changes: 6 additions & 6 deletions src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@
VerticalAlignment="Stretch"
TemplatedControl.IsTemplateFocusTarget="True">
<Grid Name="PART_ButtonContentGrid" ColumnDefinitions="78*,Auto,132*,Auto,78*">
<TextBlock Name="PART_DayTextBlock" Text="day" HorizontalAlignment="Center"
Padding="{DynamicResource DatePickerHostPadding}"/>
<TextBlock Name="PART_MonthTextBlock" Text="month" TextAlignment="Left"
Padding="{DynamicResource DatePickerHostMonthPadding}"/>
<TextBlock Name="PART_YearTextBlock" Text="year" HorizontalAlignment="Center"
Padding="{DynamicResource DatePickerHostPadding}"/>
<TextBlock Name="PART_DayTextBlock" Text="{DynamicResource StringDatePickerDayText}" HorizontalAlignment="Center"
Padding="{DynamicResource DatePickerHostPadding}" />
<TextBlock Name="PART_MonthTextBlock" Text="{DynamicResource StringDatePickerMonthText}" TextAlignment="Left"
Padding="{DynamicResource DatePickerHostMonthPadding}" />
<TextBlock Name="PART_YearTextBlock" Text="{DynamicResource StringDatePickerYearText}" HorizontalAlignment="Center"
Padding="{DynamicResource DatePickerHostPadding}" />
<Rectangle x:Name="PART_FirstSpacer"
Fill="{DynamicResource DatePickerSpacerFill}"
HorizontalAlignment="Center"
Expand Down
Loading

0 comments on commit 9bb1bcc

Please sign in to comment.