Browse Source
* win32 ime wip * ime window starts tracking the cursor, but coords are wrong * fix win32 ime cursor coord * win32-ime lang-specific behaviors * track language id in WindowImpl * lowercase dllimport * create initial ime on window creation * InputMethodManager: connect to client even if im is absent at the moment * proposal: IKeyboardDevice.NotifyInputMethodUpdated * finalizing * ime: allow client to request active state change * remove backward incompatible ActiveState. * InputMethodManager: NotifyInputMethodUpdated: filter the window of current focused element * [IME] [Windows] ability to enable/disable IME for any InputElement * [IME] [Windows] Refactor Imm32InputMethod - create a single one for dispatcher. Also change a method of enabling/disabling IME to work like in WPF. * [IME] [Windows] Fix IME after dialog show not working - active window context is not applied. * [IME] [Windows] fix intermediate input position * [IME] [Windows] PreEdit font size is applied * [IME] [Windows] Make MoveImeWindow code to be exact like in chrome - fix a lot of possible issues. Added comments. Minor Refactoring * [IME] [Windows] Refactor caret management, improve deactivation, remove comments * [IME] [Windows] Remove redundant api changes (request from @kekekeks) * Fix .sln and ApiCompatBesaline.txt redundant changes. * [Windows] [IME] move IsInputMethodEnabled subscription to InputMethodManager, Move check for IsInputMethodEnabled before TextInputMethodClientRequestedEvent query * [IME] [Windows] remove redundant SetActive(false) call, because it's called in Client setter * remove redundant change Co-authored-by: Yatao Li <yatli@microsoft.com> Co-authored-by: Max Katz <maxkatz6@outlook.com>pull/7146/head
committed by
GitHub
10 changed files with 525 additions and 14 deletions
@ -0,0 +1,32 @@ |
|||||
|
namespace Avalonia.Input |
||||
|
{ |
||||
|
public class InputMethod |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A dependency property that enables alternative text inputs.
|
||||
|
/// </summary>
|
||||
|
public static readonly AvaloniaProperty<bool> IsInputMethodEnabledProperty = |
||||
|
AvaloniaProperty.RegisterAttached<InputMethod, InputElement, bool>("IsInputMethodEnabled", true); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Setter for IsInputMethodEnabled AvaloniaProperty
|
||||
|
/// </summary>
|
||||
|
public static void SetIsInputMethodEnabled(InputElement target, bool value) |
||||
|
{ |
||||
|
target.SetValue(IsInputMethodEnabledProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Getter for IsInputMethodEnabled AvaloniaProperty
|
||||
|
/// </summary>
|
||||
|
public static bool GetIsInputMethodEnabled(InputElement target) |
||||
|
{ |
||||
|
return target.GetValue<bool>(IsInputMethodEnabledProperty); |
||||
|
} |
||||
|
|
||||
|
private InputMethod() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
using System; |
||||
|
using static Avalonia.Win32.Interop.UnmanagedMethods; |
||||
|
|
||||
|
namespace Avalonia.Win32.Input |
||||
|
{ |
||||
|
internal struct Imm32CaretManager |
||||
|
{ |
||||
|
private bool _isCaretCreated; |
||||
|
|
||||
|
public void TryCreate(int _langId, IntPtr hwnd) |
||||
|
{ |
||||
|
if (!_isCaretCreated) |
||||
|
{ |
||||
|
if (_langId == LANG_ZH || _langId == LANG_JA) |
||||
|
{ |
||||
|
_isCaretCreated = CreateCaret(hwnd, IntPtr.Zero, 2, 10); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void TryMove(int x, int y) |
||||
|
{ |
||||
|
if (_isCaretCreated) |
||||
|
{ |
||||
|
SetCaretPos(x, y); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void TryDestroy() |
||||
|
{ |
||||
|
if (_isCaretCreated) |
||||
|
{ |
||||
|
DestroyCaret(); |
||||
|
_isCaretCreated = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,231 @@ |
|||||
|
using System; |
||||
|
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>
|
||||
|
class Imm32InputMethod : ITextInputMethodImpl |
||||
|
{ |
||||
|
public IntPtr HWND { get; private set; } |
||||
|
private IntPtr _defaultImc; |
||||
|
private WindowImpl _parent; |
||||
|
private bool _active; |
||||
|
private bool _showCompositionWindow; |
||||
|
private Imm32CaretManager _caretManager = new(); |
||||
|
private bool _showCandidateList; |
||||
|
private ushort _langId; |
||||
|
private const int _caretMargin = 1; |
||||
|
|
||||
|
public void SetLanguageAndWindow(WindowImpl parent, IntPtr hwnd, IntPtr HKL) |
||||
|
{ |
||||
|
if (HWND != hwnd) |
||||
|
{ |
||||
|
_defaultImc = IntPtr.Zero; |
||||
|
} |
||||
|
HWND = hwnd; |
||||
|
_parent = parent; |
||||
|
_active = false; |
||||
|
_langId = PRIMARYLANGID(LGID(HKL)); |
||||
|
_showCompositionWindow = true; |
||||
|
_showCandidateList = true; |
||||
|
|
||||
|
IsComposing = false; |
||||
|
} |
||||
|
|
||||
|
//Dependant on CurrentThread. When Avalonia will support Multiple Dispatchers -
|
||||
|
//every Dispatcher should have their own InputMethod.
|
||||
|
public static Imm32InputMethod Current { get; } = new Imm32InputMethod(); |
||||
|
|
||||
|
private IntPtr DefaultImc |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
if (_defaultImc == IntPtr.Zero && |
||||
|
HWND != IntPtr.Zero) |
||||
|
{ |
||||
|
_defaultImc = ImmGetContext(HWND); |
||||
|
ImmReleaseContext(HWND, _defaultImc); |
||||
|
} |
||||
|
|
||||
|
if (_defaultImc == IntPtr.Zero) |
||||
|
{ |
||||
|
_defaultImc = ImmCreateContext(); |
||||
|
} |
||||
|
|
||||
|
return _defaultImc; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Reset() |
||||
|
{ |
||||
|
if (IsComposing) |
||||
|
{ |
||||
|
Dispatcher.UIThread.Post(() => |
||||
|
{ |
||||
|
ImmNotifyIME(DefaultImc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0); |
||||
|
ImmReleaseContext(HWND, DefaultImc); |
||||
|
IsComposing = false; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void SetActive(bool active) |
||||
|
{ |
||||
|
_active = active; |
||||
|
Dispatcher.UIThread.Post(() => |
||||
|
{ |
||||
|
if (active) |
||||
|
{ |
||||
|
if (DefaultImc != IntPtr.Zero) |
||||
|
{ |
||||
|
_caretManager.TryCreate(_langId, HWND); |
||||
|
// Load the default IME context.
|
||||
|
// NOTE(hbono)
|
||||
|
// IMM ignores this call if the IME context is loaded. Therefore, we do
|
||||
|
// not have to check whether or not the IME context is loaded.
|
||||
|
ImmAssociateContext(HWND, _defaultImc); |
||||
|
} |
||||
|
} |
||||
|
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.
|
||||
|
if (IsComposing) |
||||
|
{ |
||||
|
ImmNotifyIME(DefaultImc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0); |
||||
|
ImmReleaseContext(HWND, DefaultImc); |
||||
|
IsComposing = false; |
||||
|
} |
||||
|
ImmAssociateContext(HWND, IntPtr.Zero); |
||||
|
_caretManager.TryDestroy(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public void SetCursorRect(Rect rect) |
||||
|
{ |
||||
|
var focused = GetActiveWindow() == HWND; |
||||
|
if (!focused) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
Dispatcher.UIThread.Post(() => |
||||
|
{ |
||||
|
IntPtr himc = DefaultImc; |
||||
|
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(TextInputOptionsQueryEventArgs options) |
||||
|
{ |
||||
|
// we're skipping this. not usable on windows
|
||||
|
} |
||||
|
|
||||
|
public bool IsComposing { get; set; } |
||||
|
|
||||
|
~Imm32InputMethod() |
||||
|
{ |
||||
|
_caretManager.TryDestroy(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue