|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using Microsoft.Win32;
using Windows.Win32.UI.Input.KeyboardAndMouse;
namespace System.Windows.Forms;
/// <summary>
/// Provides methods and fields to manage the input language.
/// </summary>
public sealed class InputLanguage
{
/// <summary>
/// The HKL handle.
/// </summary>
private readonly nint _handle;
internal InputLanguage(IntPtr handle)
{
_handle = handle;
}
/// <summary>
/// Returns the culture of the current input language.
/// </summary>
public CultureInfo Culture => new(LanguageTag);
/// <summary>
/// Gets or sets the input language for the current thread.
/// </summary>
[AllowNull]
public static InputLanguage CurrentInputLanguage
{
get
{
Application.OleRequired();
return new InputLanguage(PInvoke.GetKeyboardLayout(0));
}
set
{
// OleInitialize needs to be called before we can call ActivateKeyboardLayout.
Application.OleRequired();
value ??= DefaultInputLanguage;
HKL handleOld = PInvoke.ActivateKeyboardLayout(new HKL(value.Handle), 0);
if (handleOld == default)
{
throw new ArgumentException(SR.ErrorBadInputLanguage, nameof(value));
}
}
}
/// <summary>
/// Returns the default input language for the system.
/// </summary>
public static InputLanguage DefaultInputLanguage
{
get
{
nint handle = 0;
PInvokeCore.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETDEFAULTINPUTLANG, ref handle);
return new InputLanguage(handle);
}
}
/// <summary>
/// Returns the handle for the input language.
/// </summary>
public IntPtr Handle => _handle;
/// <summary>
/// Returns a list of all installed input languages.
/// </summary>
public static unsafe InputLanguageCollection InstalledInputLanguages
{
get
{
int size = PInvoke.GetKeyboardLayoutList(0, null);
var handles = new HKL[size];
fixed (HKL* h = handles)
{
PInvoke.GetKeyboardLayoutList(size, h);
}
InputLanguage[] ils = new InputLanguage[size];
for (int i = 0; i < size; i++)
{
ils[i] = new InputLanguage(handles[i]);
}
return new InputLanguageCollection(ils);
}
}
private const string KeyboardLayoutsRegistryPath = @"SYSTEM\CurrentControlSet\Control\Keyboard Layouts";
/// <summary>
/// Returns the name of the current keyboard layout as it appears in the Windows
/// Regional Settings on the computer.
/// </summary>
public string LayoutName
{
get
{
// https://learn.microsoft.com/windows/win32/intl/using-registry-string-redirection#create-resources-for-keyboard-layout-strings
using RegistryKey? key = Registry.LocalMachine.OpenSubKey($@"{KeyboardLayoutsRegistryPath}\{LayoutId}");
return key.GetMUIString("Layout Display Name", "Layout Text") ?? SR.UnknownInputLanguageLayout;
}
}
/// <summary>
/// Returns the
/// <see href="https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-getkeyboardlayoutnamew">
/// keyboard layout identifier
/// </see>
/// of the current input language.
/// </summary>
/// <seealso href="https://learn.microsoft.com/windows-hardware/manufacture/desktop/windows-language-pack-default-values">
/// Keyboard identifiers and input method editors for Windows
/// </seealso>
internal string LayoutId
{
get
{
// There is no good way to do this in Windows. GetKeyboardLayoutName does what we want, but only for the
// current input language; setting and resetting the current input language would generate spurious
// InputLanguageChanged events. Try to extract needed information manually.
// High word of HKL contains a device handle to the physical layout of the keyboard but exact format of this
// handle is not documented. For older keyboard layouts device handle seems contains keyboard layout
// identifier.
int device = PARAM.HIWORD(_handle);
// But for newer keyboard layouts device handle contains special layout id if its high nibble is 0xF. This
// id may be used to search for keyboard layout under registry.
//
// NOTE: this logic may break in future versions of Windows since it is not documented.
if ((device & 0xF000) == 0xF000)
{
// Extract special layout id from the device handle
int layoutId = device & 0x0FFF;
using RegistryKey? key = Registry.LocalMachine.OpenSubKey(KeyboardLayoutsRegistryPath);
if (key is not null)
{
// Match keyboard layout by layout id
foreach (string subKeyName in key.GetSubKeyNames())
{
using RegistryKey? subKey = key.OpenSubKey(subKeyName);
if (subKey is not null
&& subKey.GetValue("Layout Id") is string subKeyLayoutId
&& Convert.ToInt32(subKeyLayoutId, 16) == layoutId)
{
Debug.Assert(subKeyName.Length == 8, $"unexpected key length in registry: {subKey.Name}");
return subKeyName.ToUpperInvariant();
}
}
}
}
else
{
// Use input language only if keyboard layout language is not available. This is crucial in cases when
// keyboard is installed more than once or under different languages. For example when French keyboard
// is installed under US input language we need to return French keyboard identifier.
if (device == 0)
{
// According to the GetKeyboardLayout API function docs low word of HKL contains input language.
device = PARAM.LOWORD(_handle);
}
}
return device.ToString("X8");
}
}
private const string UserProfileRegistryPath = @"Control Panel\International\User Profile";
/// <summary>
/// Returns the
/// <see href="https://learn.microsoft.com/globalization/locale/standard-locale-names">
/// BCP 47 language tag
/// </see>
/// of the current input language.
/// </summary>
private string LanguageTag
{
get
{
// According to the GetKeyboardLayout API function docs low word of HKL contains input language identifier.
int langId = PARAM.LOWORD(_handle);
// We need to convert the language identifier to a language tag, because they are deprecated and may have a
// transient value.
// https://learn.microsoft.com/globalization/locale/other-locale-names#lcid
// https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks
//
// It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect
// language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID"
// instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet).
//
// Try to extract proper language tag from registry as a workaround approved by a Windows team.
// https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949
//
// NOTE: this logic may break in future versions of Windows since it is not documented.
if (langId is (int)PInvoke.LOCALE_TRANSIENT_KEYBOARD1
or (int)PInvoke.LOCALE_TRANSIENT_KEYBOARD2
or (int)PInvoke.LOCALE_TRANSIENT_KEYBOARD3
or (int)PInvoke.LOCALE_TRANSIENT_KEYBOARD4)
{
using RegistryKey? key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath);
if (key is not null && key.GetValue("Languages") is string[] languages)
{
foreach (string language in languages)
{
using RegistryKey? subKey = key.OpenSubKey(language);
if (subKey is not null
&& subKey.GetValue("TransientLangId") is int transientLangId
&& transientLangId == langId)
{
return language;
}
}
}
}
return CultureInfo.GetCultureInfo(langId).Name;
}
}
/// <summary>
/// Creates an InputLanguageChangedEventArgs given a windows message.
/// </summary>
internal static InputLanguageChangedEventArgs CreateInputLanguageChangedEventArgs(Message m)
{
return new InputLanguageChangedEventArgs(new InputLanguage(m.LParamInternal), (byte)(nint)m.WParamInternal);
}
/// <summary>
/// Creates an InputLanguageChangingEventArgs given a windows message.
/// </summary>
internal static InputLanguageChangingEventArgs CreateInputLanguageChangingEventArgs(Message m)
{
InputLanguage inputLanguage = new(m.LParamInternal);
// NOTE: by default we should allow any locale switch
bool localeSupportedBySystem = m.WParamInternal != 0u;
return new InputLanguageChangingEventArgs(inputLanguage, localeSupportedBySystem);
}
/// <summary>
/// Specifies whether two input languages are equal.
/// </summary>
public override bool Equals(object? value)
=> value is InputLanguage other && _handle == other._handle;
/// <summary>
/// Returns the input language associated with the specified culture.
/// </summary>
public static InputLanguage? FromCulture(CultureInfo culture)
{
ArgumentNullException.ThrowIfNull(culture);
foreach (InputLanguage? lang in InstalledInputLanguages)
{
if (culture.Equals(lang?.Culture))
{
return lang;
}
}
return null;
}
/// <summary>
/// Hash code for this input language.
/// </summary>
public override int GetHashCode() => (int)_handle;
}
|