Browse Source

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"
pull/14744/head
Max Katz 2 years ago
committed by GitHub
parent
commit
9bb1bcce22
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 77
      src/Avalonia.Base/Controls/ResourceDictionary.cs
  2. 102
      src/Avalonia.Base/Controls/ResourceProvider.cs
  3. 7
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  4. 20
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  5. 40
      src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs
  6. 12
      src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
  7. 35
      src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
  8. 2
      src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml
  9. 12
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  10. 2
      src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
  11. 1
      src/Avalonia.Themes.Fluent/FluentTheme.xaml
  12. 28
      src/Avalonia.Themes.Fluent/Strings/InvariantResources.xaml
  13. 3
      src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj
  14. 6
      src/Avalonia.Themes.Simple/Controls/DatePicker.xaml
  15. 35
      src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml
  16. 2
      src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml
  17. 6
      src/Avalonia.Themes.Simple/Controls/TextBox.xaml
  18. 2
      src/Avalonia.Themes.Simple/Controls/TimePicker.xaml
  19. 1
      src/Avalonia.Themes.Simple/SimpleTheme.xaml
  20. 12
      tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
  21. 8
      tests/Avalonia.Controls.UnitTests/TimePickerTests.cs

77
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
/// <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;
@ -29,7 +29,7 @@ namespace Avalonia.Controls
/// <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;
@ -50,19 +50,6 @@ namespace Avalonia.Controls
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
@ -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<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);
@ -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);
}
}

102
src/Avalonia.Base/Controls/ResourceProvider.cs

@ -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
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";
}
}

20
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();

40
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()

12
src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml

@ -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"

35
src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dialogs="using:Avalonia.Dialogs"
xmlns:internal="using:Avalonia.Dialogs.Internal"
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
x:ClassModifier="internal">
<Design.PreviewWith>
<Border Padding="20" Width="800" Height="500">
@ -183,12 +184,12 @@
IsVisible="{Binding ShowFilters}"
ItemsSource="{Binding Filters}"
SelectedItem="{Binding SelectedFilter}" />
<TextBox Text="{Binding FileName}" Watermark="File name" IsVisible="{Binding !SelectingFolder}" />
<TextBox Text="{Binding FileName}" Watermark="{DynamicResource StringManagedFileChooserFileNameWatermark}" IsVisible="{Binding !SelectingFolder}" />
</DockPanel>
<CheckBox IsChecked="{Binding ShowHiddenFiles}" Content="Show hidden files" DockPanel.Dock="Left"/>
<CheckBox IsChecked="{Binding ShowHiddenFiles}" Content="{DynamicResource StringManagedFileChooserShowHiddenFilesText}" DockPanel.Dock="Left"/>
<UniformGrid x:Name="Finalize" HorizontalAlignment="Right" Rows="1">
<Button Command="{Binding Ok}" MinWidth="80">OK</Button>
<Button Command="{Binding Cancel}" MinWidth="80">Cancel</Button>
<Button Command="{Binding Ok}" MinWidth="80" Content="{DynamicResource StringManagedFileChooserOkText}" />
<Button Command="{Binding Cancel}" MinWidth="80" Content="{DynamicResource StringManagedFileChooserCancelText}" />
</UniformGrid>
</DockPanel>
</DockPanel>
@ -218,13 +219,13 @@
</Setter>
</Style>
</Grid.Styles>
<TextBlock Grid.Column="1" Text="Name" />
<TextBlock Grid.Column="1" Text="{DynamicResource StringManagedFileChooserNameColumn}" />
<GridSplitter Grid.Column="2" />
<TextBlock Grid.Column="3" Text="Date Modified" />
<TextBlock Grid.Column="3" Text="{DynamicResource StringManagedFileChooserDateModifiedColumn}" />
<GridSplitter Grid.Column="4" />
<TextBlock Grid.Column="5" Text="Type" />
<TextBlock Grid.Column="5" Text="{DynamicResource StringManagedFileChooserTypeColumn}" />
<GridSplitter Grid.Column="6" />
<TextBlock Grid.Column="7" Text="Size" />
<TextBlock Grid.Column="7" Text="{DynamicResource StringManagedFileChooserSizeColumn}" />
<GridSplitter Grid.Column="8" />
</Grid>
<ListBox x:Name="PART_Files"
@ -355,18 +356,26 @@
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<StackPanel Spacing="10">
<TextBlock TextWrapping="Wrap"
Text="{Binding FileName, RelativeSource={RelativeSource TemplatedParent}, StringFormat='{}{0} already exists. Do you want to replace it?'}" />
<TextBlock TextWrapping="Wrap">
<TextBlock.Text>
<MultiBinding>
<MultiBinding.Converter>
<converters:StringFormatConverter />
</MultiBinding.Converter>
<DynamicResource ResourceKey="StringManagedFileChooserOverwritePromptFileAlreadyExistsText"/>
<Binding Path ="FileName" RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<StackPanel HorizontalAlignment="Right"
Spacing="10"
Orientation="Horizontal">
<Button Classes="accent" Content="Yes"
<Button Classes="accent" Content="{DynamicResource StringManagedFileChooserOverwritePromptConfirmText}"
MinWidth="80"
HorizontalContentAlignment="Center"
IsDefault="True"
Command="{Binding Confirm, RelativeSource={RelativeSource TemplatedParent}}" />
<Button Content="No"
<Button Content="{DynamicResource StringManagedFileChooserOverwritePromptCancelText}"
MinWidth="80"
IsCancel="True"
HorizontalContentAlignment="Center"

