csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
8.9 KiB
303 lines
8.9 KiB
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
|
|
{
|
|
/// <summary>
|
|
/// A Windows input method editor based on Windows Input Method Manager (IMM32).
|
|
/// </summary>
|
|
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(string? composition)
|
|
{
|
|
Composition = composition;
|
|
|
|
if (!IsActive || !Client.SupportsPreedit)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Client.SetPreeditText(composition);
|
|
}
|
|
|
|
public string? GetCompositionString(GCS flag)
|
|
{
|
|
if (!IsComposing)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var himc = ImmGetContext(Hwnd);
|
|
|
|
return ImmGetCompositionString(himc, flag);
|
|
}
|
|
|
|
~Imm32InputMethod()
|
|
{
|
|
_caretManager.TryDestroy();
|
|
}
|
|
}
|
|
}
|
|
|