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