Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WPF - Add IME Support #2757

Merged
merged 2 commits into from
May 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CefSharp.Wpf/CefSharp.Wpf.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,15 @@
<Compile Include="CefSettings.cs" />
<Compile Include="Experimental\ChromiumWebBrowserWithTouchSupport.cs" />
<Compile Include="Internals\DragOperationMaskExtensions.cs" />
<Compile Include="Internals\ImeHandler.cs" />
<Compile Include="Internals\MonitorInfo.cs" />
<Compile Include="Internals\MonitorInfoEx.cs" />
<Compile Include="Internals\ImeNative.cs" />
<Compile Include="Internals\RectStruct.cs" />
<Compile Include="Experimental\WpfImeKeyboardHandler.cs" />
<Compile Include="Internals\WpfKeyboardHandler.cs" />
<Compile Include="IWpfKeyboardHandler.cs" />
<Compile Include="Internals\VirtualKeys.cs" />
<Compile Include="Internals\WpfKeyboardHandler.cs" />
<Compile Include="Internals\WpfLegacyKeyboardHandler.cs" />
<Compile Include="DelegateCommand.cs" />
<Compile Include="IRenderHandler.cs" />
Expand Down
9 changes: 7 additions & 2 deletions CefSharp.Wpf/ChromiumWebBrowser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using CefSharp.Enums;
using CefSharp.Internals;
using CefSharp.Structs;
using CefSharp.Wpf.Experimental;
using CefSharp.Wpf.Internals;
using CefSharp.Wpf.Rendering;
using Microsoft.Win32.SafeHandles;
Expand Down Expand Up @@ -970,7 +971,11 @@ void IRenderWebBrowser.OnImeCompositionRangeChanged(Range selectedRange, Rect[]
/// <param name="characterBounds">is the bounds of each character in view coordinates.</param>
protected virtual void OnImeCompositionRangeChanged(Range selectedRange, Rect[] characterBounds)
{
//TODO: Implement this
var imeKeyboardHandler = WpfKeyboardHandler as WpfImeKeyboardHandler;
if (imeKeyboardHandler != null)
{
imeKeyboardHandler.ChangeCompositionRange(selectedRange, characterBounds);
}
}

void IRenderWebBrowser.OnVirtualKeyboardRequested(IBrowser browser, TextInputMode inputMode)
Expand Down Expand Up @@ -1791,7 +1796,7 @@ protected virtual IWindowInfo CreateOffscreenBrowserWindowInfo(IntPtr handle)
/// </summary>
/// <param name="action">The action.</param>
/// <param name="priority">The priority.</param>
private void UiThreadRunAsync(Action action, DispatcherPriority priority = DispatcherPriority.DataBind)
internal void UiThreadRunAsync(Action action, DispatcherPriority priority = DispatcherPriority.DataBind)
{
if (Dispatcher.CheckAccess())
{
Expand Down
348 changes: 348 additions & 0 deletions CefSharp.Wpf/Experimental/WpfIMEKeyboardHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
// Copyright © 2018 The CefSharp Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using CefSharp.Internals;
using CefSharp.Structs;
using CefSharp.Wpf.Internals;
using Point = System.Windows.Point;
using Rect = CefSharp.Structs.Rect;

namespace CefSharp.Wpf.Experimental
{
public class WpfImeKeyboardHandler : WpfKeyboardHandler
{
private int languageCodeId;
private bool systemCaret;
private bool isDisposed;
private List<Rect> compositionBounds = new List<Rect>();
private HwndSource source;
private HwndSourceHook sourceHook;
private bool hasImeComposition;
private MouseButtonEventHandler mouseDownEventHandler;
private bool isActive;

public WpfImeKeyboardHandler(ChromiumWebBrowser owner) : base(owner)
{
}

public void ChangeCompositionRange(Range selectionRange, Rect[] characterBounds)
{
if (!isActive)
{
return;
}

var screenInfo = ((IRenderWebBrowser)owner).GetScreenInfo();
var scaleFactor = screenInfo.HasValue ? screenInfo.Value.DeviceScaleFactor : 1.0f;

//This is called on the CEF UI thread, we need to invoke back onte main UI thread to
//access the UI controls
owner.UiThreadRunAsync(() =>
{
//TODO: Getting the root window for every composition range change seems expensive,
//we should cache the position and update it on window move.
var parentWindow = Window.GetWindow(owner);
if (parentWindow != null)
{
//TODO: What are we calculating here exactly???
var point = owner.TransformToAncestor(parentWindow).Transform(new Point(0, 0));

var rects = new List<Rect>();

foreach (var item in characterBounds)
{
rects.Add(new Rect(
(int)((point.X + item.X) * scaleFactor),
(int)((point.Y + item.Y) * scaleFactor),
(int)(item.Width * scaleFactor),
(int)(item.Height * scaleFactor)));
}

compositionBounds = rects;
MoveImeWindow(source.Handle);
}
});
}

public override void Setup(HwndSource source)
{
this.source = source;
sourceHook = SourceHook;
source.AddHook(SourceHook);

owner.GotFocus += OwnerGotFocus;
owner.LostFocus += OwnerLostFocus;

mouseDownEventHandler = new MouseButtonEventHandler(OwnerMouseDown);

owner.AddHandler(UIElement.MouseDownEvent, mouseDownEventHandler, true);

owner.Focus();
}

public override void Dispose()
{
if (isDisposed)
{
return;
}

isDisposed = true;

owner.GotFocus -= OwnerGotFocus;
owner.LostFocus -= OwnerLostFocus;

owner.RemoveHandler(UIElement.MouseDownEvent, mouseDownEventHandler);

if (source != null && sourceHook != null)
{
source.RemoveHook(sourceHook);
source = null;
}
}

private void OwnerMouseDown(object sender, MouseButtonEventArgs e)
{
CloseImeComposition();
}

private void OwnerLostFocus(object sender, RoutedEventArgs e)
{
isActive = false;

// These calls are needed in order for IME to function correctly.
InputMethod.SetIsInputMethodEnabled(owner, false);
InputMethod.SetIsInputMethodSuspended(owner, false);
}

private void OwnerGotFocus(object sender, RoutedEventArgs e)
{
// These calls are needed in order for IME to function correctly.
InputMethod.SetIsInputMethodEnabled(owner, true);
InputMethod.SetIsInputMethodSuspended(owner, true);

isActive = true;
}

private IntPtr SourceHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
handled = false;

if (!isActive || isDisposed || owner == null || owner.IsDisposed || owner.GetBrowserHost() == null)
{
return IntPtr.Zero;
}

switch ((WM)msg)
{
case WM.IME_SETCONTEXT:
{
OnImeSetContext(hwnd, (uint)msg, wParam, lParam);
handled = true;
break;
}
case WM.IME_STARTCOMPOSITION:
{
OnIMEStartComposition(hwnd);
hasImeComposition = true;
handled = true;
break;
}
case WM.IME_COMPOSITION:
{
OnImeComposition(hwnd, lParam.ToInt32());
handled = true;
break;
}
case WM.IME_ENDCOMPOSITION:
{
OnImeEndComposition(hwnd);
hasImeComposition = false;
handled = true;
break;
}
}

return handled ? IntPtr.Zero : new IntPtr(1);
}

private void CloseImeComposition()
{
if (hasImeComposition)
{
// Set focus to 0, which destroys IME suggestions window.
ImeNative.SetFocus(IntPtr.Zero);
// Restore focus.
ImeNative.SetFocus(source.Handle);
}
}

private void OnImeComposition(IntPtr hwnd, int lParam)
{
string text = string.Empty;

if (ImeHandler.GetResult(hwnd, (uint)lParam, out text))
{
owner.GetBrowserHost().ImeCommitText(text, new Range(int.MaxValue, int.MaxValue), 0);
}
else
{
var underlines = new List<CompositionUnderline>();
int compositionStart = 0;

if (ImeHandler.GetComposition(hwnd, (uint)lParam, underlines, ref compositionStart, out text))
{
owner.GetBrowserHost().ImeSetComposition(text, underlines.ToArray(), new Range(int.MaxValue, int.MaxValue), new Range(compositionStart, compositionStart));

UpdateCaretPosition(compositionStart - 1);
}
else
{
CancelComposition(hwnd);
}
}
}

public void CancelComposition(IntPtr hwnd)
{
owner.GetBrowserHost().ImeCancelComposition();
DestroyImeWindow(hwnd);
}

private void OnImeEndComposition(IntPtr hwnd)
{
owner.GetBrowserHost().ImeFinishComposingText(false);
DestroyImeWindow(hwnd);
}

private void OnImeSetContext(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam)
{
// We handle the IME Composition Window ourselves (but let the IME Candidates
// Window be handled by IME through DefWindowProc()), so clear the
// ISC_SHOWUICOMPOSITIONWINDOW flag:
ImeNative.DefWindowProc(hwnd, msg, wParam, (IntPtr)(lParam.ToInt64() & ~ImeNative.ISC_SHOWUICOMPOSITIONWINDOW));
// TODO: should we call ImmNotifyIME?

CreateImeWindow(hwnd);
MoveImeWindow(hwnd);
}

private void OnIMEStartComposition(IntPtr hwnd)
{
CreateImeWindow(hwnd);
MoveImeWindow(hwnd);
}

private void CreateImeWindow(IntPtr hwnd)
{
// Chinese/Japanese IMEs somehow ignore function calls to
// ::ImmSetCandidateWindow(), and use the position of the current system
// caret instead -::GetCaretPos().
// Therefore, we create a temporary system caret for Chinese IMEs and use
// it during this input context.
// Since some third-party Japanese IME also uses ::GetCaretPos() to determine
// their window position, we also create a caret for Japanese IMEs.
languageCodeId = PrimaryLangId(InputLanguageManager.Current.CurrentInputLanguage.KeyboardLayoutId);

if (languageCodeId == ImeNative.LANG_JAPANESE || languageCodeId == ImeNative.LANG_CHINESE)
{
if (!systemCaret)
{
if (ImeNative.CreateCaret(hwnd, IntPtr.Zero, 1, 1))
{
systemCaret = true;
}
}
}
}

private int PrimaryLangId(int lgid)
{
return lgid & 0x3ff;
}

private void MoveImeWindow(IntPtr hwnd)
{
if (compositionBounds.Count == 0)
{
return;
}

var hIMC = ImeNative.ImmGetContext(hwnd);

var rc = compositionBounds[0];

var x = rc.X + rc.Width;
var y = rc.Y + rc.Height;

const int kCaretMargin = 1;
// As written in a comment in ImeInput::CreateImeWindow(),
// 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 if it exists.
var candidatePosition = new ImeNative.CANDIDATEFORM
{
dwIndex = 0,
dwStyle = (int)ImeNative.CFS_CANDIDATEPOS,
ptCurrentPos = new ImeNative.POINT(x, y),
rcArea = new ImeNative.RECT(0, 0, 0, 0)
};
ImeNative.ImmSetCandidateWindow(hIMC, ref candidatePosition);

if (systemCaret)
{
ImeNative.SetCaretPos(x, y);
}

if (languageCodeId == ImeNative.LANG_KOREAN)
{
// 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.
y += kCaretMargin;
}
// 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 ImeNative.CANDIDATEFORM
{
dwIndex = 0,
dwStyle = (int)ImeNative.CFS_EXCLUDE,
ptCurrentPos = new ImeNative.POINT(x, y),
rcArea = new ImeNative.RECT(rc.X, rc.Y, x, y + kCaretMargin)
};
ImeNative.ImmSetCandidateWindow(hIMC, ref excludeRectangle);

ImeNative.ImmReleaseContext(hwnd, hIMC);
}

private void DestroyImeWindow(IntPtr hwnd)
{
if (systemCaret)
{
ImeNative.DestroyCaret();
systemCaret = false;
}
}

//TODO: Should we remove this, it's only a single method
private void UpdateCaretPosition(int index)
{
MoveImeWindow(source.Handle);
}
}
}
Loading