diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index adea1b90fc..13c88d7ed1 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -174,6 +174,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml b/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml new file mode 100644 index 0000000000..2a6e375413 --- /dev/null +++ b/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml.cs b/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml.cs new file mode 100644 index 0000000000..b235c9a3ac --- /dev/null +++ b/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml.cs @@ -0,0 +1,15 @@ +using Avalonia.Controls; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class PlatformSettingsPage : UserControl + { + public PlatformSettingsPage() + { + InitializeComponent(); + DataContext = new PlatformSettingsViewModel(); + } + } +} + diff --git a/samples/ControlCatalog/ViewModels/PlatformSettingsViewModel.cs b/samples/ControlCatalog/ViewModels/PlatformSettingsViewModel.cs new file mode 100644 index 0000000000..a9ff1214cf --- /dev/null +++ b/samples/ControlCatalog/ViewModels/PlatformSettingsViewModel.cs @@ -0,0 +1,77 @@ +using System; +using Avalonia; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Platform; +using MiniMvvm; + +namespace ControlCatalog.ViewModels; + +public class PlatformSettingsViewModel : ViewModelBase +{ + private readonly IPlatformSettings? _platformSettings; + private PlatformColorValues? _colorValues; + private string? _preferredLanguage; + + public PlatformSettingsViewModel() + { + _platformSettings = AvaloniaLocator.Current.GetService(); + + if (_platformSettings != null) + { + _colorValues = _platformSettings.GetColorValues(); + _preferredLanguage = _platformSettings.PreferredApplicationLanguage; + + _platformSettings.ColorValuesChanged += OnColorValuesChanged; + _platformSettings.PreferredApplicationLanguageChanged += OnPreferredLanguageChanged; + } + } + + private void OnColorValuesChanged(object? sender, PlatformColorValues e) + { + _colorValues = e; + RaisePropertyChanged(nameof(ThemeVariant)); + RaisePropertyChanged(nameof(ContrastPreference)); + RaisePropertyChanged(nameof(AccentColor1)); + RaisePropertyChanged(nameof(AccentColor2)); + RaisePropertyChanged(nameof(AccentColor3)); + } + + private void OnPreferredLanguageChanged(object? sender, EventArgs e) + { + if (_platformSettings != null) + { + _preferredLanguage = _platformSettings.PreferredApplicationLanguage; + RaisePropertyChanged(nameof(PreferredLanguage)); + } + } + + public bool IsAvailable => _platformSettings != null; + + public string PreferredLanguage => _preferredLanguage ?? "Not available"; + + public string ThemeVariant => _colorValues?.ThemeVariant.ToString() ?? "Not available"; + + public string ContrastPreference => _colorValues?.ContrastPreference.ToString() ?? "Not available"; + + public Color AccentColor1 => _colorValues?.AccentColor1 ?? Colors.Gray; + + public Color AccentColor2 => _colorValues?.AccentColor2 ?? Colors.Gray; + + public Color AccentColor3 => _colorValues?.AccentColor3 ?? Colors.Gray; + + public string HoldWaitDuration => _platformSettings?.HoldWaitDuration.ToString() ?? "Not available"; + + public string TapSizeTouch => _platformSettings?.GetTapSize(PointerType.Touch).ToString() ?? "Not available"; + + public string TapSizeMouse => _platformSettings?.GetTapSize(PointerType.Mouse).ToString() ?? "Not available"; + + public string DoubleTapSizeTouch => _platformSettings?.GetDoubleTapSize(PointerType.Touch).ToString() ?? "Not available"; + + public string DoubleTapSizeMouse => _platformSettings?.GetDoubleTapSize(PointerType.Mouse).ToString() ?? "Not available"; + + public string DoubleTapTimeTouch => _platformSettings?.GetDoubleTapTime(PointerType.Touch).ToString() ?? "Not available"; + + public string DoubleTapTimeMouse => _platformSettings?.GetDoubleTapTime(PointerType.Mouse).ToString() ?? "Not available"; +} + diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs index 5f17627ace..afc2673861 100644 --- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Media; @@ -39,6 +40,9 @@ namespace Avalonia.Platform public PlatformHotkeyConfiguration HotkeyConfiguration => AvaloniaLocator.Current.GetRequiredService(); + public virtual string PreferredApplicationLanguage => + CultureInfo.CurrentUICulture.Name; + public virtual PlatformColorValues GetColorValues() { return new PlatformColorValues @@ -48,11 +52,18 @@ namespace Avalonia.Platform } public virtual event EventHandler? ColorValuesChanged; + public virtual event EventHandler? PreferredApplicationLanguageChanged; protected void OnColorValuesChanged(PlatformColorValues colorValues) { Dispatcher.UIThread.Send( _ => ColorValuesChanged?.Invoke(this, colorValues)); } + + protected void OnPreferredApplicationLanguageChanged() + { + Dispatcher.UIThread.Send( + _ => PreferredApplicationLanguageChanged?.Invoke(this, EventArgs.Empty)); + } } } diff --git a/src/Avalonia.Base/Platform/IPlatformSettings.cs b/src/Avalonia.Base/Platform/IPlatformSettings.cs index 46980c6d51..61a8f1870a 100644 --- a/src/Avalonia.Base/Platform/IPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/IPlatformSettings.cs @@ -37,12 +37,17 @@ namespace Avalonia.Platform /// Holding duration between pointer press and when event is fired. /// TimeSpan HoldWaitDuration { get; } - + /// /// Get a configuration for platform-specific hotkeys in an Avalonia application. /// PlatformHotkeyConfiguration HotkeyConfiguration { get; } - + + /// + /// Gets the preferred application language as specified in the operating system settings. + /// + string PreferredApplicationLanguage { get; } + /// /// Gets current system color values including dark mode and accent colors. /// @@ -52,5 +57,10 @@ namespace Avalonia.Platform /// Raises when current system color values are changed. Including changing of a dark mode and accent colors. /// event EventHandler? ColorValuesChanged; + + /// + /// Raises when the preferred application language is changed in the operating system settings. + /// + event EventHandler? PreferredApplicationLanguageChanged; } } diff --git a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs index 2a3cef4334..ea6b064bff 100644 --- a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs +++ b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Browser.Interop; using Avalonia.Platform; @@ -9,20 +10,41 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings private bool _isDarkMode; private bool _isHighContrast; private bool _isInitialized; + private string? _lastLanguage; public override event EventHandler? ColorValuesChanged { add { - EnsureBackend(); + EnsureSettings(); base.ColorValuesChanged += value; } remove => base.ColorValuesChanged -= value; } + public override event EventHandler? PreferredApplicationLanguageChanged + { + add + { + EnsureSettings(); + base.PreferredApplicationLanguageChanged += value; + } + remove => base.PreferredApplicationLanguageChanged -= value; + } + + public override string PreferredApplicationLanguage + { + get + { + EnsureSettings(); + + return _lastLanguage ?? base.PreferredApplicationLanguage; + } + } + public override PlatformColorValues GetColorValues() { - EnsureBackend(); + EnsureSettings(); return base.GetColorValues() with { @@ -31,18 +53,27 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings }; } - public void OnValuesChanged(bool isDarkMode, bool isHighContrast) + public void OnColorValuesChanged(bool isDarkMode, bool isHighContrast) { _isDarkMode = isDarkMode; _isHighContrast = isHighContrast; OnColorValuesChanged(GetColorValues()); } - - private void EnsureBackend() + + public void OnPreferredLanguageChanged(string? language) + { + if (language is not null && _lastLanguage != language) + { + _lastLanguage = language; + OnPreferredApplicationLanguageChanged(); + } + } + + private void EnsureSettings() { if (!_isInitialized) { - // WASM module has async nature of initialization. We can't native code right away during components registration. + // WASM module has async nature of initialization. We can't call platform code right away during components registration. _isInitialized = true; var values = DomHelper.GetDarkMode(BrowserWindowingPlatform.GlobalThis); if (values.Length == 2) @@ -50,6 +81,8 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings _isDarkMode = values[0] > 0; _isHighContrast = values[1] > 0; } + + _lastLanguage = DomHelper.GetNavigatorLanguage(BrowserWindowingPlatform.GlobalThis); } } } diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs index 4b929660e5..22b924b0bd 100644 --- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices.JavaScript; +using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; @@ -31,6 +31,9 @@ internal static partial class DomHelper [JSImport("AvaloniaDOM.getDarkMode", AvaloniaModule.MainModuleName)] public static partial int[] GetDarkMode(JSObject globalThis); + [JSImport("AvaloniaDOM.getNavigatorLanguage", AvaloniaModule.MainModuleName)] + public static partial string? GetNavigatorLanguage(JSObject globalThis); + [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)] public static partial void AddCssClass(JSObject element, string className); @@ -40,7 +43,7 @@ internal static partial class DomHelper [JSExport] public static Task DarkModeChanged(bool isDarkMode, bool isHighContrast) { - (AvaloniaLocator.Current.GetService() as BrowserPlatformSettings)?.OnValuesChanged(isDarkMode, isHighContrast); + (AvaloniaLocator.Current.GetService() as BrowserPlatformSettings)?.OnColorValuesChanged(isDarkMode, isHighContrast); return Task.CompletedTask; } @@ -51,6 +54,13 @@ internal static partial class DomHelper return Task.CompletedTask; } + [JSExport] + public static Task LanguageChanged(string language) + { + (AvaloniaLocator.Current.GetService() as BrowserPlatformSettings)?.OnPreferredLanguageChanged(language); + return Task.CompletedTask; + } + [JSExport] public static Task ScreensChanged() { diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts index 175e51c0da..a09f926c07 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts @@ -136,6 +136,10 @@ export class AvaloniaDOM { }); } + globalThis.addEventListener("languagechange", () => { + JsExports.DomHelper.LanguageChanged(globalThis.navigator.language); + }); + globalThis.document.addEventListener("visibilitychange", () => { JsExports.DomHelper.DocumentVisibilityChanged(globalThis.document.visibilityState); }); @@ -167,4 +171,8 @@ export class AvaloniaDOM { prefersContrastMedia.matches ? 1 : 0 ]; } + + public static getNavigatorLanguage(globalThis: Window): string | null { + return globalThis.navigator?.language ?? null; + } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 1f8b0bdce3..edfd4dd9b5 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1334,6 +1334,9 @@ namespace Avalonia.Win32.Interop [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetModuleHandleW", ExactSpelling = true)] public static extern IntPtr GetModuleHandle(string? lpModuleName); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern int GetUserDefaultLocaleName(char* lpLocaleName, int cchLocaleName); + [DllImport("user32.dll")] public static extern int GetSystemMetrics(SystemMetric smIndex); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 7903a62d8f..1fdf3e59fa 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -183,6 +183,10 @@ namespace Avalonia.Win32 { win32PlatformSettings.OnColorValuesChanged(); } + else if (changedSetting == "intl") // language/locale change + { + win32PlatformSettings.OnLanguageChanged(); + } } if (msg == (uint)WindowsMessage.WM_TIMER) diff --git a/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs index c4b016c1b9..de2a871af7 100644 --- a/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs +++ b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs @@ -13,6 +13,7 @@ internal class Win32PlatformSettings : DefaultPlatformSettings && WinRTApiInformation.IsTypePresent("Windows.UI.ViewManagement.AccessibilitySettings")); private PlatformColorValues? _lastColorValues; + private string? _lastLanguage; public override Size GetTapSize(PointerType type) { @@ -33,6 +34,27 @@ internal class Win32PlatformSettings : DefaultPlatformSettings } public override TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(GetDoubleClickTime()); + + public override string PreferredApplicationLanguage + { + get + { + if (_lastLanguage is null) + { + unsafe + { + const int LOCALE_NAME_MAX_LENGTH = 85; + var buffer = stackalloc char[LOCALE_NAME_MAX_LENGTH]; + var length = GetUserDefaultLocaleName(buffer, LOCALE_NAME_MAX_LENGTH); + _lastLanguage = length > 0 + ? new string(buffer, 0, length - 1) + : base.PreferredApplicationLanguage; + } + } + + return _lastLanguage; + } + } public override PlatformColorValues GetColorValues() { @@ -88,4 +110,16 @@ internal class Win32PlatformSettings : DefaultPlatformSettings OnColorValuesChanged(colorValues); } } + + internal void OnLanguageChanged() + { + var oldLanguage = _lastLanguage; + _lastLanguage = null; + var newLanguage = PreferredApplicationLanguage; + + if (oldLanguage != newLanguage) + { + OnPreferredApplicationLanguageChanged(); + } + } }