File: System\Windows\Forms\Control.Ime.cs
Web Access
Project: src\src\System.Windows.Forms\src\System.Windows.Forms.csproj (System.Windows.Forms)
// 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 &amp; 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;
 
        // 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;
}