2
src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml

@ -6,7 +6,7 @@
</Design.PreviewWith>
<MenuFlyout x:Key="SelectableTextBlockContextFlyout" Placement="Bottom">
<MenuItem x:Name="SelectableTextBlockContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[SelectableTextBlock].Copy}"
<MenuItem x:Name="SelectableTextBlockContextFlyoutCopyItem" Header="{DynamicResource StringTextFlyoutCopyText}" Command="{Binding $parent[SelectableTextBlock].Copy}"
IsEnabled="{Binding $parent[SelectableTextBlock].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}" />
</MenuFlyout>

12
src/Avalonia.Themes.Fluent/Controls/TextBox.xaml

@ -22,14 +22,14 @@
<StreamGeometry x:Key="PasswordBoxHideButtonData">m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z</StreamGeometry>
<MenuFlyout x:Key="DefaultTextBoxContextFlyout">
<MenuItem x:Name="TextBoxContextFlyoutCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
<MenuItem x:Name="TextBoxContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
<MenuItem x:Name="TextBoxContextFlyoutPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}"/>
<MenuItem x:Name="TextBoxContextFlyoutCutItem" Header="{DynamicResource StringTextFlyoutCutText}" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
<MenuItem x:Name="TextBoxContextFlyoutCopyItem" Header="{DynamicResource StringTextFlyoutCopyText}" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
<MenuItem x:Name="TextBoxContextFlyoutPasteItem" Header="{DynamicResource StringTextFlyoutPasteText}" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}"/>
</MenuFlyout>
<MenuFlyout x:Key="HorizontalTextBoxContextFlyout" FlyoutPresenterTheme="{StaticResource HorizontalMenuFlyoutPresenter}" ItemContainerTheme="{StaticResource HorizontalMenuItem}">
<MenuItem x:Name="HorizontalTextBoxContextFlyoutCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" IsVisible="{Binding $parent[TextBox].CanCut}" />
<MenuItem x:Name="HorizontalTextBoxContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" IsVisible="{Binding $parent[TextBox].CanCopy}" />
<MenuItem x:Name="HorizontalTextBoxContextFlyoutPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" />
<MenuItem x:Name="HorizontalTextBoxContextFlyoutCutItem" Header="{DynamicResource StringTextFlyoutCutText}" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" IsVisible="{Binding $parent[TextBox].CanCut}" />
<MenuItem x:Name="HorizontalTextBoxContextFlyoutCopyItem" Header="{DynamicResource StringTextFlyoutCopyText}" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" IsVisible="{Binding $parent[TextBox].CanCopy}" />
<MenuItem x:Name="HorizontalTextBoxContextFlyoutPasteItem" Header="{DynamicResource StringTextFlyoutPasteText}" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" />
</MenuFlyout>
<ControlTheme x:Key="FluentTextBoxButton" TargetType="Button">

2
src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml

