using System; using System.Diagnostics.CodeAnalysis; using System.Text; using Avalonia.Input.TextInput; using Avalonia.Threading; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32.Input { /// /// A Windows input method editor based on Windows Input Method Manager (IMM32). /// internal class Imm32InputMethod : ITextInputMethodImpl { public IntPtr Hwnd { get; private set; } private IntPtr _currentHimc; private WindowImpl? _parent; private Imm32CaretManager _caretManager; private ushort _langId; private const int CaretMargin = 1; public ITextInputMethodClient? Client { get; private set; } [MemberNotNullWhen(true, nameof(Client))] public bool IsActive => Client != null; public bool IsComposing { get; set; } public bool ShowCompositionWindow => false; public string? Composition { get; internal set; } public void CreateCaret() { _caretManager.TryCreate(Hwnd); } public void EnableImm() { var himc = ImmGetContext(Hwnd); if(himc == IntPtr.Zero) { himc = ImmCreateContext(); } if(himc != _currentHimc) { if(_currentHimc != IntPtr.Zero) { DisableImm(); } ImmAssociateContext(Hwnd, himc); ImmReleaseContext(Hwnd, himc); _currentHimc = himc; _caretManager.TryCreate(Hwnd); } } public void DisableImm() { _caretManager.TryDestroy(); Reset(); ImmAssociateContext(Hwnd, IntPtr.Zero); _caretManager.TryDestroy(); _currentHimc = IntPtr.Zero; } public void SetLanguageAndWindow(WindowImpl parent, IntPtr hwnd, IntPtr HKL) { Hwnd = hwnd; _parent = parent; _langId = PRIMARYLANGID(LGID(HKL)); _parent = parent; var langId= PRIMARYLANGID(LGID(HKL)); if(langId != _langId) { DisableImm(); } _langId = langId; EnableImm(); } public void ClearLanguageAndWindow() { DisableImm(); Hwnd = IntPtr.Zero; _parent = null; Client = null; _langId = 0; IsComposing = false; } //Dependant on CurrentThread. When Avalonia will support Multiple Dispatchers - //every Dispatcher should have their own InputMethod. public static Imm32InputMethod Current { get; } = new(); public void Reset() { Dispatcher.UIThread.Post(() => { var himc = ImmGetContext(Hwnd); if (IsComposing) { ImmNotifyIME(himc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0); IsComposing = false; } ImmReleaseContext(Hwnd, himc); }); } public void SetClient(ITextInputMethodClient? client) { Client = client; Dispatcher.UIThread.Post(() => { if (IsActive) { EnableImm(); } else { // A renderer process have moved its input focus to a password input // when there is an ongoing composition, e.g. a user has clicked a // mouse button and selected a password input while composing a text. // For this case, we have to complete the ongoing composition and // clean up the resources attached to this object BEFORE DISABLING THE IME. DisableImm(); } }); } public void SetCursorRect(Rect rect) { var focused = GetActiveWindow() == Hwnd; if (!focused) { return; } Dispatcher.UIThread.Post(() => { var himc = ImmGetContext(Hwnd); if (himc == IntPtr.Zero) { return; } MoveImeWindow(rect, himc); ImmReleaseContext(Hwnd, himc); }); } // see: https://chromium.googlesource.com/experimental/chromium/src/+/bf09a5036ccfb77d2277247c66dc55daf41df3fe/chrome/browser/ime_input.cc // see: https://engine.chinmaygarde.com/window__win32_8cc_source.html private void MoveImeWindow(Rect rect, IntPtr himc) { var p1 = rect.TopLeft; var p2 = rect.BottomRight; var s = _parent?.DesktopScaling ?? 1; var (x1, y1, x2, y2) = ((int) (p1.X * s), (int) (p1.Y * s), (int) (p2.X * s), (int) (p2.Y * s)); if (!ShowCompositionWindow && _langId == LANG_ZH) { // Chinese IMEs ignore function calls to ::ImmSetCandidateWindow() // when a user disables TSF (Text Service Framework) and CUAS (Cicero // Unaware Application Support). // On the other hand, when a user enables TSF and CUAS, Chinese IMEs // ignore the position of the current system caret and uses the // parameters given to ::ImmSetCandidateWindow() with its 'dwStyle' // parameter CFS_CANDIDATEPOS. // Therefore, we do not only call ::ImmSetCandidateWindow() but also // set the positions of the temporary system caret. var candidateForm = new CANDIDATEFORM { dwIndex = 0, dwStyle = CFS_CANDIDATEPOS, ptCurrentPos = new POINT {X = x2, Y = y2} }; ImmSetCandidateWindow(himc, ref candidateForm); } _caretManager.TryMove(x2, y2); if (ShowCompositionWindow) { ConfigureCompositionWindow(x1, y1, himc, y2 - y1); // Don't need to set the position of candidate window. return; } if (_langId == LANG_KO) { // Chinese IMEs and Japanese IMEs require the upper-left corner of // the caret to move the position of their candidate windows. // On the other hand, Korean IMEs require the lower-left corner of the // caret to move their candidate windows. y2 += CaretMargin; } // Need to return here since some Chinese IMEs would stuck if set // candidate window position with CFS_EXCLUDE style. if (_langId == LANG_ZH) { return; } // Japanese IMEs and Korean IMEs also use the rectangle given to // ::ImmSetCandidateWindow() with its 'dwStyle' parameter CFS_EXCLUDE // to move their candidate windows when a user disables TSF and CUAS. // Therefore, we also set this parameter here. var excludeRectangle = new CANDIDATEFORM { dwIndex = 0, dwStyle = CFS_EXCLUDE, ptCurrentPos = new POINT {X = x1, Y = y1}, rcArea = new RECT {left = x1, top = y1, right = x2, bottom = y2 + CaretMargin} }; ImmSetCandidateWindow(himc, ref excludeRectangle); } private static void ConfigureCompositionWindow(int x1, int y1, IntPtr himc, int height) { var compForm = new COMPOSITIONFORM { dwStyle = CFS_POINT, ptCurrentPos = new POINT {X = x1, Y = y1}, }; ImmSetCompositionWindow(himc, ref compForm); var logFont = new LOGFONT() { lfHeight = height, lfQuality = 5 //CLEARTYPE_QUALITY }; ImmSetCompositionFont(himc, ref logFont); } public void SetOptions(TextInputOptions options) { // we're skipping this. not usable on windows } public void CompositionChanged() { if (!IsComposing) { return; } if(!IsActive || !Client.SupportsPreedit) { return; } var composition = GetCompositionString(); Composition = composition; Client.SetPreeditText(composition); } private string? GetCompositionString() { var himc = ImmGetContext(Hwnd); return ImmGetCompositionString(himc, GCS.GCS_COMPSTR); } ~Imm32InputMethod() { _caretManager.TryDestroy(); } } }