|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using Windows.Win32.UI.Input.Ime;
namespace System.Windows.Forms;
/// <summary>
/// Control's IME feature.
/// </summary>
public partial class Control
{
/// <summary>
/// Constants starting/ending the WM_CHAR messages to ignore count. See ImeWmCharsToIgnore property.
/// </summary>
private const int ImeCharsToIgnoreDisabled = -1;
private const int ImeCharsToIgnoreEnabled = 0;
/// <summary>
/// The ImeMode value for controls with ImeMode = ImeMode.NoControl. See PropagatingImeMode property.
/// </summary>
private static ImeMode s_propagatingImeMode = ImeMode.Inherit; // Inherit means uninitialized.
/// <summary>
/// This flag prevents resetting ImeMode value of the focused control. See IgnoreWmImeNotify property.
/// </summary>
private static bool s_ignoreWmImeNotify;
/// <summary>
/// The ImeMode in the property store.
/// </summary>
internal ImeMode CachedImeMode
{
get
{
// Get the ImeMode from the property store
if (!Properties.TryGetValue(s_imeModeProperty, out ImeMode cachedImeMode))
{
cachedImeMode = DefaultImeMode;
}
// If inherited, get the mode from this control's parent
if (cachedImeMode == ImeMode.Inherit)
{
Control? parent = ParentInternal;
if (parent is not null)
{
cachedImeMode = parent.CachedImeMode;
}
else
{
cachedImeMode = ImeMode.NoControl;
}
}
return cachedImeMode;
}
set
{
// When the control is in restricted mode (!CanEnableIme) the CachedImeMode should be changed only
// programmatically, calls generated by user interaction should be wrapped with a check for CanEnableIme.
Properties.AddValue(s_imeModeProperty, value);
}
}
/// <summary>
/// Specifies whether the ImeMode property value can be changed to an active value.
/// Added to support Password & ReadOnly (and maybe other) properties, which when set, should force disabling
/// the IME if using one.
/// </summary>
protected virtual bool CanEnableIme
{
get
{
// Note: If overriding this property make sure to add the Debug tracing code and call this method (base.CanEnableIme).
return ImeSupported;
}
}
/// <summary>
/// Gets the current IME context mode. If no IME associated, ImeMode.Inherit is returned.
/// </summary>
internal ImeMode CurrentImeContextMode
{
get
{
if (IsHandleCreated)
{
return ImeContext.GetImeMode(Handle);
}
else
{
// window is not yet created hence no IME associated yet.
return ImeMode.Inherit;
}
}
}
protected virtual ImeMode DefaultImeMode => ImeMode.Inherit;
/// <summary>
/// Flag used to avoid reentrancy during WM_IME_NOTIFY message processing - see WmImeNotify().
/// Also to avoid raising the ImeModeChanged event more than once during the process of changing the ImeMode.
/// </summary>
internal int DisableImeModeChangedCount
{
get
{
int val = Properties.GetValueOrDefault<int>(s_disableImeModeChangedCountProperty);
Debug.Assert(val >= 0, "Counter underflow.");
return val;
}
set => Properties.AddValue(s_disableImeModeChangedCountProperty, value);
}
/// <summary>
/// Flag used to prevent setting ImeMode in focused control when losing focus and hosted in a non-Form shell.
/// See WmImeKillFocus() for more info.
/// </summary>
private static bool IgnoreWmImeNotify
{
get => s_ignoreWmImeNotify;
set => s_ignoreWmImeNotify = value;
}
/// <summary>
/// Specifies a value that determines the IME (Input Method Editor) status of the
/// object when that object is selected.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Localizable(true)]
[AmbientValue(ImeMode.Inherit)]
[SRDescription(nameof(SR.ControlIMEModeDescr))]
public ImeMode ImeMode
{
get
{
ImeMode imeMode = ImeModeBase;
if (imeMode == ImeMode.OnHalf) // This is for compatibility. See QFE#4448.
{
imeMode = ImeMode.On;
}
return imeMode;
}
set
{
ImeModeBase = value;
}
}
/// <summary>
/// Internal version of ImeMode property. This is provided for controls that override CanEnableIme and that
/// return ImeMode.Disable for the ImeMode property when CanEnableIme is false - See TextBoxBase controls.
/// </summary>
protected virtual ImeMode ImeModeBase
{
get => CachedImeMode;
set
{
// valid values are -1 to 0xb
SourceGenerated.EnumValidator.Validate(value);
ImeMode oldImeMode = CachedImeMode;
CachedImeMode = value;
if (oldImeMode == value)
{
return;
}
// Cache current value to determine whether we need to raise the ImeModeChanged.
Control? ctl = null;
if (!DesignMode && ImeModeConversion.InputLanguageTable != ImeModeConversion.UnsupportedTable)
{
// Set the context to the new value if control is focused.
if (Focused)
{
ctl = this;
}
else if (ContainsFocus)
{
ctl = FromChildHandle(PInvoke.GetFocus());
}
if (ctl is not null && ctl.CanEnableIme)
{
// Block ImeModeChanged since we are checking for it below.
DisableImeModeChangedCount++;
try
{
ctl.UpdateImeContextMode();
}
finally
{
DisableImeModeChangedCount--;
}
}
}
VerifyImeModeChanged(oldImeMode, CachedImeMode);
}
}
/// <summary>
/// Determines whether the Control supports IME handling by default.
/// </summary>
private bool ImeSupported => DefaultImeMode != ImeMode.Disable;
[WinCategory("Behavior")]
[SRDescription(nameof(SR.ControlOnImeModeChangedDescr))]
public event EventHandler ImeModeChanged
{
add => Events.AddHandler(s_imeModeChangedEvent, value);
remove => Events.RemoveHandler(s_imeModeChangedEvent, value);
}
/// <summary>
/// Returns the current number of WM_CHAR messages to ignore after processing corresponding WM_IME_CHAR msgs.
/// </summary>
internal int ImeWmCharsToIgnore
{
// The IME sends WM_IME_CHAR messages for each character in the composition string, and then
// after all messages are sent, corresponding WM_CHAR messages are also sent. (in non-unicode
// windows two WM_CHAR messages are sent per char in the IME). We need to keep a counter
// not to process each character twice or more.
get => Properties.GetValueOrDefault<int>(s_imeWmCharsToIgnoreProperty);
set
{
// WM_CHAR is not send after WM_IME_CHAR when the composition has been closed by either, changing the conversion mode or
// dissociating the IME (for instance when loosing focus and conversion is forced to complete).
if (ImeWmCharsToIgnore != ImeCharsToIgnoreDisabled)
{
Properties.AddValue(s_imeWmCharsToIgnoreProperty, value);
}
}
}
/// <summary>
/// Gets the last value CanEnableIme property when it was last checked for ensuring IME context restriction mode.
/// This is used by controls that implement some sort of IME restriction mode (like TextBox on Password/ReadOnly mode).
/// See the VerifyImeRestrictedModeChanged() method.
/// </summary>
private bool LastCanEnableIme
{
get => Properties.GetValueOrDefault(s_lastCanEnableImeProperty, true);
set => Properties.AddOrRemoveValue(s_lastCanEnableImeProperty, value, defaultValue: true);
}
/// <summary>
/// Represents the internal ImeMode value for controls with ImeMode = ImeMode.NoControl. This property is changed
/// only by user interaction and is required to set the IME context appropriately while keeping the ImeMode property
/// unchanged.
/// </summary>
protected static ImeMode PropagatingImeMode
{
get
{
if (s_propagatingImeMode == ImeMode.Inherit)
{
// Initialize the propagating IME mode to the value the IME associated to the focused window currently has,
// this enables propagating the IME mode from/to unmanaged applications hosting winforms controls.
ImeMode imeMode = ImeMode.Inherit;
HWND focusHandle = PInvoke.GetFocus();
if (!focusHandle.IsNull)
{
imeMode = ImeContext.GetImeMode(focusHandle);
// If focused control is disabled we won't be able to get the app ime context mode, try the top window.
// this is the case of a disabled winforms control hosted in a non-Form shell.
if (imeMode == ImeMode.Disable)
{
focusHandle = PInvoke.GetAncestor(focusHandle, GET_ANCESTOR_FLAGS.GA_ROOT);
if (!focusHandle.IsNull)
{
imeMode = ImeContext.GetImeMode(focusHandle);
}
}
}
// If IME is disabled the PropagatingImeMode will not be initialized, see property setter below.
PropagatingImeMode = imeMode;
}
return s_propagatingImeMode;
}
private set
{
if (s_propagatingImeMode == value)
{
return;
}
switch (value)
{
case ImeMode.NoControl:
case ImeMode.Disable:
// Cannot set propagating ImeMode to one of these values.
return;
default:
s_propagatingImeMode = value;
break;
}
}
}
/// <summary>
/// Sets the IME context to the appropriate ImeMode according to the control's ImeMode state.
/// This method is commonly used when attaching the IME to the control's window.
/// </summary>
internal void UpdateImeContextMode()
{
ImeMode[] inputLanguageTable = ImeModeConversion.InputLanguageTable;
if (!DesignMode && (inputLanguageTable != ImeModeConversion.UnsupportedTable) && Focused)
{
// Note: CHN IME won't send WM_IME_NOTIFY msg when getting associated, setting the IME context mode
// forces the message to be sent as a side effect.
// If the value is not supported by the Ime, it will be mapped to a corresponding one, we need
// to update the cached ImeMode to the actual value.
ImeMode newImeContextMode = ImeMode.Disable;
ImeMode currentImeMode = CachedImeMode;
if (ImeSupported && CanEnableIme)
{
newImeContextMode = currentImeMode == ImeMode.NoControl ? PropagatingImeMode : currentImeMode;
}
// If PropagatingImeMode has not been initialized it will return ImeMode.Inherit above, need to check newImeContextMode for this.
if (CurrentImeContextMode != newImeContextMode && newImeContextMode != ImeMode.Inherit)
{
// If the context changes the window will receive one or more WM_IME_NOTIFY messages and as part of its
// processing it will raise the ImeModeChanged event if needed. We need to prevent the event from been
// raised here from here.
DisableImeModeChangedCount++;
// Setting IME status to Disable will first close the IME and then disable it. For CHN IME, the first action will
// update the PropagatingImeMode to ImeMode.Close which is incorrect. We need to save the PropagatingImeMode in
// this case and restore it after the context has been changed.
// Also this call here is very important since it will initialize the PropagatingImeMode if not already initialized
// before setting the IME context to the control's ImeMode value which could be different from the propagating value.
ImeMode savedPropagatingImeMode = PropagatingImeMode;
try
{
ImeContext.SetImeStatus(newImeContextMode, Handle);
}
finally
{
DisableImeModeChangedCount--;
if (newImeContextMode == ImeMode.Disable && inputLanguageTable == ImeModeConversion.ChineseTable)
{
// Restore saved propagating mode.
PropagatingImeMode = savedPropagatingImeMode;
}
}
// Get mapped value from the context.
if (currentImeMode == ImeMode.NoControl)
{
if (CanEnableIme)
{
PropagatingImeMode = CurrentImeContextMode;
}
}
else
{
if (CanEnableIme)
{
CachedImeMode = CurrentImeContextMode;
}
// Need to raise the ImeModeChanged event?
VerifyImeModeChanged(newImeContextMode, CachedImeMode);
}
}
}
}
/// <summary>
/// Checks if specified ImeMode values are different and raise the event if true.
/// </summary>
private void VerifyImeModeChanged(ImeMode oldMode, ImeMode newMode)
{
if (ImeSupported && (DisableImeModeChangedCount == 0) && (newMode != ImeMode.NoControl) && oldMode != newMode)
{
OnImeModeChanged(EventArgs.Empty);
}
}
/// <summary>
/// Verifies whether the IME context mode is correct based on the control's Ime restriction mode (CanEnableIme)
/// and updates the IME context if needed.
/// </summary>
internal void VerifyImeRestrictedModeChanged()
{
bool currentCanEnableIme = CanEnableIme;
if (LastCanEnableIme == currentCanEnableIme)
{
return;
}
if (Focused)
{
// Disable ImeModeChanged from the following call since we'll raise it here if needed.
DisableImeModeChangedCount++;
try
{
UpdateImeContextMode();
}
finally
{
DisableImeModeChangedCount--;
}
}
// Assume for a moment the control is getting restricted;
ImeMode oldImeMode = CachedImeMode;
ImeMode newImeMode = ImeMode.Disable;
if (currentCanEnableIme)
{
// Control is actually getting unrestricted, swap values.
newImeMode = oldImeMode;
oldImeMode = ImeMode.Disable;
}
// Do we need to raise the ImeModeChanged event?
VerifyImeModeChanged(oldImeMode, newImeMode);
// Finally update the saved CanEnableIme value.
LastCanEnableIme = currentCanEnableIme;
}
/// <summary>
/// Update internal ImeMode properties (PropagatingImeMode/CachedImeMode) with actual IME context mode if needed.
/// This method can be used with a child control when the IME mode is more relevant to it than to the control itself,
/// for instance ComboBox and its native ListBox/Edit controls.
/// </summary>
internal void OnImeContextStatusChanged(IntPtr handle)
{
Debug.Assert(ImeSupported, "WARNING: Attempting to update ImeMode properties on IME-Unaware control!");
Debug.Assert(!DesignMode, "Shouldn't be updating cached ime mode at design-time");
ImeMode fromContext = ImeContext.GetImeMode(handle);
if (fromContext != ImeMode.Inherit)
{
ImeMode oldImeMode = CachedImeMode;
if (CanEnableIme)
{
// Cache or Propagating ImeMode should not be updated by interaction when the control is in restricted mode.
if (oldImeMode != ImeMode.NoControl)
{
CachedImeMode = fromContext; // This could end up in the same value due to ImeMode language mapping.
// ImeMode may be changing by user interaction.
VerifyImeModeChanged(oldImeMode, CachedImeMode);
}
else
{
PropagatingImeMode = fromContext;
}
}
}
}
/// <summary>
/// Raises the <see cref="OnImeModeChanged"/> event.
/// </summary>
protected virtual void OnImeModeChanged(EventArgs e)
{
Debug.Assert(ImeSupported, "ImeModeChanged should not be raised on an Ime-Unaware control.");
((EventHandler?)Events[s_imeModeChangedEvent])?.Invoke(this, e);
}
/// <summary>
/// Resets the Ime mode.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public void ResetImeMode() => ImeMode = DefaultImeMode;
/// <summary>
/// Returns true if the ImeMode should be persisted in code gen.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal virtual bool ShouldSerializeImeMode()
{
// This method is for designer support. If the ImeMode has not been changed or it is the same as the
// default value it should not be serialized.
return Properties.TryGetValue(s_imeModeProperty, out ImeMode imeMode) && imeMode != DefaultImeMode;
}
/// <summary>
/// Handles the WM_INPUTLANGCHANGE message
/// </summary>
private void WmInputLangChange(ref Message m)
{
// Make sure the IME context is associated with the correct (mapped) mode.
UpdateImeContextMode();
// If detaching IME (setting to English) reset propagating IME mode so when reattaching the IME is set to direct input again.
if (ImeModeConversion.InputLanguageTable == ImeModeConversion.UnsupportedTable)
{
PropagatingImeMode = ImeMode.Off;
}
if (FindForm() is Form form)
{
InputLanguageChangedEventArgs e = InputLanguage.CreateInputLanguageChangedEventArgs(m);
form.PerformOnInputLanguageChanged(e);
}
DefWndProc(ref m);
}
/// <summary>
/// Handles the WM_INPUTLANGCHANGEREQUEST message
/// </summary>
private void WmInputLangChangeRequest(ref Message m)
{
InputLanguageChangingEventArgs e = InputLanguage.CreateInputLanguageChangingEventArgs(m);
if (FindForm() is Form form)
{
form.PerformOnInputLanguageChanging(e);
}
if (!e.Cancel)
{
DefWndProc(ref m);
}
else
{
m.ResultInternal = (LRESULT)0;
}
}
/// <summary>
/// Handles the WM_IME_CHAR message
/// </summary>
private void WmImeChar(ref Message m)
{
if (ProcessKeyEventArgs(ref m))
{
return;
}
DefWndProc(ref m);
}
/// <summary>
/// Handles the WM_IME_ENDCOMPOSITION message
/// </summary>
private void WmImeEndComposition(ref Message m)
{
ImeWmCharsToIgnore = ImeCharsToIgnoreDisabled;
DefWndProc(ref m);
}
/// <summary>
/// Handles the WM_IME_NOTIFY message
/// </summary>
private void WmImeNotify(ref Message m)
{
if (ImeSupported && ImeModeConversion.InputLanguageTable != ImeModeConversion.UnsupportedTable && !IgnoreWmImeNotify)
{
int wparam = (int)m.WParamInternal;
// The WM_IME_NOTIFY message is not consistent across the different IMEs, particularly the notification type
// we care about (IMN_SETCONVERSIONMODE & IMN_SETOPENSTATUS).
// The IMN_SETOPENSTATUS command is sent when the open status of the input context is updated.
// The IMN_SETCONVERSIONMODE command is sent when the conversion mode of the input context is updated.
// - The Korean IME sents both msg notifications when changing the conversion mode (From/To Hangul/Alpha).
// - The Chinese IMEs sends the IMN_SETCONVERSIONMODE when changing mode (On/Close, Full Shape/Half Shape)
// and IMN_SETOPENSTATUS when getting disabled/enabled or closing/opening as well, but it does not send any
// WM_IME_NOTIFY when associating an IME to the app for the first time; setting the IME mode to direct input
// during WM_INPUTLANGCHANGED forces the IMN_SETOPENSTATUS message to be sent.
// - The Japanese IME sends IMN_SETCONVERSIONMODE when changing from Off to one of the active modes (Katakana..)
// and IMN_SETOPENSTATUS when changing between the active modes or when enabling/disabling the IME.
// In any case we update the cache.
// Warning:
// Attempting to change the IME mode from here will cause reentrancy - WM_IME_NOTIFY is resent.
// We guard against reentrancy since the ImeModeChanged event can be raised and any changes from the handler could
// lead to another WM_IME_NOTIFY loop.
if (wparam is ((int)PInvoke.IMN_SETCONVERSIONMODE) or ((int)PInvoke.IMN_SETOPENSTATUS))
{
// Synchronize internal properties with the IME context mode.
OnImeContextStatusChanged(Handle);
}
}
DefWndProc(ref m);
}
/// <summary>
/// Handles the WM_SETFOCUS message for IME related stuff.
/// </summary>
internal void WmImeSetFocus()
{
if (ImeModeConversion.InputLanguageTable != ImeModeConversion.UnsupportedTable)
{
// Make sure the IME context is set to the correct value.
// Consider - Perf improvement: ContainerControl controls should update the IME context only when they don't contain
// a focusable control since it will be updated by that control.
UpdateImeContextMode();
}
}
/// <summary>
/// Handles the WM_IME_STARTCOMPOSITION message
/// </summary>
private void WmImeStartComposition(ref Message m)
{
// Need to call the property store directly because the WmImeCharsToIgnore property is locked when ImeCharsToIgnoreDisabled.
Properties.AddValue(s_imeWmCharsToIgnoreProperty, ImeCharsToIgnoreEnabled);
DefWndProc(ref m);
}
/// <summary>
/// Handles the WM_KILLFOCUS message
/// </summary>
private void WmImeKillFocus()
{
Control topMostWinformsParent = TopMostParent;
Form? appForm = topMostWinformsParent as Form;
if ((appForm is null || appForm.Modal) && !topMostWinformsParent.ContainsFocus)
{
// This means the winforms component container is not a WinForms host and it is no longer focused.
// Or it is not the main app host.
// We need to reset the PropagatingImeMode to force reinitialization when the winforms component gets focused again;
// this enables inheriting the propagating mode from an unmanaged application hosting a winforms component.
// But before leaving the winforms container we need to set the IME to the propagating IME mode since the focused control
// may not support IME which would leave the IME disabled.
// See the PropagatingImeMode property
// Note: We need to check the static field here directly to avoid initialization of the property.
if (s_propagatingImeMode != ImeMode.Inherit)
{
// Setting the ime context of the top window will generate a WM_IME_NOTIFY on the focused control which will
// update its ImeMode, we need to prevent this temporarily.
IgnoreWmImeNotify = true;
try
{
ImeContext.SetImeStatus(PropagatingImeMode, topMostWinformsParent.Handle);
PropagatingImeMode = ImeMode.Inherit;
}
finally
{
IgnoreWmImeNotify = false;
}
}
}
}
}
/// <summary>
/// Represents the native IME context.
/// </summary>
public static class ImeContext
{
/// <summary>
/// The IME context handle obtained when first associating an IME.
/// </summary>
private static HIMC s_originalImeContext;
/// <summary>
/// Disable the IME
/// </summary>
public static void Disable(IntPtr handle)
{
if (ImeModeConversion.InputLanguageTable == ImeModeConversion.UnsupportedTable)
{
return;
}
// Close the IME if necessary
if (IsOpen(handle))
{
SetOpenStatus(false, handle);
}
// Disable the IME by disassociating the context from the window.
HIMC oldContext = PInvoke.ImmAssociateContext((HWND)handle, (HIMC)IntPtr.Zero);
if (oldContext != IntPtr.Zero)
{
s_originalImeContext = oldContext;
}
}
/// <summary>
/// Enable the IME
/// </summary>
public static void Enable(IntPtr handle)
{
if (ImeModeConversion.InputLanguageTable != ImeModeConversion.UnsupportedTable)
{
HIMC inputContext = PInvoke.ImmGetContext((HWND)handle);
// Enable IME by associating the IME context to the window.
if (inputContext == IntPtr.Zero)
{
if (s_originalImeContext == IntPtr.Zero)
{
inputContext = PInvoke.ImmCreateContext();
if (inputContext != IntPtr.Zero)
{
PInvoke.ImmAssociateContext((HWND)handle, inputContext);
}
}
else
{
PInvoke.ImmAssociateContext((HWND)handle, s_originalImeContext);
}
}
else
{
PInvoke.ImmReleaseContext((HWND)handle, inputContext);
}
// Make sure the IME is opened.
if (!IsOpen(handle))
{
SetOpenStatus(true, handle);
}
}
}
/// <summary>
/// Gets the ImeMode that corresponds to ImeMode.Disable based on the current input language ImeMode table.
/// </summary>
public static unsafe ImeMode GetImeMode(IntPtr handle)
{
HIMC inputContext = (HIMC)IntPtr.Zero;
ImeMode retval = ImeMode.NoControl;
// Get the right table for the current keyboard layout
ImeMode[] countryTable = ImeModeConversion.InputLanguageTable;
if (countryTable == ImeModeConversion.UnsupportedTable)
{
// No IME associated with current culture.
retval = ImeMode.Inherit;
goto cleanup;
}
inputContext = PInvoke.ImmGetContext((HWND)handle);
if (inputContext == IntPtr.Zero)
{
// No IME context attached - The Ime has been disabled.
retval = ImeMode.Disable;
goto cleanup;
}
if (!IsOpen(handle))
{
// There's an IME associated with the window but is closed - the input is taken from the keyboard as is (English).
retval = countryTable[ImeModeConversion.ImeClosed];
goto cleanup;
}
// Determine the IME mode from the conversion status
IME_CONVERSION_MODE conversion;
IME_SENTENCE_MODE sentence;
PInvoke.ImmGetConversionStatus(inputContext, &conversion, &sentence);
Debug.Assert(countryTable is not null, "countryTable is null");
if ((conversion & IME_CONVERSION_MODE.IME_CMODE_NATIVE) != 0)
{
if ((conversion & IME_CONVERSION_MODE.IME_CMODE_KATAKANA) != 0)
{
retval = ((conversion & IME_CONVERSION_MODE.IME_CMODE_FULLSHAPE) != 0)
? countryTable[ImeModeConversion.ImeNativeFullKatakana]
: countryTable[ImeModeConversion.ImeNativeHalfKatakana];
goto cleanup;
}
else
{
retval = ((conversion & IME_CONVERSION_MODE.IME_CMODE_FULLSHAPE) != 0)
? countryTable[ImeModeConversion.ImeNativeFullHiragana]
: countryTable[ImeModeConversion.ImeNativeHalfHiragana];
goto cleanup;
}
}
else
{
retval = ((conversion & IME_CONVERSION_MODE.IME_CMODE_FULLSHAPE) != 0)
? countryTable[ImeModeConversion.ImeAlphaFull]
: countryTable[ImeModeConversion.ImeAlphaHalf];
}
cleanup:
if (inputContext != IntPtr.Zero)
{
PInvoke.ImmReleaseContext((HWND)handle, inputContext);
}
return retval;
}
/// <summary>
/// Returns true if the IME is currently open
/// </summary>
public static bool IsOpen(IntPtr handle)
{
HIMC inputContext = PInvoke.ImmGetContext((HWND)handle);
bool retval = false;
if (inputContext != IntPtr.Zero)
{
retval = PInvoke.ImmGetOpenStatus(inputContext);
PInvoke.ImmReleaseContext((HWND)handle, inputContext);
}
return retval;
}
/// <summary>
/// Sets the actual IME context value.
/// </summary>
public static unsafe void SetImeStatus(ImeMode imeMode, IntPtr handle)
{
Debug.Assert(imeMode != ImeMode.Inherit, "ImeMode.Inherit is an invalid argument to ImeContext.SetImeStatus");
if (imeMode is ImeMode.Inherit or ImeMode.NoControl)
{
// No action required
return;
}
ImeMode[] inputLanguageTable = ImeModeConversion.InputLanguageTable;
if (inputLanguageTable == ImeModeConversion.UnsupportedTable)
{
// We only support Japanese, Korean and Chinese IME.
return;
}
if (imeMode == ImeMode.Disable)
{
Disable(handle);
}
else
{
// This will make sure the IME is opened.
Enable(handle);
}
switch (imeMode)
{
case ImeMode.NoControl:
case ImeMode.Disable:
break; // No action required
case ImeMode.On:
// IME active mode (CHN = On, JPN = Hiragana, KOR = Hangul).
// Setting ImeMode to Hiragana (or any other active value) will force the IME to get to an active value
// independent on the language.
imeMode = ImeMode.Hiragana;
goto default;
case ImeMode.Off:
// IME direct input (CHN = Off, JPN = Off, KOR = Alpha).
if (inputLanguageTable == ImeModeConversion.JapaneseTable)
{
// Japanese IME interprets Close as Off.
goto case ImeMode.Close;
}
// CHN: to differentiate between Close and Off we set the ImeMode to Alpha.
imeMode = ImeMode.Alpha;
goto default;
case ImeMode.Close:
if (inputLanguageTable == ImeModeConversion.KoreanTable)
{
// Korean IME has no idea what Close means.
imeMode = ImeMode.Alpha;
goto default;
}
SetOpenStatus(false, handle);
break;
default:
if (ImeModeConversion.ImeModeConversionBits.TryGetValue(imeMode, out ImeModeConversion conversionEntry))
{
// Update the conversion status
HIMC inputContext = PInvoke.ImmGetContext((HWND)handle);
IME_CONVERSION_MODE conversion;
IME_SENTENCE_MODE sentence;
PInvoke.ImmGetConversionStatus(inputContext, &conversion, &sentence);
conversion |= conversionEntry.SetBits;
conversion &= ~conversionEntry.ClearBits;
PInvoke.ImmSetConversionStatus(inputContext, conversion, sentence);
PInvoke.ImmReleaseContext((HWND)handle, inputContext);
}
break;
}
}
/// <summary>
/// Opens or closes the IME context.
/// </summary>
public static void SetOpenStatus(bool open, IntPtr handle)
{
if (ImeModeConversion.InputLanguageTable == ImeModeConversion.UnsupportedTable)
{
return;
}
HIMC inputContext = PInvoke.ImmGetContext((HWND)handle);
if (inputContext != IntPtr.Zero)
{
bool succeeded = PInvoke.ImmSetOpenStatus(inputContext, open);
Debug.Assert(succeeded, "Could not set the IME open status.");
if (succeeded)
{
succeeded = PInvoke.ImmReleaseContext((HWND)handle, inputContext);
Debug.Assert(succeeded, "Could not release IME context.");
}
}
}
}
/// <summary>
/// Helper class that provides information about IME conversion mode. Conversion mode refers to how IME interprets input like
/// ALPHANUMERIC or HIRAGANA and depending on its value the IME enables/disables the IME conversion window appropriately.
/// </summary>
public readonly struct ImeModeConversion
{
private static volatile Dictionary<ImeMode, ImeModeConversion>? s_imeModeConversionBits;
internal IME_CONVERSION_MODE SetBits { get; init; }
internal IME_CONVERSION_MODE ClearBits { get; init; }
// Tables of conversions from IME context bits to IME mode
//
// internal const int ImeNotAvailable = 0;
internal const int ImeDisabled = 1;
internal const int ImeDirectInput = 2;
internal const int ImeClosed = 3;
internal const int ImeNativeInput = 4;
internal const int ImeNativeFullHiragana = 4; // Index of Native Input Mode.
internal const int ImeNativeHalfHiragana = 5;
internal const int ImeNativeFullKatakana = 6;
internal const int ImeNativeHalfKatakana = 7;
internal const int ImeAlphaFull = 8;
internal const int ImeAlphaHalf = 9;
/// <summary>
/// Supported input language ImeMode tables.
/// WARNING: Do not try to map 'active' IME modes from one table to another since they can have a different
/// meaning depending on the language; for instance ImeMode.Off means 'disable' or 'alpha' to Chinese
/// but to Japanese it is 'alpha' and to Korean it has no meaning.
/// </summary>
private static readonly ImeMode[] s_japaneseTable =
[
ImeMode.Inherit,
ImeMode.Disable,
ImeMode.Off,
ImeMode.Off,
ImeMode.Hiragana,
ImeMode.Hiragana,
ImeMode.Katakana,
ImeMode.KatakanaHalf,
ImeMode.AlphaFull,
ImeMode.Alpha
];
private static readonly ImeMode[] s_koreanTable =
[
ImeMode.Inherit,
ImeMode.Disable,
ImeMode.Alpha,
ImeMode.Alpha,
ImeMode.HangulFull,
ImeMode.Hangul,
ImeMode.HangulFull,
ImeMode.Hangul,
ImeMode.AlphaFull,
ImeMode.Alpha
];
private static readonly ImeMode[] s_chineseTable =
[
ImeMode.Inherit,
ImeMode.Disable,
ImeMode.Off,
ImeMode.Close,
ImeMode.On,
ImeMode.OnHalf,
ImeMode.On,
ImeMode.OnHalf,
ImeMode.Off,
ImeMode.Off
];
private static readonly ImeMode[] s_unsupportedTable = [];
internal static ImeMode[] ChineseTable => s_chineseTable;
internal static ImeMode[] JapaneseTable => s_japaneseTable;
internal static ImeMode[] KoreanTable => s_koreanTable;
internal static ImeMode[] UnsupportedTable => s_unsupportedTable;
/// <summary>
/// Gets the ImeMode table of the current input language.
/// Although this property is per-thread based we cannot cache it and share it among controls running in the same thread
/// for two main reasons: we still have some controls that don't handle IME properly (TabControl, ComboBox, TreeView...)
/// and would render it invalid and since the IME API is not public third party controls would not have a way to update
/// the cached value.
/// </summary>
internal static ImeMode[] InputLanguageTable
{
get
{
InputLanguage inputLanguage = InputLanguage.CurrentInputLanguage;
int lcid = (int)(inputLanguage.Handle & (long)0xFFFF);
return lcid switch
{
0x0404 or 0x0804 or 0x0c04 or 0x1004 or 0x1404 => s_chineseTable,
// Korean (Johab)
0x0412 or 0x0812 => s_koreanTable,
// Japanese
0x0411 => s_japaneseTable,
_ => s_unsupportedTable,
};
}
}
/// <summary>
/// Dictionary of ImeMode and corresponding conversion flags.
/// </summary>
public static Dictionary<ImeMode, ImeModeConversion> ImeModeConversionBits
{
get => s_imeModeConversionBits ??= new(7)
{
// Hiragana, On
{
ImeMode.Hiragana,
new()
{
SetBits = IME_CONVERSION_MODE.IME_CMODE_FULLSHAPE | IME_CONVERSION_MODE.IME_CMODE_NATIVE,
ClearBits = IME_CONVERSION_MODE.IME_CMODE_KATAKANA
}
},
// Katakana
{
ImeMode.Katakana,
new()
{
SetBits = IME_CONVERSION_MODE.IME_CMODE_FULLSHAPE | IME_CONVERSION_MODE.IME_CMODE_KATAKANA | IME_CONVERSION_MODE.IME_CMODE_NATIVE,
ClearBits = 0
}
},
// KatakanaHalf
{
ImeMode.KatakanaHalf,
new()
{
SetBits = IME_CONVERSION_MODE.IME_CMODE_KATAKANA | IME_CONVERSION_MODE.IME_CMODE_NATIVE,
ClearBits = IME_CONVERSION_MODE.IME_CMODE_FULLSHAPE
}
},
// AlphaFull
{
ImeMode.AlphaFull,
new()
{
SetBits = IME_CONVERSION_MODE.IME_CMODE_FULLSHAPE,
ClearBits = IME_CONVERSION_MODE.IME_CMODE_KATAKANA | IME_CONVERSION_MODE.IME_CMODE_NATIVE
}
},
// Alpha
{
ImeMode.Alpha,
new()
{
SetBits = 0,
ClearBits = IME_CONVERSION_MODE.IME_CMODE_FULLSHAPE | IME_CONVERSION_MODE.IME_CMODE_KATAKANA | IME_CONVERSION_MODE.IME_CMODE_NATIVE
}
},
// HangulFull
{
ImeMode.HangulFull,
new()
{
SetBits = IME_CONVERSION_MODE.IME_CMODE_FULLSHAPE | IME_CONVERSION_MODE.IME_CMODE_NATIVE,
ClearBits = 0
}
},
// Hangul
{
ImeMode.Hangul,
new()
{
SetBits = IME_CONVERSION_MODE.IME_CMODE_NATIVE,
ClearBits = IME_CONVERSION_MODE.IME_CMODE_FULLSHAPE
}
},
// OnHalf
{
ImeMode.OnHalf,
new()
{
SetBits = IME_CONVERSION_MODE.IME_CMODE_NATIVE,
ClearBits = IME_CONVERSION_MODE.IME_CMODE_KATAKANA | IME_CONVERSION_MODE.IME_CMODE_FULLSHAPE
}
}
};
}
public static bool IsCurrentConversionTableSupported => InputLanguageTable != UnsupportedTable;
}
|