@ -102,6 +102,7 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TextBlock x:Name="PART_HourTextBlock"
Text="{DynamicResource StringTimePickerHourText}"
HorizontalAlignment="Center"
Padding="{DynamicResource TimePickerHostPadding}" />
</Border>
@ -117,6 +118,7 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TextBlock x:Name="PART_MinuteTextBlock"
Text="{DynamicResource StringTimePickerMinuteText}"
HorizontalAlignment="Center"
Padding="{DynamicResource TimePickerHostPadding}"/>
</Border>

1
src/Avalonia.Themes.Fluent/FluentTheme.xaml

@ -15,6 +15,7 @@
<!-- Resources and brushes will be merged into current dictionary for slightly better performance and possible optimizations -->
<MergeResourceInclude Source="/Accents/BaseResources.xaml" />
<MergeResourceInclude Source="/Accents/FluentControlResources.xaml" />
<MergeResourceInclude Source="/Strings/InvariantResources.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- These are not part of MergedDictionaries so we can add or remove them easier -->

28
src/Avalonia.Themes.Fluent/Strings/InvariantResources.xaml

@ -0,0 +1,28 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal">
<!-- DatePicker -->
<x:String x:Key="StringDatePickerDayText">day</x:String>
<x:String x:Key="StringDatePickerMonthText">month</x:String>
<x:String x:Key="StringDatePickerYearText">year</x:String>
<!-- TimePicker -->
<x:String x:Key="StringTimePickerHourText">hour</x:String>
<x:String x:Key="StringTimePickerMinuteText">minute</x:String>
<!-- TextBox/SelectableTextBox flyout -->
<x:String x:Key="StringTextFlyoutCutText">Cut</x:String>
<x:String x:Key="StringTextFlyoutCopyText">Copy</x:String>
<x:String x:Key="StringTextFlyoutPasteText">Paste</x:String>
<!-- ManagedFileChooser -->
<x:String x:Key="StringManagedFileChooserFileNameWatermark">File name</x:String>
<x:String x:Key="StringManagedFileChooserShowHiddenFilesText">Show hidden files</x:String>
<x:String x:Key="StringManagedFileChooserOkText">OK</x:String>
<x:String x:Key="StringManagedFileChooserCancelText">Cancel</x:String>
<x:String x:Key="StringManagedFileChooserNameColumn">Name</x:String>
<x:String x:Key="StringManagedFileChooserDateModifiedColumn">Date Modified</x:String>
<x:String x:Key="StringManagedFileChooserTypeColumn">Type</x:String>
<x:String x:Key="StringManagedFileChooserSizeColumn">Size</x:String>
<x:String x:Key="StringManagedFileChooserOverwritePromptFileAlreadyExistsText">{0} already exists. Do you want to replace it?</x:String>
<x:String x:Key="StringManagedFileChooserOverwritePromptConfirmText">Yes</x:String>
<x:String x:Key="StringManagedFileChooserOverwritePromptCancelText">No</x:String>
</ResourceDictionary>

3
src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj

@ -9,6 +9,9 @@
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<AvaloniaResource Include="**/*.xaml" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="..\Avalonia.Themes.Fluent\Strings\InvariantResources.xaml" Link="Strings\InvariantResources.xaml" />
</ItemGroup>
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\TrimmingEnable.props" />

6
src/Avalonia.Themes.Simple/Controls/DatePicker.xaml

@ -113,13 +113,13 @@
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
Text="day" />
Text="{DynamicResource StringDatePickerDayText}" />
<TextBlock Name="PART_MonthTextBlock"
Padding="{DynamicResource DatePickerHostMonthPadding}"
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
Text="month"
Text="{DynamicResource StringDatePickerMonthText}"
TextAlignment="Left" />
<TextBlock Name="PART_YearTextBlock"
Padding="{DynamicResource DatePickerHostPadding}"
@ -127,7 +127,7 @@
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
Text="year" />
Text="{DynamicResource StringDatePickerYearText}" />
<Rectangle x:Name="PART_FirstSpacer"
Grid.Column="1"
Width="1"

35
src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dialogs="using:Avalonia.Dialogs"
xmlns:internal="using:Avalonia.Dialogs.Internal"
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
x:ClassModifier="internal">
<DrawingGroup x:Key="LevelUp">
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
@ -89,7 +90,7 @@
<StackPanel DockPanel.Dock="Left"
Orientation="Horizontal">
<CheckBox IsChecked="{Binding ShowHiddenFiles}">
<TextBlock>Show hidden files</TextBlock>
<TextBlock Text="{DynamicResource StringManagedFileChooserShowHiddenFilesText}" />
</CheckBox>
</StackPanel>
<StackPanel HorizontalAlignment="Right"
@ -100,8 +101,8 @@
<Setter Property="Margin" Value="4" />
</Style>
</StackPanel.Styles>
<Button Command="{Binding Ok}" MinWidth="60">OK</Button>
<Button Command="{Binding Cancel}" MinWidth="60">Cancel</Button>
<Button Command="{Binding Ok}" MinWidth="60" Content="{DynamicResource StringManagedFileChooserOkText}" />
<Button Command="{Binding Cancel}" MinWidth="60" Content="{DynamicResource StringManagedFileChooserCancelText}" />
</StackPanel>
</DockPanel>
@ -114,7 +115,7 @@
<TextBox DockPanel.Dock="Bottom"
IsVisible="{Binding !SelectingFolder}"
Text="{Binding FileName}"
Watermark="File name" />
Watermark="{DynamicResource StringManagedFileChooserFileNameWatermark}" />
<ListBox x:Name="PART_QuickLinks"
MaxWidth="200"
@ -154,13 +155,13 @@
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="1"
Text="Name" />
Text="{DynamicResource StringManagedFileChooserNameColumn}" />
<GridSplitter Grid.Column="2"
ResizeDirection="Columns"
Background="Transparent" />
<Rectangle HorizontalAlignment="Left" Grid.Column="2" VerticalAlignment="Stretch" Width="1" Fill="{DynamicResource ThemeControlMidBrush}"/>
<TextBlock Grid.Column="3"
Text="Date Modified" />
Text="{DynamicResource StringManagedFileChooserDateModifiedColumn}" />
<GridSplitter Grid.Column="4"
ResizeDirection="Columns"
Background="Transparent" />
@ -171,7 +172,7 @@
Fill="{DynamicResource ThemeControlMidBrush}"/>
<TextBlock Grid.Column="5"
Text="Type" />
Text="{DynamicResource StringManagedFileChooserTypeColumn}" />
<GridSplitter Grid.Column="6" ResizeDirection="Columns"
Background="Transparent" />
<Rectangle HorizontalAlignment="Left"
@ -181,7 +182,7 @@
Fill="{DynamicResource ThemeControlMidBrush}"/>
<TextBlock Grid.Column="7"
Text="Size" />
Text="{DynamicResource StringManagedFileChooserSizeColumn}" />
<GridSplitter Grid.Column="8"
ResizeDirection="Columns"
Background="Transparent" />
@ -253,18 +254,26 @@
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<StackPanel Spacing="10">
<TextBlock TextWrapping="Wrap"
Text="{Binding FileName, RelativeSource={RelativeSource TemplatedParent}, StringFormat='{}{0} already exists. Do you want to replace it?'}" />
<TextBlock TextWrapping="Wrap">
<TextBlock.Text>
<MultiBinding>
<MultiBinding.Converter>
<converters:StringFormatConverter />
</MultiBinding.Converter>
<DynamicResource ResourceKey="StringManagedFileChooserOverwritePromptFileAlreadyExistsText"/>
<Binding Path ="FileName" RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<StackPanel HorizontalAlignment="Right"
Spacing="10"
Orientation="Horizontal">
<Button Classes="accent" Content="Yes"
<Button Classes="accent" Content="{DynamicResource StringManagedFileChooserOverwritePromptConfirmText}"
MinWidth="80"
HorizontalContentAlignment="Center"
IsDefault="True"
Command="{Binding Confirm, RelativeSource={RelativeSource TemplatedParent}}" />
<Button Content="No"
<Button Content="{DynamicResource StringManagedFileChooserOverwritePromptCancelText}"
MinWidth="80"
IsCancel="True"
HorizontalContentAlignment="Center"

2
src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml

@ -6,7 +6,7 @@
</Design.PreviewWith>
<MenuFlyout x:Key="SelectableTextBlockContextFlyout" Placement="Bottom">
<MenuItem x:Name="SelectableTextBlockContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[SelectableTextBlock].Copy}"
<MenuItem x:Name="SelectableTextBlockContextFlyoutCopyItem" Header="{DynamicResource StringTextFlyoutCopyText}" Command="{Binding $parent[SelectableTextBlock].Copy}"
IsEnabled="{Binding $parent[SelectableTextBlock].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}" />
</MenuFlyout>

6
src/Avalonia.Themes.Simple/Controls/TextBox.xaml

@ -6,11 +6,11 @@
<StreamGeometry x:Key="PasswordBoxHideButtonData">m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z</StreamGeometry>
<MenuFlyout x:Key="SimpleTextBoxContextFlyout" Placement="Bottom">
<MenuItem x:Name="TextBoxContextFlyoutCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}"
<MenuItem x:Name="TextBoxContextFlyoutCutItem" Header="{DynamicResource StringTextFlyoutCutText}" Command="{Binding $parent[TextBox].Cut}"
IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
<MenuItem x:Name="TextBoxContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}"
<MenuItem x:Name="TextBoxContextFlyoutCopyItem" Header="{DynamicResource StringTextFlyoutCopyText}" Command="{Binding $parent[TextBox].Copy}"
IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}" />
<MenuItem x:Name="TextBoxContextFlyoutPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}"
<MenuItem x:Name="TextBoxContextFlyoutPasteItem" Header="{DynamicResource StringTextFlyoutPasteText}" Command="{Binding $parent[TextBox].Paste}"
IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}" />
</MenuFlyout>

2
src/Avalonia.Themes.Simple/Controls/TimePicker.xaml

@ -110,6 +110,7 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TextBlock x:Name="PART_HourTextBlock"
Text="{DynamicResource StringTimePickerHourText}"
Padding="{DynamicResource TimePickerHostPadding}"
HorizontalAlignment="Center"
FontFamily="{TemplateBinding FontFamily}"
@ -128,6 +129,7 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TextBlock x:Name="PART_MinuteTextBlock"
Text="{DynamicResource StringTimePickerMinuteText}"
Padding="{DynamicResource TimePickerHostPadding}"
HorizontalAlignment="Center"
FontFamily="{TemplateBinding FontFamily}"

1
src/Avalonia.Themes.Simple/SimpleTheme.xaml

@ -5,6 +5,7 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<MergeResourceInclude Source="/Accents/Base.xaml" />
<MergeResourceInclude Source="/Strings/InvariantResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>

12
tests/Avalonia.Controls.UnitTests/DatePickerTests.cs

@ -192,16 +192,16 @@ namespace Avalonia.Controls.UnitTests
DateTimeOffset value = new DateTimeOffset(2000, 10, 10, 0, 0, 0, TimeSpan.Zero);
datePicker.SelectedDate = value;
Assert.False(dayText.Text == "day");
Assert.False(monthText.Text == "month");
Assert.False(yearText.Text == "year");
Assert.NotNull(dayText.Text);
Assert.NotNull(monthText.Text);
Assert.NotNull(yearText.Text);
Assert.False(datePicker.Classes.Contains(":hasnodate"));
datePicker.SelectedDate = null;
Assert.True(dayText.Text == "day");
Assert.True(monthText.Text == "month");
Assert.True(yearText.Text == "year");
Assert.Null(dayText.Text);
Assert.Null(monthText.Text);
Assert.Null(yearText.Text);
Assert.True(datePicker.Classes.Contains(":hasnodate"));
}
}

8
tests/Avalonia.Controls.UnitTests/TimePickerTests.cs

@ -93,12 +93,12 @@ namespace Avalonia.Controls.UnitTests
TimeSpan ts = TimeSpan.FromHours(10);
timePicker.SelectedTime = ts;
Assert.False(hourText.Text == "hour");
Assert.False(minuteText.Text == "minute");
Assert.NotNull(hourText.Text);
Assert.NotNull(minuteText.Text);
timePicker.SelectedTime = null;
Assert.True(hourText.Text == "hour");
Assert.True(minuteText.Text == "minute");
Assert.Null(hourText.Text);
Assert.Null(minuteText.Text);
}
}

Loading…
Cancel
Save