[Mac] Support asynchronous NSTextInputClient
authorap@apple.com <ap@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 20 Mar 2014 17:45:04 +0000 (17:45 +0000)
committerap@apple.com <ap@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 20 Mar 2014 17:45:04 +0000 (17:45 +0000)
https://bugs.webkit.org/show_bug.cgi?id=130479

Reviewed by Anders Carlsson.

The implementation is currently disabled, pending lower level support.
Most of the code is not under compile time guard however, to facilitate cross-platform
reuse, or at least under a PLATFORM(COCOA) guard to share the code with iOS.

* UIProcess/API/mac/WKView.mm: Added a compile time branch for USE(ASYNC_NSTEXTINPUTCLIENT).
We still implement sync NSTextInputClient here, in order to get assertions when
its methods are unexpectedly called.
The new code first sends an event to input method asynchronously, handling any callbacks
that may arrive. During this time, we no longer care about WKViewInterpretKeyEventsParameters
at all. Once done, we interpret key bindings synchronously, collecting them into
a vector.

* UIProcess/API/mac/WKViewInternal.h: We no longer expose _interpretKeyEvent outside
WKView.

* UIProcess/WebPageProxy.cpp:
* UIProcess/WebPageProxy.h:
Added async calls and callbacks. Removed unnecessary and slightly harmful .get() when moving
a callback pointer into map. Moved insertDictatedText() and getAttributedSubstringFromRange()
from PLATFORM(COCOA) to PLATFORM(MAC), because they are unused and unimplemented on
iOS, and unlikely to be needed any time soon. Changed USE(APPKIT) to PLATFORM(MAC),
because that's more accurate in this case (nothing depends on AppKit, it's just code
that we only need on Mac).

* UIProcess/WebPageProxy.messages.in: Added messages for new async IM responses.

* UIProcess/ios/WebPageProxyIOS.mm: Removed insertDictatedText() and getAttributedSubstringFromRange().

* UIProcess/mac/WebPageProxyMac.mm:
(WebKit::WebPageProxy::insertDictatedTextAsync):
(WebKit::WebPageProxy::attributedSubstringForCharacterRangeAsync):
(WebKit::WebPageProxy::attributedStringForCharacterRangeCallback):
Added async calls and callbacks that are Mac only.

* WebProcess/WebPage/WebPage.cpp:
* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/mac/WebPageMac.mm:
Added async implementations (which are essentially the same as sync ones, sadly).

* WebProcess/WebPage/WebPage.messages.in: Added async messages, moved some messages
under PLATFORM(MAC).

* WebProcess/WebPage/ios/WebPageIOS.mm: More of deleting functions that are Mac only,
and cannot be easily implemented in WebPage.cpp with shared code.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@165972 268f45cc-cd09-0410-ab3c-d52691b4dbfc

13 files changed:
Source/WebKit2/ChangeLog
Source/WebKit2/UIProcess/API/mac/WKView.mm
Source/WebKit2/UIProcess/API/mac/WKViewInternal.h
Source/WebKit2/UIProcess/WebPageProxy.cpp
Source/WebKit2/UIProcess/WebPageProxy.h
Source/WebKit2/UIProcess/WebPageProxy.messages.in
Source/WebKit2/UIProcess/ios/WebPageProxyIOS.mm
Source/WebKit2/UIProcess/mac/WebPageProxyMac.mm
Source/WebKit2/WebProcess/WebPage/WebPage.cpp
Source/WebKit2/WebProcess/WebPage/WebPage.h
Source/WebKit2/WebProcess/WebPage/WebPage.messages.in
Source/WebKit2/WebProcess/WebPage/ios/WebPageIOS.mm
Source/WebKit2/WebProcess/WebPage/mac/WebPageMac.mm

index 2a5dcb7..c074fbc 100644 (file)
@@ -1,3 +1,55 @@
+2014-03-19  Alexey Proskuryakov  <ap@apple.com>
+
+        [Mac] Support asynchronous NSTextInputClient
+        https://bugs.webkit.org/show_bug.cgi?id=130479
+
+        Reviewed by Anders Carlsson.
+
+        The implementation is currently disabled, pending lower level support.
+        Most of the code is not under compile time guard however, to facilitate cross-platform
+        reuse, or at least under a PLATFORM(COCOA) guard to share the code with iOS.
+
+        * UIProcess/API/mac/WKView.mm: Added a compile time branch for USE(ASYNC_NSTEXTINPUTCLIENT).
+        We still implement sync NSTextInputClient here, in order to get assertions when
+        its methods are unexpectedly called.
+        The new code first sends an event to input method asynchronously, handling any callbacks
+        that may arrive. During this time, we no longer care about WKViewInterpretKeyEventsParameters
+        at all. Once done, we interpret key bindings synchronously, collecting them into
+        a vector.
+
+        * UIProcess/API/mac/WKViewInternal.h: We no longer expose _interpretKeyEvent outside
+        WKView.
+
+        * UIProcess/WebPageProxy.cpp:
+        * UIProcess/WebPageProxy.h:
+        Added async calls and callbacks. Removed unnecessary and slightly harmful .get() when moving
+        a callback pointer into map. Moved insertDictatedText() and getAttributedSubstringFromRange()
+        from PLATFORM(COCOA) to PLATFORM(MAC), because they are unused and unimplemented on
+        iOS, and unlikely to be needed any time soon. Changed USE(APPKIT) to PLATFORM(MAC),
+        because that's more accurate in this case (nothing depends on AppKit, it's just code
+        that we only need on Mac).
+
+        * UIProcess/WebPageProxy.messages.in: Added messages for new async IM responses.
+
+        * UIProcess/ios/WebPageProxyIOS.mm: Removed insertDictatedText() and getAttributedSubstringFromRange().
+
+        * UIProcess/mac/WebPageProxyMac.mm:
+        (WebKit::WebPageProxy::insertDictatedTextAsync):
+        (WebKit::WebPageProxy::attributedSubstringForCharacterRangeAsync):
+        (WebKit::WebPageProxy::attributedStringForCharacterRangeCallback):
+        Added async calls and callbacks that are Mac only.
+
+        * WebProcess/WebPage/WebPage.cpp:
+        * WebProcess/WebPage/WebPage.h:
+        * WebProcess/WebPage/mac/WebPageMac.mm:
+        Added async implementations (which are essentially the same as sync ones, sadly).
+
+        * WebProcess/WebPage/WebPage.messages.in: Added async messages, moved some messages
+        under PLATFORM(MAC).
+
+        * WebProcess/WebPage/ios/WebPageIOS.mm: More of deleting functions that are Mac only,
+        and cannot be easily implemented in WebPage.cpp with shared code.
+
 2014-03-20  Martin Robinson  <mrobinson@igalia.com>
 
         Remove remaining GTK+ unused parameter warnings from WebKit2
index c6bf557..188f406 100644 (file)
 - (void)_maskRoundedBottomCorners:(NSRect)clipRect;
 @end
 
+#if USE(ASYNC_NSTEXTINPUTCLIENT)
+@interface NSTextInputContext (WKNSTextInputContextDetails)
+- (void)handleEvent:(NSEvent *)theEvent completionHandler:(void(^)(BOOL handled))completionHandler;
+- (void)handleEventByInputMethod:(NSEvent *)theEvent completionHandler:(void(^)(BOOL handled))completionHandler;
+- (BOOL)handleEventByKeyboardLayout:(NSEvent *)theEvent;
+@end
+#endif
+
 #if defined(__has_include) && __has_include(<CoreGraphics/CoreGraphicsPrivate.h>)
 #import <CoreGraphics/CoreGraphicsPrivate.h>
 #import <CoreGraphics/CGSCapture.h>
@@ -139,12 +147,14 @@ typedef HashMap<String, ValidationVector> ValidationMap;
 
 }
 
+#if !USE(ASYNC_NSTEXTINPUTCLIENT)
 struct WKViewInterpretKeyEventsParameters {
     bool eventInterpretationHadSideEffects;
     bool consumedByIM;
     bool executingSavedKeypressCommands;
     Vector<KeypressCommand>* commands;
 };
+#endif
 
 @interface WKViewData : NSObject {
 @public
@@ -173,7 +183,11 @@ struct WKViewInterpretKeyEventsParameters {
     // the application to distinguish the case of a new event from one 
     // that has been already sent to WebCore.
     RetainPtr<NSEvent> _keyDownEventBeingResent;
+#if USE(ASYNC_NSTEXTINPUTCLIENT)
+    Vector<KeypressCommand>* _collectedKeypressCommands;
+#else
     WKViewInterpretKeyEventsParameters* _interpretKeyEventsParameters;
+#endif
 
     NSSize _resizeScrollOffset;
 
@@ -1083,6 +1097,26 @@ static NSToolbarItem *toolbarItem(id <NSValidatedUserInterfaceItem> item)
     _data->_mouseDownEvent = [event retain];
 }
 
+#if USE(ASYNC_NSTEXTINPUTCLIENT)
+#define NATIVE_MOUSE_EVENT_HANDLER(Selector) \
+    - (void)Selector:(NSEvent *)theEvent \
+    { \
+        if ([self shouldIgnoreMouseEvents]) \
+            return; \
+        if (NSTextInputContext *context = [self inputContext]) { \
+            [context handleEvent:theEvent completionHandler:^(BOOL handled) { \
+                if (handled) \
+                    LOG(TextInput, "%s was handled by text input context", String(#Selector).substring(0, String(#Selector).find("Internal")).ascii().data()); \
+                else { \
+                    NativeWebMouseEvent webEvent(theEvent, self); \
+                    _data->_page->handleMouseEvent(webEvent); \
+                } \
+            }]; \
+        } \
+        NativeWebMouseEvent webEvent(theEvent, self); \
+        _data->_page->handleMouseEvent(webEvent); \
+    }
+#else
 #define NATIVE_MOUSE_EVENT_HANDLER(Selector) \
     - (void)Selector:(NSEvent *)theEvent \
     { \
@@ -1095,6 +1129,7 @@ static NSToolbarItem *toolbarItem(id <NSValidatedUserInterfaceItem> item)
         NativeWebMouseEvent webEvent(theEvent, self); \
         _data->_page->handleMouseEvent(webEvent); \
     }
+#endif
 
 NATIVE_MOUSE_EVENT_HANDLER(mouseEntered)
 NATIVE_MOUSE_EVENT_HANDLER(mouseExited)
@@ -1213,20 +1248,126 @@ NATIVE_MOUSE_EVENT_HANDLER(rightMouseUp)
     return result;
 }
 
+- (void)_disableComplexTextInputIfNecessary
+{
+    if (!_data->_pluginComplexTextInputIdentifier)
+        return;
+
+    if (_data->_pluginComplexTextInputState != PluginComplexTextInputEnabled)
+        return;
+
+    // Check if the text input window has been dismissed.
+    if (![[WKTextInputWindowController sharedTextInputWindowController] hasMarkedText])
+        [self _setPluginComplexTextInputState:PluginComplexTextInputDisabled];
+}
+
+- (BOOL)_handlePluginComplexTextInputKeyDown:(NSEvent *)event
+{
+    ASSERT(_data->_pluginComplexTextInputIdentifier);
+    ASSERT(_data->_pluginComplexTextInputState != PluginComplexTextInputDisabled);
+
+    BOOL usingLegacyCocoaTextInput = _data->_pluginComplexTextInputState == PluginComplexTextInputEnabledLegacy;
+
+    NSString *string = nil;
+    BOOL didHandleEvent = [[WKTextInputWindowController sharedTextInputWindowController] interpretKeyEvent:event usingLegacyCocoaTextInput:usingLegacyCocoaTextInput string:&string];
+
+    if (string) {
+        _data->_page->sendComplexTextInputToPlugin(_data->_pluginComplexTextInputIdentifier, string);
+
+        if (!usingLegacyCocoaTextInput)
+            _data->_pluginComplexTextInputState = PluginComplexTextInputDisabled;
+    }
+
+    return didHandleEvent;
+}
+
+- (BOOL)_tryHandlePluginComplexTextInputKeyDown:(NSEvent *)event
+{
+    if (!_data->_pluginComplexTextInputIdentifier || _data->_pluginComplexTextInputState == PluginComplexTextInputDisabled)
+        return NO;
+
+    // Check if the text input window has been dismissed and let the plug-in process know.
+    // This is only valid with the updated Cocoa text input spec.
+    [self _disableComplexTextInputIfNecessary];
+
+    // Try feeding the keyboard event directly to the plug-in.
+    if (_data->_pluginComplexTextInputState == PluginComplexTextInputEnabledLegacy)
+        return [self _handlePluginComplexTextInputKeyDown:event];
+
+    return NO;
+}
+
+static void extractUnderlines(NSAttributedString *string, Vector<CompositionUnderline>& result)
+{
+    int length = [[string string] length];
+    
+    int i = 0;
+    while (i < length) {
+        NSRange range;
+        NSDictionary *attrs = [string attributesAtIndex:i longestEffectiveRange:&range inRange:NSMakeRange(i, length - i)];
+        
+        if (NSNumber *style = [attrs objectForKey:NSUnderlineStyleAttributeName]) {
+            Color color = Color::black;
+            if (NSColor *colorAttr = [attrs objectForKey:NSUnderlineColorAttributeName])
+                color = colorFromNSColor([colorAttr colorUsingColorSpaceName:NSDeviceRGBColorSpace]);
+            result.append(CompositionUnderline(range.location, NSMaxRange(range), color, [style intValue] > 1));
+        }
+        
+        i = range.location + range.length;
+    }
+}
+
+#if USE(ASYNC_NSTEXTINPUTCLIENT)
+
+- (void)_collectKeyboardLayoutCommandsForEvent:(NSEvent *)event to:(Vector<KeypressCommand>&)commands
+{
+    if ([event type] != NSKeyDown)
+        return;
+
+    ASSERT(!_data->_collectedKeypressCommands);
+    _data->_collectedKeypressCommands = &commands;
+
+    if (NSTextInputContext *context = [self inputContext])
+        [context handleEventByKeyboardLayout:event];
+    else
+        [self interpretKeyEvents:[NSArray arrayWithObject:event]];
+
+    _data->_collectedKeypressCommands = nullptr;
+}
+
+- (void)_interpretKeyEvent:(NSEvent *)event completionHandler:(void(^)(BOOL handled, const Vector<KeypressCommand>& commands))completionHandler
+{
+    if (![self inputContext]) {
+        Vector<KeypressCommand> commands;
+        [self _collectKeyboardLayoutCommandsForEvent:event to:commands];
+        completionHandler(NO, commands);
+        return;
+    }
+
+    LOG(TextInput, "-> handleEventByInputMethod:%p %@", event, event);
+    [[self inputContext] handleEventByInputMethod:event completionHandler:^(BOOL handled) {
+        
+        LOG(TextInput, "... handleEventByInputMethod%s handled", handled ? "" : " not");
+        if (handled) {
+            completionHandler(YES, Vector<KeypressCommand>());
+            return;
+        }
+
+        Vector<KeypressCommand> commands;
+        [self _collectKeyboardLayoutCommandsForEvent:event to:commands];
+        completionHandler(NO, commands);
+    }];
+}
+
 - (void)doCommandBySelector:(SEL)selector
 {
     LOG(TextInput, "doCommandBySelector:\"%s\"", sel_getName(selector));
 
-    WKViewInterpretKeyEventsParameters* parameters = _data->_interpretKeyEventsParameters;
-    if (parameters)
-        parameters->consumedByIM = false;
-
-    // As in insertText:replacementRange:, we assume that the call comes from an input method if there is marked text.
-    bool isFromInputMethod = _data->_page->editorState().hasComposition;
+    Vector<KeypressCommand>* keypressCommands = _data->_collectedKeypressCommands;
 
-    if (parameters && !isFromInputMethod) {
+    if (keypressCommands) {
         KeypressCommand command(NSStringFromSelector(selector));
-        parameters->commands->append(command);
+        keypressCommands->append(command);
         LOG(TextInput, "...stored");
         _data->_page->registerKeypressCommandName(command.commandName);
     } else {
@@ -1238,7 +1379,7 @@ NATIVE_MOUSE_EVENT_HANDLER(rightMouseUp)
 
 - (void)insertText:(id)string
 {
-    // Unlike and NSTextInputClient variant with replacementRange, this NSResponder method is called when there is no input context,
+    // Unlike an NSTextInputClient variant with replacementRange, this NSResponder method is called when there is no input context,
     // so text input processing isn't performed. We are not going to actually insert any text in that case, but saving an insertText
     // command ensures that a keypress event is dispatched as appropriate.
     [self insertText:string replacementRange:NSMakeRange(NSNotFound, 0)];
@@ -1253,13 +1394,8 @@ NATIVE_MOUSE_EVENT_HANDLER(rightMouseUp)
         LOG(TextInput, "insertText:\"%@\" replacementRange:(%u, %u)", isAttributedString ? [string string] : string, replacementRange.location, replacementRange.length);
     else
         LOG(TextInput, "insertText:\"%@\"", isAttributedString ? [string string] : string);
-    WKViewInterpretKeyEventsParameters* parameters = _data->_interpretKeyEventsParameters;
-    if (parameters)
-        parameters->consumedByIM = false;
 
     NSString *text;
-    bool isFromInputMethod = _data->_page->editorState().hasComposition;
-
     Vector<TextAlternativeWithRange> dictationAlternatives;
 
     if (isAttributedString) {
@@ -1272,136 +1408,307 @@ NATIVE_MOUSE_EVENT_HANDLER(rightMouseUp)
         text = string;
 
     // insertText can be called for several reasons:
-    // - If it's from normal key event processing (including key bindings), we may need to save the action to perform it later.
-    // - If it's from an input method, then we should go ahead and insert the text now. We assume it's from the input method if we have marked text.
-    // FIXME: In theory, this could be wrong for some input methods, so we should try to find another way to determine if the call is from the input method.
+    // - If it's from normal key event processing (including key bindings), we save the action to perform it later.
+    // - If it's from an input method, then we should go ahead and insert the text now.
     // - If it's sent outside of keyboard event processing (e.g. from Character Viewer, or when confirming an inline input area with a mouse),
     // then we also execute it immediately, as there will be no other chance.
-    if (parameters && !isFromInputMethod) {
-        // FIXME: Handle replacementRange in this case, too. It's known to occur in practice when canceling Press and Hold (see <rdar://11940670>).
+    Vector<KeypressCommand>* keypressCommands = _data->_collectedKeypressCommands;
+    if (keypressCommands) {
         ASSERT(replacementRange.location == NSNotFound);
         KeypressCommand command("insertText:", text);
-        parameters->commands->append(command);
+        keypressCommands->append(command);
+        LOG(TextInput, "...stored");
         _data->_page->registerKeypressCommandName(command.commandName);
         return;
     }
 
     String eventText = text;
     eventText.replace(NSBackTabCharacter, NSTabCharacter); // same thing is done in KeyEventMac.mm in WebCore
-    bool eventHandled;
     if (!dictationAlternatives.isEmpty())
-        eventHandled = _data->_page->insertDictatedText(eventText, replacementRange, dictationAlternatives);
+        _data->_page->insertDictatedTextAsync(eventText, replacementRange, dictationAlternatives);
     else
-        eventHandled = _data->_page->insertText(eventText, replacementRange);
-
-    if (parameters)
-        parameters->eventInterpretationHadSideEffects |= eventHandled;
+        _data->_page->insertTextAsync(eventText, replacementRange);
 }
 
-- (BOOL)performKeyEquivalent:(NSEvent *)event
+- (void)selectedRangeWithCompletionHandler:(void(^)(NSRange selectedRange))completionHandlerPtr
 {
-    // There's a chance that responding to this event will run a nested event loop, and
-    // fetching a new event might release the old one. Retaining and then autoreleasing
-    // the current event prevents that from causing a problem inside WebKit or AppKit code.
-    [[event retain] autorelease];
+    RetainPtr<id> completionHandler = adoptNS([completionHandlerPtr copy]);
 
-    // We get Esc key here after processing either Esc or Cmd+period. The former starts as a keyDown, and the latter starts as a key equivalent,
-    // but both get transformed to a cancelOperation: command, executing which passes an Esc key event to -performKeyEquivalent:.
-    // Don't interpret this event again, avoiding re-entrancy and infinite loops.
-    if ([[event charactersIgnoringModifiers] isEqualToString:@"\e"] && !([event modifierFlags] & NSDeviceIndependentModifierFlagsMask))
-        return [super performKeyEquivalent:event];
+    LOG(TextInput, "selectedRange");
+    _data->_page->getSelectedRangeAsync(EditingRangeCallback::create([completionHandler](bool error, const EditingRange& editingRangeResult) {
+        void (^completionHandlerBlock)(NSRange) = (void (^)(NSRange))completionHandler.get();
+        if (error) {
+            LOG(TextInput, "    ...selectedRange failed.");
+            completionHandlerBlock(NSMakeRange(NSNotFound, 0));
+            return;
+        }
+        NSRange result = editingRangeResult;
+        if (result.location == NSNotFound)
+            LOG(TextInput, "    -> selectedRange returned (NSNotFound, %llu)", result.length);
+        else
+            LOG(TextInput, "    -> selectedRange returned (%llu, %llu)", result.location, result.length);
+        completionHandlerBlock(result);
+    }));
+}
 
-    // If we are already re-sending the event, then WebCore has already seen it, no need for custom processing.
-    BOOL eventWasSentToWebCore = (_data->_keyDownEventBeingResent == event);
-    if (eventWasSentToWebCore)
-        return [super performKeyEquivalent:event];
+- (void)markedRangeWithCompletionHandler:(void(^)(NSRange markedRange))completionHandlerPtr
+{
+    RetainPtr<id> completionHandler = adoptNS([completionHandlerPtr copy]);
 
-    ASSERT(event == [NSApp currentEvent]);
+    LOG(TextInput, "markedRange");
+    _data->_page->getMarkedRangeAsync(EditingRangeCallback::create([completionHandler](bool error, const EditingRange& editingRangeResult) {
+        void (^completionHandlerBlock)(NSRange) = (void (^)(NSRange))completionHandler.get();
+        if (error) {
+            LOG(TextInput, "    ...markedRange failed.");
+            completionHandlerBlock(NSMakeRange(NSNotFound, 0));
+            return;
+        }
+        NSRange result = editingRangeResult;
+        if (result.location == NSNotFound)
+            LOG(TextInput, "    -> markedRange returned (NSNotFound, %llu)", result.length);
+        else
+            LOG(TextInput, "    -> markedRange returned (%llu, %llu)", result.location, result.length);
+        completionHandlerBlock(result);
+    }));
+}
 
-    [self _disableComplexTextInputIfNecessary];
+- (void)hasMarkedTextWithCompletionHandler:(void(^)(BOOL hasMarkedText))completionHandlerPtr
+{
+    RetainPtr<id> completionHandler = adoptNS([completionHandlerPtr copy]);
 
-    // Pass key combos through WebCore if there is a key binding available for
-    // this event. This lets web pages have a crack at intercepting key-modified keypresses.
-    // FIXME: Why is the firstResponder check needed?
-    if (self == [[self window] firstResponder]) {
-        Vector<KeypressCommand> commands;
-        BOOL handledByInputMethod = [self _interpretKeyEvent:event savingCommandsTo:commands];
-        _data->_page->handleKeyboardEvent(NativeWebKeyboardEvent(event, handledByInputMethod, commands));
-        return YES;
-    }
-    
-    return [super performKeyEquivalent:event];
+    LOG(TextInput, "hasMarkedText");
+    _data->_page->getMarkedRangeAsync(EditingRangeCallback::create([completionHandler](bool error, const EditingRange& editingRangeResult) {
+        void (^completionHandlerBlock)(BOOL) = (void (^)(BOOL))completionHandler.get();
+        if (error) {
+            LOG(TextInput, "    ...hasMarkedText failed.");
+            completionHandlerBlock(NO);
+            return;
+        }
+        BOOL hasMarkedText = editingRangeResult.location != notFound;
+        LOG(TextInput, "    -> hasMarkedText returned %u", hasMarkedText);
+        completionHandlerBlock(hasMarkedText);
+    }));
 }
 
-- (void)keyUp:(NSEvent *)theEvent
+- (void)attributedSubstringForProposedRange:(NSRange)nsRange completionHandler:(void(^)(NSAttributedString *attrString, NSRange actualRange))completionHandlerPtr
 {
-    LOG(TextInput, "keyUp:%p %@", theEvent, theEvent);
-    // We don't interpret the keyUp event, as this breaks key bindings (see <https://bugs.webkit.org/show_bug.cgi?id=130100>).
-    _data->_page->handleKeyboardEvent(NativeWebKeyboardEvent(theEvent, false, Vector<KeypressCommand>()));
+    RetainPtr<id> completionHandler = adoptNS([completionHandlerPtr copy]);
+
+    LOG(TextInput, "attributedSubstringFromRange:(%u, %u)", nsRange.location, nsRange.length);
+    _data->_page->attributedSubstringForCharacterRangeAsync(nsRange, AttributedStringForCharacterRangeCallback::create([completionHandler](bool error, const AttributedString& string, const EditingRange& actualRange) {
+        void (^completionHandlerBlock)(NSAttributedString *, NSRange) = (void (^)(NSAttributedString *, NSRange))completionHandler.get();
+        if (error) {
+            LOG(TextInput, "    ...attributedSubstringFromRange failed.");
+            completionHandlerBlock(0, NSMakeRange(NSNotFound, 0));
+            return;
+        }
+        LOG(TextInput, "    -> attributedSubstringFromRange returned %@", [string.string.get() string]);
+        completionHandlerBlock([[string.string.get() retain] autorelease], actualRange);
+    }));
 }
 
-- (void)_disableComplexTextInputIfNecessary
+- (void)firstRectForCharacterRange:(NSRange)theRange completionHandler:(void(^)(NSRect firstRect, NSRange actualRange))completionHandlerPtr
 {
-    if (!_data->_pluginComplexTextInputIdentifier)
-        return;
+    RetainPtr<id> completionHandler = adoptNS([completionHandlerPtr copy]);
 
-    if (_data->_pluginComplexTextInputState != PluginComplexTextInputEnabled)
-        return;
+    LOG(TextInput, "firstRectForCharacterRange:(%u, %u)", theRange.location, theRange.length);
 
-    // Check if the text input window has been dismissed.
-    if (![[WKTextInputWindowController sharedTextInputWindowController] hasMarkedText])
-        [self _setPluginComplexTextInputState:PluginComplexTextInputDisabled];
+    // Just to match NSTextView's behavior. Regression tests cannot detect this;
+    // to reproduce, use a test application from http://bugs.webkit.org/show_bug.cgi?id=4682
+    // (type something; try ranges (1, -1) and (2, -1).
+    if ((theRange.location + theRange.length < theRange.location) && (theRange.location + theRange.length != 0))
+        theRange.length = 0;
+
+    _data->_page->firstRectForCharacterRangeAsync(theRange, RectForCharacterRangeCallback::create([self, completionHandler](bool error, const IntRect& rect, const EditingRange& actualRange) {
+        void (^completionHandlerBlock)(NSRect, NSRange) = (void (^)(NSRect, NSRange))completionHandler.get();
+        if (error) {
+            LOG(TextInput, "    ...firstRectForCharacterRange failed.");
+            completionHandlerBlock(NSMakeRect(0, 0, 0, 0), NSMakeRange(NSNotFound, 0));
+            return;
+        }
+
+        NSRect resultRect = [self convertRect:rect toView:nil];
+        resultRect = [self.window convertRectToScreen:resultRect];
+
+        LOG(TextInput, "    -> firstRectForCharacterRange returned (%f, %f, %f, %f)", resultRect.origin.x, resultRect.origin.y, resultRect.size.width, resultRect.size.height);
+        completionHandlerBlock(resultRect, actualRange);
+    }));
 }
 
-- (BOOL)_handlePluginComplexTextInputKeyDown:(NSEvent *)event
+- (void)characterIndexForPoint:(NSPoint)thePoint completionHandler:(void(^)(NSUInteger))completionHandlerPtr
 {
-    ASSERT(_data->_pluginComplexTextInputIdentifier);
-    ASSERT(_data->_pluginComplexTextInputState != PluginComplexTextInputDisabled);
-
-    BOOL usingLegacyCocoaTextInput = _data->_pluginComplexTextInputState == PluginComplexTextInputEnabledLegacy;
+    RetainPtr<id> completionHandler = adoptNS([completionHandlerPtr copy]);
 
-    NSString *string = nil;
-    BOOL didHandleEvent = [[WKTextInputWindowController sharedTextInputWindowController] interpretKeyEvent:event usingLegacyCocoaTextInput:usingLegacyCocoaTextInput string:&string];
+    LOG(TextInput, "characterIndexForPoint:(%f, %f)", thePoint.x, thePoint.y);
 
-    if (string) {
-        _data->_page->sendComplexTextInputToPlugin(_data->_pluginComplexTextInputIdentifier, string);
+    NSWindow *window = [self window];
 
-        if (!usingLegacyCocoaTextInput)
-            _data->_pluginComplexTextInputState = PluginComplexTextInputDisabled;
-    }
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+    if (window)
+        thePoint = [window convertScreenToBase:thePoint];
+#pragma clang diagnostic pop
+    thePoint = [self convertPoint:thePoint fromView:nil];  // the point is relative to the main frame
 
-    return didHandleEvent;
+    _data->_page->characterIndexForPointAsync(IntPoint(thePoint), UnsignedCallback::create([completionHandler](bool error, uint64_t result) {
+        void (^completionHandlerBlock)(NSUInteger) = (void (^)(NSUInteger))completionHandler.get();
+        if (error) {
+            LOG(TextInput, "    ...characterIndexForPoint failed.");
+            completionHandlerBlock(0);
+            return;
+        }
+        if (result == notFound)
+            result = NSNotFound;
+        LOG(TextInput, "    -> characterIndexForPoint returned %lu", result);
+        completionHandlerBlock(result);
+    }));
 }
 
-- (BOOL)_tryHandlePluginComplexTextInputKeyDown:(NSEvent *)event
+- (NSTextInputContext *)inputContext
 {
-    if (!_data->_pluginComplexTextInputIdentifier || _data->_pluginComplexTextInputState == PluginComplexTextInputDisabled)
-        return NO;
+    bool collectingKeypressCommands = _data->_collectedKeypressCommands;
 
-    // Check if the text input window has been dismissed and let the plug-in process know.
-    // This is only valid with the updated Cocoa text input spec.
-    [self _disableComplexTextInputIfNecessary];
+    if (_data->_pluginComplexTextInputIdentifier && !collectingKeypressCommands)
+        return [[WKTextInputWindowController sharedTextInputWindowController] inputContext];
 
-    // Try feeding the keyboard event directly to the plug-in.
-    if (_data->_pluginComplexTextInputState == PluginComplexTextInputEnabledLegacy)
-        return [self _handlePluginComplexTextInputKeyDown:event];
+    // Disable text input machinery when in non-editable content. An invisible inline input area affects performance, and can prevent Expose from working.
+    if (!_data->_page->editorState().isContentEditable)
+        return nil;
 
-    return NO;
+    return [super inputContext];
 }
 
-- (void)keyDown:(NSEvent *)theEvent
+- (void)unmarkText
 {
-    LOG(TextInput, "keyDown:%p %@%s", theEvent, theEvent, (theEvent == _data->_keyDownEventBeingResent) ? " (re-sent)" : "");
-
-    // There's a chance that responding to this event will run a nested event loop, and
-    // fetching a new event might release the old one. Retaining and then autoreleasing
-    // the current event prevents that from causing a problem inside WebKit or AppKit code.
-    [[theEvent retain] autorelease];
+    LOG(TextInput, "unmarkText");
 
-    if ([self _tryHandlePluginComplexTextInputKeyDown:theEvent]) {
-        LOG(TextInput, "...handled by plug-in");
-        return;
-    }
+    _data->_page->confirmCompositionAsync();
+}
+
+- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange
+{
+    BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
+    ASSERT(isAttributedString || [string isKindOfClass:[NSString class]]);
+
+    LOG(TextInput, "setMarkedText:\"%@\" selectedRange:(%u, %u) replacementRange:(%u, %u)", isAttributedString ? [string string] : string, selectedRange.location, selectedRange.length, replacementRange.location, replacementRange.length);
+
+    Vector<CompositionUnderline> underlines;
+    NSString *text;
+
+    if (isAttributedString) {
+        // FIXME: We ignore most attributes from the string, so an input method cannot specify e.g. a font or a glyph variation.
+        text = [string string];
+        extractUnderlines(string, underlines);
+    } else
+        text = string;
+
+    if (_data->_inSecureInputState) {
+        // In password fields, we only allow ASCII dead keys, and don't allow inline input, matching NSSecureTextInputField.
+        // Allowing ASCII dead keys is necessary to enable full Roman input when using a Vietnamese keyboard.
+        ASSERT(!_data->_page->editorState().hasComposition);
+        [self _notifyInputContextAboutDiscardedComposition];
+        // FIXME: We should store the command to handle it after DOM event processing, as it's regular keyboard input now, not a composition.
+        if ([text length] == 1 && isASCII([text characterAtIndex:0]))
+            _data->_page->insertTextAsync(text, replacementRange);
+        else
+            NSBeep();
+        return;
+    }
+
+    _data->_page->setCompositionAsync(text, underlines, selectedRange, replacementRange);
+}
+
+// Synchronous NSTextInputClient is still implemented to catch spurious sync calls. Remove when that is no longer needed.
+
+- (NSRange)selectedRange NO_RETURN_DUE_TO_ASSERT
+{
+    ASSERT_NOT_REACHED();
+    return NSMakeRange(NSNotFound, 0);
+}
+
+- (BOOL)hasMarkedText NO_RETURN_DUE_TO_ASSERT
+{
+    ASSERT_NOT_REACHED();
+    return NO;
+}
+
+- (NSRange)markedRange NO_RETURN_DUE_TO_ASSERT
+{
+    ASSERT_NOT_REACHED();
+    return NSMakeRange(NSNotFound, 0);
+}
+
+- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)nsRange actualRange:(NSRangePointer)actualRange NO_RETURN_DUE_TO_ASSERT
+{
+    ASSERT_NOT_REACHED();
+    return nil;
+}
+
+- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint NO_RETURN_DUE_TO_ASSERT
+{
+    ASSERT_NOT_REACHED();
+    return 0;
+}
+
+- (NSRect)firstRectForCharacterRange:(NSRange)theRange actualRange:(NSRangePointer)actualRange NO_RETURN_DUE_TO_ASSERT
+{ 
+    ASSERT_NOT_REACHED();
+    return NSMakeRect(0, 0, 0, 0);
+}
+
+- (BOOL)performKeyEquivalent:(NSEvent *)event
+{
+    // There's a chance that responding to this event will run a nested event loop, and
+    // fetching a new event might release the old one. Retaining and then autoreleasing
+    // the current event prevents that from causing a problem inside WebKit or AppKit code.
+    [[event retain] autorelease];
+
+    // We get Esc key here after processing either Esc or Cmd+period. The former starts as a keyDown, and the latter starts as a key equivalent,
+    // but both get transformed to a cancelOperation: command, executing which passes an Esc key event to -performKeyEquivalent:.
+    // Don't interpret this event again, avoiding re-entrancy and infinite loops.
+    if ([[event charactersIgnoringModifiers] isEqualToString:@"\e"] && !([event modifierFlags] & NSDeviceIndependentModifierFlagsMask))
+        return [super performKeyEquivalent:event];
+
+    // If we are already re-sending the event, then WebCore has already seen it, no need for custom processing.
+    BOOL eventWasSentToWebCore = (_data->_keyDownEventBeingResent == event);
+    if (eventWasSentToWebCore)
+        return [super performKeyEquivalent:event];
+
+    ASSERT(event == [NSApp currentEvent]);
+
+    [self _disableComplexTextInputIfNecessary];
+
+    // Pass key combos through WebCore if there is a key binding available for
+    // this event. This lets web pages have a crack at intercepting key-modified keypresses.
+    // FIXME: Why is the firstResponder check needed?
+    if (self == [[self window] firstResponder]) {
+        [self _interpretKeyEvent:event completionHandler:^(BOOL handledByInputMethod, const Vector<KeypressCommand>& commands) {
+            _data->_page->handleKeyboardEvent(NativeWebKeyboardEvent(event, handledByInputMethod, commands));
+        }];
+        return YES;
+    }
+    
+    return [super performKeyEquivalent:event];
+}
+
+- (void)keyUp:(NSEvent *)theEvent
+{
+    LOG(TextInput, "keyUp:%p %@", theEvent, theEvent);
+
+    [self _interpretKeyEvent:theEvent completionHandler:^(BOOL handledByInputMethod, const Vector<KeypressCommand>& commands) {
+        ASSERT(!handledByInputMethod || commands.isEmpty());
+        _data->_page->handleKeyboardEvent(NativeWebKeyboardEvent(theEvent, handledByInputMethod, commands));
+    }];
+}
+
+- (void)keyDown:(NSEvent *)theEvent
+{
+    LOG(TextInput, "keyDown:%p %@%s", theEvent, theEvent, (theEvent == _data->_keyDownEventBeingResent) ? " (re-sent)" : "");
+
+    if ([self _tryHandlePluginComplexTextInputKeyDown:theEvent]) {
+        LOG(TextInput, "...handled by plug-in");
+        return;
+    }
 
     // We could be receiving a key down from AppKit if we have re-sent an event
     // that maps to an action that is currently unavailable (for example a copy when
@@ -1412,34 +1719,63 @@ NATIVE_MOUSE_EVENT_HANDLER(rightMouseUp)
         return;
     }
 
-    Vector<KeypressCommand> commands;
-    BOOL handledByInputMethod = [self _interpretKeyEvent:theEvent savingCommandsTo:commands];
-    if (!commands.isEmpty()) {
-        // An input method may make several actions per keypress. For example, pressing Return with Korean IM both confirms it and sends a newline.
-        // IM-like actions are handled immediately (so the return value from UI process is true), but there are saved commands that
-        // should be handled like normal text input after DOM event dispatch.
-        handledByInputMethod = NO;
-    }
-
-    _data->_page->handleKeyboardEvent(NativeWebKeyboardEvent(theEvent, handledByInputMethod, commands));
+    [self _interpretKeyEvent:theEvent completionHandler:^(BOOL handledByInputMethod, const Vector<KeypressCommand>& commands) {
+        ASSERT(!handledByInputMethod || commands.isEmpty());
+        _data->_page->handleKeyboardEvent(NativeWebKeyboardEvent(theEvent, handledByInputMethod, commands));
+    }];
 }
 
 - (void)flagsChanged:(NSEvent *)theEvent
 {
     LOG(TextInput, "flagsChanged:%p %@", theEvent, theEvent);
 
-    // There's a chance that responding to this event will run a nested event loop, and
-    // fetching a new event might release the old one. Retaining and then autoreleasing
-    // the current event prevents that from causing a problem inside WebKit or AppKit code.
-    [[theEvent retain] autorelease];
-
     unsigned short keyCode = [theEvent keyCode];
 
     // Don't make an event from the num lock and function keys
     if (!keyCode || keyCode == 10 || keyCode == 63)
         return;
 
-    _data->_page->handleKeyboardEvent(NativeWebKeyboardEvent(theEvent, false, Vector<KeypressCommand>()));
+    [self _interpretKeyEvent:theEvent completionHandler:^(BOOL handledByInputMethod, const Vector<KeypressCommand>& commands) {
+        _data->_page->handleKeyboardEvent(NativeWebKeyboardEvent(theEvent, handledByInputMethod, commands));
+    }];
+}
+
+#else // USE(ASYNC_NSTEXTINPUTCLIENT)
+
+- (BOOL)_interpretKeyEvent:(NSEvent *)event savingCommandsTo:(Vector<WebCore::KeypressCommand>&)commands
+{
+    ASSERT(!_data->_interpretKeyEventsParameters);
+    ASSERT(commands.isEmpty());
+
+    if ([event type] == NSFlagsChanged)
+        return NO;
+
+    WKViewInterpretKeyEventsParameters parameters;
+    parameters.eventInterpretationHadSideEffects = false;
+    parameters.executingSavedKeypressCommands = false;
+    // We assume that an input method has consumed the event, and only change this assumption if one of the NSTextInput methods is called.
+    // We assume the IM will *not* consume hotkey sequences.
+    parameters.consumedByIM = !([event modifierFlags] & NSCommandKeyMask);
+    parameters.commands = &commands;
+    _data->_interpretKeyEventsParameters = &parameters;
+
+    LOG(TextInput, "-> interpretKeyEvents:%p %@", event, event);
+    [self interpretKeyEvents:[NSArray arrayWithObject:event]];
+
+    _data->_interpretKeyEventsParameters = nullptr;
+
+    // An input method may consume an event and not tell us (e.g. when displaying a candidate window),
+    // in which case we should not bubble the event up the DOM.
+    if (parameters.consumedByIM) {
+        ASSERT(commands.isEmpty());
+        LOG(TextInput, "...event %p was consumed by an input method", event);
+        return YES;
+    }
+
+    LOG(TextInput, "...interpretKeyEvents for event %p done, returns %d", event, parameters.eventInterpretationHadSideEffects);
+
+    // If we have already executed all or some of the commands, the event is "handled". Note that there are additional checks on web process side.
+    return parameters.eventInterpretationHadSideEffects;
 }
 
 - (void)_executeSavedKeypressCommands
@@ -1463,18 +1799,89 @@ NATIVE_MOUSE_EVENT_HANDLER(rightMouseUp)
     LOG(TextInput, "...done executing saved keypress commands.");
 }
 
-- (NSTextInputContext *)inputContext
+- (void)doCommandBySelector:(SEL)selector
 {
+    LOG(TextInput, "doCommandBySelector:\"%s\"", sel_getName(selector));
+
     WKViewInterpretKeyEventsParameters* parameters = _data->_interpretKeyEventsParameters;
+    if (parameters)
+        parameters->consumedByIM = false;
 
-    if (_data->_pluginComplexTextInputIdentifier && !parameters)
-        return [[WKTextInputWindowController sharedTextInputWindowController] inputContext];
+    // As in insertText:replacementRange:, we assume that the call comes from an input method if there is marked text.
+    bool isFromInputMethod = _data->_page->editorState().hasComposition;
 
-    // Disable text input machinery when in non-editable content. An invisible inline input area affects performance, and can prevent Expose from working.
-    if (!_data->_page->editorState().isContentEditable)
-        return nil;
+    if (parameters && !isFromInputMethod) {
+        KeypressCommand command(NSStringFromSelector(selector));
+        parameters->commands->append(command);
+        LOG(TextInput, "...stored");
+        _data->_page->registerKeypressCommandName(command.commandName);
+    } else {
+        // FIXME: Send the command to Editor synchronously and only send it along the
+        // responder chain if it's a selector that does not correspond to an editing command.
+        [super doCommandBySelector:selector];
+    }
+}
 
-    return [super inputContext];
+- (void)insertText:(id)string
+{
+    // Unlike an NSTextInputClient variant with replacementRange, this NSResponder method is called when there is no input context,
+    // so text input processing isn't performed. We are not going to actually insert any text in that case, but saving an insertText
+    // command ensures that a keypress event is dispatched as appropriate.
+    [self insertText:string replacementRange:NSMakeRange(NSNotFound, 0)];
+}
+
+- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
+{
+    BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
+    ASSERT(isAttributedString || [string isKindOfClass:[NSString class]]);
+
+    if (replacementRange.location != NSNotFound)
+        LOG(TextInput, "insertText:\"%@\" replacementRange:(%u, %u)", isAttributedString ? [string string] : string, replacementRange.location, replacementRange.length);
+    else
+        LOG(TextInput, "insertText:\"%@\"", isAttributedString ? [string string] : string);
+    WKViewInterpretKeyEventsParameters* parameters = _data->_interpretKeyEventsParameters;
+    if (parameters)
+        parameters->consumedByIM = false;
+
+    NSString *text;
+    bool isFromInputMethod = _data->_page->editorState().hasComposition;
+
+    Vector<TextAlternativeWithRange> dictationAlternatives;
+
+    if (isAttributedString) {
+#if USE(DICTATION_ALTERNATIVES)
+        collectDictationTextAlternatives(string, dictationAlternatives);
+#endif
+        // FIXME: We ignore most attributes from the string, so for example inserting from Character Palette loses font and glyph variation data.
+        text = [string string];
+    } else
+        text = string;
+
+    // insertText can be called for several reasons:
+    // - If it's from normal key event processing (including key bindings), we may need to save the action to perform it later.
+    // - If it's from an input method, then we should go ahead and insert the text now. We assume it's from the input method if we have marked text.
+    // FIXME: In theory, this could be wrong for some input methods, so we should try to find another way to determine if the call is from the input method.
+    // - If it's sent outside of keyboard event processing (e.g. from Character Viewer, or when confirming an inline input area with a mouse),
+    // then we also execute it immediately, as there will be no other chance.
+    if (parameters && !isFromInputMethod) {
+        // FIXME: Handle replacementRange in this case, too. It's known to occur in practice when canceling Press and Hold (see <rdar://11940670>).
+        ASSERT(replacementRange.location == NSNotFound);
+        KeypressCommand command("insertText:", text);
+        parameters->commands->append(command);
+        _data->_page->registerKeypressCommandName(command.commandName);
+        return;
+    }
+
+    String eventText = text;
+    eventText.replace(NSBackTabCharacter, NSTabCharacter); // same thing is done in KeyEventMac.mm in WebCore
+    bool eventHandled;
+    if (!dictationAlternatives.isEmpty())
+        eventHandled = _data->_page->insertDictatedText(eventText, replacementRange, dictationAlternatives);
+    else
+        eventHandled = _data->_page->insertText(eventText, replacementRange);
+
+    if (parameters)
+        parameters->eventInterpretationHadSideEffects |= eventHandled;
 }
 
 - (NSRange)selectedRange
@@ -1532,48 +1939,6 @@ NATIVE_MOUSE_EVENT_HANDLER(rightMouseUp)
     _data->_page->confirmComposition();
 }
 
-- (NSArray *)validAttributesForMarkedText
-{
-    static NSArray *validAttributes;
-    if (!validAttributes) {
-        validAttributes = [[NSArray alloc] initWithObjects:
-                           NSUnderlineStyleAttributeName, NSUnderlineColorAttributeName,
-                           NSMarkedClauseSegmentAttributeName,
-#if USE(DICTATION_ALTERNATIVES)
-                           NSTextAlternativesAttributeName,
-#endif
-                           nil];
-        // NSText also supports the following attributes, but it's
-        // hard to tell which are really required for text input to
-        // work well; I have not seen any input method make use of them yet.
-        //     NSFontAttributeName, NSForegroundColorAttributeName,
-        //     NSBackgroundColorAttributeName, NSLanguageAttributeName.
-        CFRetain(validAttributes);
-    }
-    LOG(TextInput, "validAttributesForMarkedText -> (...)");
-    return validAttributes;
-}
-
-static void extractUnderlines(NSAttributedString *string, Vector<CompositionUnderline>& result)
-{
-    int length = [[string string] length];
-    
-    int i = 0;
-    while (i < length) {
-        NSRange range;
-        NSDictionary *attrs = [string attributesAtIndex:i longestEffectiveRange:&range inRange:NSMakeRange(i, length - i)];
-        
-        if (NSNumber *style = [attrs objectForKey:NSUnderlineStyleAttributeName]) {
-            Color color = Color::black;
-            if (NSColor *colorAttr = [attrs objectForKey:NSUnderlineColorAttributeName])
-                color = colorFromNSColor([colorAttr colorUsingColorSpaceName:NSDeviceRGBColorSpace]);
-            result.append(CompositionUnderline(range.location, NSMaxRange(range), color, [style intValue] > 1));
-        }
-        
-        i = range.location + range.length;
-    }
-}
-
 - (void)setMarkedText:(id)string selectedRange:(NSRange)newSelectedRange replacementRange:(NSRange)replacementRange
 {
     [self _executeSavedKeypressCommands];
@@ -1699,6 +2064,125 @@ static void extractUnderlines(NSAttributedString *string, Vector<CompositionUnde
     return resultRect;
 }
 
+- (BOOL)performKeyEquivalent:(NSEvent *)event
+{
+    // There's a chance that responding to this event will run a nested event loop, and
+    // fetching a new event might release the old one. Retaining and then autoreleasing
+    // the current event prevents that from causing a problem inside WebKit or AppKit code.
+    [[event retain] autorelease];
+
+    // We get Esc key here after processing either Esc or Cmd+period. The former starts as a keyDown, and the latter starts as a key equivalent,
+    // but both get transformed to a cancelOperation: command, executing which passes an Esc key event to -performKeyEquivalent:.
+    // Don't interpret this event again, avoiding re-entrancy and infinite loops.
+    if ([[event charactersIgnoringModifiers] isEqualToString:@"\e"] && !([event modifierFlags] & NSDeviceIndependentModifierFlagsMask))
+        return [super performKeyEquivalent:event];
+
+    // If we are already re-sending the event, then WebCore has already seen it, no need for custom processing.
+    BOOL eventWasSentToWebCore = (_data->_keyDownEventBeingResent == event);
+    if (eventWasSentToWebCore)
+        return [super performKeyEquivalent:event];
+
+    ASSERT(event == [NSApp currentEvent]);
+
+    [self _disableComplexTextInputIfNecessary];
+
+    // Pass key combos through WebCore if there is a key binding available for
+    // this event. This lets web pages have a crack at intercepting key-modified keypresses.
+    // FIXME: Why is the firstResponder check needed?
+    if (self == [[self window] firstResponder]) {
+        Vector<KeypressCommand> commands;
+        BOOL handledByInputMethod = [self _interpretKeyEvent:event savingCommandsTo:commands];
+        _data->_page->handleKeyboardEvent(NativeWebKeyboardEvent(event, handledByInputMethod, commands));
+        return YES;
+    }
+    
+    return [super performKeyEquivalent:event];
+}
+
+- (void)keyUp:(NSEvent *)theEvent
+{
+    LOG(TextInput, "keyUp:%p %@", theEvent, theEvent);
+    // We don't interpret the keyUp event, as this breaks key bindings (see <https://bugs.webkit.org/show_bug.cgi?id=130100>).
+    _data->_page->handleKeyboardEvent(NativeWebKeyboardEvent(theEvent, false, Vector<KeypressCommand>()));
+}
+
+- (void)keyDown:(NSEvent *)theEvent
+{
+    LOG(TextInput, "keyDown:%p %@%s", theEvent, theEvent, (theEvent == _data->_keyDownEventBeingResent) ? " (re-sent)" : "");
+
+    // There's a chance that responding to this event will run a nested event loop, and
+    // fetching a new event might release the old one. Retaining and then autoreleasing
+    // the current event prevents that from causing a problem inside WebKit or AppKit code.
+    [[theEvent retain] autorelease];
+
+    if ([self _tryHandlePluginComplexTextInputKeyDown:theEvent]) {
+        LOG(TextInput, "...handled by plug-in");
+        return;
+    }
+
+    // We could be receiving a key down from AppKit if we have re-sent an event
+    // that maps to an action that is currently unavailable (for example a copy when
+    // there is no range selection).
+    // If this is the case we should ignore the key down.
+    if (_data->_keyDownEventBeingResent == theEvent) {
+        [super keyDown:theEvent];
+        return;
+    }
+
+    Vector<KeypressCommand> commands;
+    BOOL handledByInputMethod = [self _interpretKeyEvent:theEvent savingCommandsTo:commands];
+    if (!commands.isEmpty()) {
+        // An input method may make several actions per keypress. For example, pressing Return with Korean IM both confirms it and sends a newline.
+        // IM-like actions are handled immediately (so the return value from UI process is true), but there are saved commands that
+        // should be handled like normal text input after DOM event dispatch.
+        handledByInputMethod = NO;
+    }
+
+    _data->_page->handleKeyboardEvent(NativeWebKeyboardEvent(theEvent, handledByInputMethod, commands));
+}
+
+- (void)flagsChanged:(NSEvent *)theEvent
+{
+    LOG(TextInput, "flagsChanged:%p %@", theEvent, theEvent);
+
+    // There's a chance that responding to this event will run a nested event loop, and
+    // fetching a new event might release the old one. Retaining and then autoreleasing
+    // the current event prevents that from causing a problem inside WebKit or AppKit code.
+    [[theEvent retain] autorelease];
+
+    unsigned short keyCode = [theEvent keyCode];
+
+    // Don't make an event from the num lock and function keys
+    if (!keyCode || keyCode == 10 || keyCode == 63)
+        return;
+
+    _data->_page->handleKeyboardEvent(NativeWebKeyboardEvent(theEvent, false, Vector<KeypressCommand>()));
+}
+
+#endif // USE(ASYNC_NSTEXTINPUTCLIENT)
+
+- (NSArray *)validAttributesForMarkedText
+{
+    static NSArray *validAttributes;
+    if (!validAttributes) {
+        validAttributes = [[NSArray alloc] initWithObjects:
+                           NSUnderlineStyleAttributeName, NSUnderlineColorAttributeName,
+                           NSMarkedClauseSegmentAttributeName,
+#if USE(DICTATION_ALTERNATIVES)
+                           NSTextAlternativesAttributeName,
+#endif
+                           nil];
+        // NSText also supports the following attributes, but it's
+        // hard to tell which are really required for text input to
+        // work well; I have not seen any input method make use of them yet.
+        //     NSFontAttributeName, NSForegroundColorAttributeName,
+        //     NSBackgroundColorAttributeName, NSLanguageAttributeName.
+        CFRetain(validAttributes);
+    }
+    LOG(TextInput, "validAttributesForMarkedText -> (...)");
+    return validAttributes;
+}
+
 #if ENABLE(DRAG_SUPPORT)
 - (void)draggedImage:(NSImage *)anImage endedAt:(NSPoint)aPoint operation:(NSDragOperation)operation
 {
@@ -2319,42 +2803,6 @@ static void createSandboxExtensionsForFileUpload(NSPasteboard *pasteboard, Sandb
     _data->_keyDownEventBeingResent = nullptr;
 }
 
-- (BOOL)_interpretKeyEvent:(NSEvent *)event savingCommandsTo:(Vector<WebCore::KeypressCommand>&)commands
-{
-    ASSERT(!_data->_interpretKeyEventsParameters);
-    ASSERT(commands.isEmpty());
-
-    if ([event type] == NSFlagsChanged)
-        return NO;
-
-    WKViewInterpretKeyEventsParameters parameters;
-    parameters.eventInterpretationHadSideEffects = false;
-    parameters.executingSavedKeypressCommands = false;
-    // We assume that an input method has consumed the event, and only change this assumption if one of the NSTextInput methods is called.
-    // We assume the IM will *not* consume hotkey sequences.
-    parameters.consumedByIM = !([event modifierFlags] & NSCommandKeyMask);
-    parameters.commands = &commands;
-    _data->_interpretKeyEventsParameters = &parameters;
-
-    LOG(TextInput, "-> interpretKeyEvents:%p %@", event, event);
-    [self interpretKeyEvents:[NSArray arrayWithObject:event]];
-
-    _data->_interpretKeyEventsParameters = nullptr;
-
-    // An input method may consume an event and not tell us (e.g. when displaying a candidate window),
-    // in which case we should not bubble the event up the DOM.
-    if (parameters.consumedByIM) {
-        ASSERT(commands.isEmpty());
-        LOG(TextInput, "...event %p was consumed by an input method", event);
-        return YES;
-    }
-
-    LOG(TextInput, "...interpretKeyEvents for event %p done, returns %d", event, parameters.eventInterpretationHadSideEffects);
-
-    // If we have already executed all or some of the commands, the event is "handled". Note that there are additional checks on web process side.
-    return parameters.eventInterpretationHadSideEffects;
-}
-
 - (NSRect)_convertToDeviceSpace:(NSRect)rect
 {
     return toDeviceSpace(rect, [self window]);
index 34ce64d..4bccfbf 100644 (file)
@@ -72,7 +72,6 @@ struct WebPageConfiguration;
 - (void)_toolTipChangedFrom:(NSString *)oldToolTip to:(NSString *)newToolTip;
 - (void)_setCursor:(NSCursor *)cursor;
 - (void)_setUserInterfaceItemState:(NSString *)commandName enabled:(BOOL)isEnabled state:(int)newState;
-- (BOOL)_interpretKeyEvent:(NSEvent *)theEvent savingCommandsTo:(Vector<WebCore::KeypressCommand>&)commands;
 - (void)_doneWithKeyEvent:(NSEvent *)event eventWasHandled:(BOOL)eventWasHandled;
 - (bool)_executeSavedCommandBySelector:(SEL)selector;
 - (void)_setIntrinsicContentSize:(NSSize)intrinsicContentSize;
index 1a498da..0575c43 100644 (file)
@@ -1091,7 +1091,7 @@ void WebPageProxy::validateCommand(const String& commandName, PassRefPtr<Validat
     }
 
     uint64_t callbackID = callback->callbackID();
-    m_validateCommandCallbacks.set(callbackID, callback.get());
+    m_validateCommandCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::ValidateCommand(commandName, callbackID), m_pageID);
 }
 
@@ -1946,7 +1946,7 @@ void WebPageProxy::runJavaScriptInMainFrame(const String& script, PassRefPtr<Scr
     }
 
     uint64_t callbackID = callback->callbackID();
-    m_scriptValueCallbacks.set(callbackID, callback.get());
+    m_scriptValueCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::RunJavaScriptInMainFrame(script, callbackID), m_pageID);
 }
 
@@ -1959,7 +1959,7 @@ void WebPageProxy::getRenderTreeExternalRepresentation(PassRefPtr<StringCallback
     }
     
     uint64_t callbackID = callback->callbackID();
-    m_stringCallbacks.set(callbackID, callback.get());
+    m_stringCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::GetRenderTreeExternalRepresentation(callbackID), m_pageID);
 }
 
@@ -1973,7 +1973,7 @@ void WebPageProxy::getSourceForFrame(WebFrameProxy* frame, PassRefPtr<StringCall
     
     uint64_t callbackID = callback->callbackID();
     m_loadDependentStringCallbackIDs.add(callbackID);
-    m_stringCallbacks.set(callbackID, callback.get());
+    m_stringCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::GetSourceForFrame(frame->frameID(), callbackID), m_pageID);
 }
 
@@ -1987,7 +1987,7 @@ void WebPageProxy::getContentsAsString(PassRefPtr<StringCallback> prpCallback)
     
     uint64_t callbackID = callback->callbackID();
     m_loadDependentStringCallbackIDs.add(callbackID);
-    m_stringCallbacks.set(callbackID, callback.get());
+    m_stringCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::GetContentsAsString(callbackID), m_pageID);
 }
 
@@ -2001,7 +2001,7 @@ void WebPageProxy::getBytecodeProfile(PassRefPtr<StringCallback> prpCallback)
     
     uint64_t callbackID = callback->callbackID();
     m_loadDependentStringCallbackIDs.add(callbackID);
-    m_stringCallbacks.set(callbackID, callback.get());
+    m_stringCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::GetBytecodeProfile(callbackID), m_pageID);
 }
     
@@ -2015,7 +2015,7 @@ void WebPageProxy::getContentsAsMHTMLData(PassRefPtr<DataCallback> prpCallback,
     }
 
     uint64_t callbackID = callback->callbackID();
-    m_dataCallbacks.set(callbackID, callback.get());
+    m_dataCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::GetContentsAsMHTMLData(callbackID, useBinaryEncoding), m_pageID);
 }
 #endif
@@ -2029,7 +2029,7 @@ void WebPageProxy::getSelectionOrContentsAsString(PassRefPtr<StringCallback> prp
     }
     
     uint64_t callbackID = callback->callbackID();
-    m_stringCallbacks.set(callbackID, callback.get());
+    m_stringCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::GetSelectionOrContentsAsString(callbackID), m_pageID);
 }
 
@@ -2042,7 +2042,7 @@ void WebPageProxy::getSelectionAsWebArchiveData(PassRefPtr<DataCallback> prpCall
     }
     
     uint64_t callbackID = callback->callbackID();
-    m_dataCallbacks.set(callbackID, callback.get());
+    m_dataCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::GetSelectionAsWebArchiveData(callbackID), m_pageID);
 }
 
@@ -2055,7 +2055,7 @@ void WebPageProxy::getMainResourceDataOfFrame(WebFrameProxy* frame, PassRefPtr<D
     }
     
     uint64_t callbackID = callback->callbackID();
-    m_dataCallbacks.set(callbackID, callback.get());
+    m_dataCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::GetMainResourceDataOfFrame(frame->frameID(), callbackID), m_pageID);
 }
 
@@ -2068,7 +2068,7 @@ void WebPageProxy::getResourceDataFromFrame(WebFrameProxy* frame, API::URL* reso
     }
     
     uint64_t callbackID = callback->callbackID();
-    m_dataCallbacks.set(callbackID, callback.get());
+    m_dataCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::GetResourceDataFromFrame(frame->frameID(), resourceURL->string(), callbackID), m_pageID);
 }
 
@@ -2081,7 +2081,7 @@ void WebPageProxy::getWebArchiveOfFrame(WebFrameProxy* frame, PassRefPtr<DataCal
     }
     
     uint64_t callbackID = callback->callbackID();
-    m_dataCallbacks.set(callbackID, callback.get());
+    m_dataCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::GetWebArchiveOfFrame(frame->frameID(), callbackID), m_pageID);
 }
 
@@ -2094,7 +2094,7 @@ void WebPageProxy::forceRepaint(PassRefPtr<VoidCallback> prpCallback)
     }
 
     uint64_t callbackID = callback->callbackID();
-    m_voidCallbacks.set(callbackID, callback.get());
+    m_voidCallbacks.set(callbackID, callback);
     m_drawingArea->waitForBackingStoreUpdateOnNextPaint();
     m_process->send(Messages::WebPage::ForceRepaint(callbackID), m_pageID); 
 }
@@ -3791,6 +3791,46 @@ void WebPageProxy::validateCommandCallback(const String& commandName, bool isEna
     callback->performCallbackWithReturnValue(commandName.impl(), isEnabled, state);
 }
 
+void WebPageProxy::unsignedCallback(uint64_t result, uint64_t callbackID)
+{
+    RefPtr<UnsignedCallback> callback = m_unsignedCallbacks.take(callbackID);
+    if (!callback) {
+        // FIXME: Log error or assert.
+        // this can validly happen if a load invalidated the callback, though
+        return;
+    }
+
+    callback->performCallbackWithReturnValue(result);
+}
+
+void WebPageProxy::editingRangeCallback(const EditingRange& range, uint64_t callbackID)
+{
+    MESSAGE_CHECK(range.isValid());
+
+    RefPtr<EditingRangeCallback> callback = m_editingRangeCallbacks.take(callbackID);
+    if (!callback) {
+        // FIXME: Log error or assert.
+        // this can validly happen if a load invalidated the callback, though
+        return;
+    }
+
+    callback->performCallbackWithReturnValue(range);
+}
+
+void WebPageProxy::rectForCharacterRangeCallback(const IntRect& rect, const EditingRange& actualRange, uint64_t callbackID)
+{
+    MESSAGE_CHECK(actualRange.isValid());
+
+    RefPtr<RectForCharacterRangeCallback> callback = m_rectForCharacterRangeCallbacks.take(callbackID);
+    if (!callback) {
+        // FIXME: Log error or assert.
+        // this can validly happen if a load invalidated the callback, though
+        return;
+    }
+
+    callback->performCallbackWithReturnValue(rect, actualRange);
+}
+
 #if PLATFORM(GTK)
 void WebPageProxy::printFinishedCallback(const ResourceError& printError, uint64_t callbackID)
 {
@@ -3933,6 +3973,12 @@ void WebPageProxy::resetState()
     invalidateCallbackMap(m_scriptValueCallbacks);
     invalidateCallbackMap(m_computedPagesCallbacks);
     invalidateCallbackMap(m_validateCommandCallbacks);
+    invalidateCallbackMap(m_unsignedCallbacks);
+    invalidateCallbackMap(m_editingRangeCallbacks);
+    invalidateCallbackMap(m_rectForCharacterRangeCallbacks);
+#if PLATFORM(MAC)
+    invalidateCallbackMap(m_attributedStringForCharacterRangeCallbacks);
+#endif
 #if PLATFORM(IOS)
     invalidateCallbackMap(m_gestureCallbacks);
     invalidateCallbackMap(m_touchesCallbacks);
@@ -4340,7 +4386,7 @@ void WebPageProxy::computePagesForPrinting(WebFrameProxy* frame, const PrintInfo
     }
 
     uint64_t callbackID = callback->callbackID();
-    m_computedPagesCallbacks.set(callbackID, callback.get());
+    m_computedPagesCallbacks.set(callbackID, callback);
     m_isInPrintingMode = true;
     m_process->send(Messages::WebPage::ComputePagesForPrinting(frame->frameID(), printInfo, callbackID), m_pageID, m_isPerformingDOMPrintOperation ? IPC::DispatchMessageEvenWhenWaitingForSyncReply : 0);
 }
@@ -4355,7 +4401,7 @@ void WebPageProxy::drawRectToImage(WebFrameProxy* frame, const PrintInfo& printI
     }
     
     uint64_t callbackID = callback->callbackID();
-    m_imageCallbacks.set(callbackID, callback.get());
+    m_imageCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::DrawRectToImage(frame->frameID(), printInfo, rect, imageSize, callbackID), m_pageID, m_isPerformingDOMPrintOperation ? IPC::DispatchMessageEvenWhenWaitingForSyncReply : 0);
 }
 
@@ -4368,7 +4414,7 @@ void WebPageProxy::drawPagesToPDF(WebFrameProxy* frame, const PrintInfo& printIn
     }
     
     uint64_t callbackID = callback->callbackID();
-    m_dataCallbacks.set(callbackID, callback.get());
+    m_dataCallbacks.set(callbackID, callback);
     m_process->send(Messages::WebPage::DrawPagesToPDF(frame->frameID(), printInfo, first, count, callbackID), m_pageID, m_isPerformingDOMPrintOperation ? IPC::DispatchMessageEvenWhenWaitingForSyncReply : 0);
 }
 #elif PLATFORM(GTK)
@@ -4381,7 +4427,7 @@ void WebPageProxy::drawPagesForPrinting(WebFrameProxy* frame, const PrintInfo& p
     }
 
     uint64_t callbackID = callback->callbackID();
-    m_printFinishedCallbacks.set(callbackID, callback.get());
+    m_printFinishedCallbacks.set(callbackID, callback);
     m_isInPrintingMode = true;
     m_process->send(Messages::WebPage::DrawPagesForPrinting(frame->frameID(), printInfo, callbackID), m_pageID, m_isPerformingDOMPrintOperation ? IPC::DispatchMessageEvenWhenWaitingForSyncReply : 0);
 }
@@ -4600,6 +4646,89 @@ void WebPageProxy::addMIMETypeWithCustomContentProvider(const String& mimeType)
     m_process->send(Messages::WebPage::AddMIMETypeWithCustomContentProvider(mimeType), m_pageID);
 }
 
+#if PLATFORM(COCOA)
+
+void WebPageProxy::insertTextAsync(const String& text, const EditingRange& replacementRange)
+{
+    if (!isValid())
+        return;
+
+    process().send(Messages::WebPage::InsertTextAsync(text, replacementRange), m_pageID);
+}
+
+void WebPageProxy::getMarkedRangeAsync(PassRefPtr<EditingRangeCallback> callback)
+{
+    if (!isValid()) {
+        callback->invalidate();
+        return;
+    }
+
+    uint64_t callbackID = callback->callbackID();
+    m_editingRangeCallbacks.set(callbackID, callback);
+
+    process().send(Messages::WebPage::GetMarkedRangeAsync(callbackID), m_pageID);
+}
+
+void WebPageProxy::getSelectedRangeAsync(PassRefPtr<EditingRangeCallback> callback)
+{
+    if (!isValid()) {
+        callback->invalidate();
+        return;
+    }
+
+    uint64_t callbackID = callback->callbackID();
+    m_editingRangeCallbacks.set(callbackID, callback);
+
+    process().send(Messages::WebPage::GetSelectedRangeAsync(callbackID), m_pageID);
+}
+
+void WebPageProxy::characterIndexForPointAsync(const WebCore::IntPoint& point, PassRefPtr<UnsignedCallback> callback)
+{
+    if (!isValid()) {
+        callback->invalidate();
+        return;
+    }
+
+    uint64_t callbackID = callback->callbackID();
+    m_unsignedCallbacks.set(callbackID, callback);
+
+    process().send(Messages::WebPage::CharacterIndexForPointAsync(point, callbackID), m_pageID);
+}
+
+void WebPageProxy::firstRectForCharacterRangeAsync(const EditingRange& range, PassRefPtr<RectForCharacterRangeCallback> callback)
+{
+    if (!isValid()) {
+        callback->invalidate();
+        return;
+    }
+
+    uint64_t callbackID = callback->callbackID();
+    m_rectForCharacterRangeCallbacks.set(callbackID, callback);
+
+    process().send(Messages::WebPage::FirstRectForCharacterRangeAsync(range, callbackID), m_pageID);
+}
+
+void WebPageProxy::setCompositionAsync(const String& text, Vector<CompositionUnderline> underlines, const EditingRange& selectionRange, const EditingRange& replacementRange)
+{
+    if (!isValid()) {
+        // If this fails, we should call -discardMarkedText on input context to notify the input method.
+        // This will happen naturally later, as part of reloading the page.
+        return;
+    }
+
+    process().send(Messages::WebPage::SetCompositionAsync(text, underlines, selectionRange, replacementRange), m_pageID);
+}
+
+void WebPageProxy::confirmCompositionAsync()
+{
+    if (!isValid())
+        return;
+
+    process().send(Messages::WebPage::ConfirmCompositionAsync(), m_pageID);
+}
+
+#endif
+
 void WebPageProxy::takeSnapshot(IntRect rect, IntSize bitmapSize, SnapshotOptions options, ImageCallback::CallbackFunction callbackFunction)
 {
     RefPtr<ImageCallback> callback = ImageCallback::create(callbackFunction);
index 201a927..51e90dc 100644 (file)
@@ -34,6 +34,7 @@
 #include "ContextMenuContextData.h"
 #include "DragControllerAction.h"
 #include "DrawingAreaProxy.h"
+#include "EditingRange.h"
 #include "EditorState.h"
 #include "GeolocationPermissionRequestManagerProxy.h"
 #include "InteractionInformationAtPosition.h"
 #include "LayerRepresentation.h"
 #endif
 
+#if PLATFORM(MAC)
+#include "AttributedString.h"
+#endif
+
 namespace API {
 class LoaderClient;
 class PolicyClient;
@@ -170,6 +175,8 @@ struct WebPopupItem;
 class WebVibrationProxy;
 #endif
 
+typedef GenericCallback<uint64_t> UnsignedCallback;
+typedef GenericCallback<EditingRange> EditingRangeCallback;
 typedef GenericCallback<StringImpl*> StringCallback;
 typedef GenericCallback<WebSerializedScriptValue*> ScriptValueCallback;
 
@@ -231,6 +238,96 @@ private:
     CallbackFunction m_callback;
 };
 
+// FIXME: Make a version of CallbackBase with two arguments, and define RectForCharacterRangeCallback as a specialization.
+class RectForCharacterRangeCallback : public CallbackBase {
+public:
+    typedef std::function<void (bool, const WebCore::IntRect&, const EditingRange&)> CallbackFunction;
+
+    static PassRefPtr<RectForCharacterRangeCallback> create(CallbackFunction callback)
+    {
+        return adoptRef(new RectForCharacterRangeCallback(callback));
+    }
+
+    virtual ~RectForCharacterRangeCallback()
+    {
+        ASSERT(!m_callback);
+    }
+
+    void performCallbackWithReturnValue(const WebCore::IntRect& rect, const EditingRange& range)
+    {
+        ASSERT(m_callback);
+
+        m_callback(false, rect, range);
+
+        m_callback = 0;
+    }
+    
+    void invalidate()
+    {
+        ASSERT(m_callback);
+
+        m_callback(true, WebCore::IntRect(), EditingRange());
+
+        m_callback = 0;
+    }
+
+private:
+
+    RectForCharacterRangeCallback(CallbackFunction callback)
+        : m_callback(callback)
+    {
+    }
+
+    CallbackFunction m_callback;
+};
+
+#if PLATFORM(MAC)
+
+// FIXME: Make a version of CallbackBase with two arguments, and define AttributedStringForCharacterRangeCallback as a specialization.
+class AttributedStringForCharacterRangeCallback : public CallbackBase {
+public:
+    typedef std::function<void (bool, const AttributedString&, const EditingRange&)> CallbackFunction;
+
+    static PassRefPtr<AttributedStringForCharacterRangeCallback> create(CallbackFunction callback)
+    {
+        return adoptRef(new AttributedStringForCharacterRangeCallback(callback));
+    }
+
+    virtual ~AttributedStringForCharacterRangeCallback()
+    {
+        ASSERT(!m_callback);
+    }
+
+    void performCallbackWithReturnValue(const AttributedString& string, const EditingRange& range)
+    {
+        ASSERT(m_callback);
+
+        m_callback(false, string, range);
+
+        m_callback = 0;
+    }
+    
+    void invalidate()
+    {
+        ASSERT(m_callback);
+
+        m_callback(true, AttributedString(), EditingRange());
+
+        m_callback = 0;
+    }
+
+private:
+
+    AttributedStringForCharacterRangeCallback(CallbackFunction callback)
+        : m_callback(callback)
+    {
+    }
+
+    CallbackFunction m_callback;
+};
+
+#endif
+
 #if PLATFORM(IOS)
 class GestureCallback : public CallbackBase {
 public:
@@ -535,10 +632,8 @@ public:
     void confirmComposition();
     void cancelComposition();
     bool insertText(const String& text, const EditingRange& replacementRange);
-    bool insertDictatedText(const String& text, const EditingRange& replacementRange, const Vector<WebCore::TextAlternativeWithRange>& dictationAlternatives);
     void getMarkedRange(EditingRange&);
     void getSelectedRange(EditingRange&);
-    void getAttributedSubstringFromRange(const EditingRange&, AttributedString&);
     uint64_t characterIndexForPoint(const WebCore::IntPoint);
     WebCore::IntRect firstRectForCharacterRange(const EditingRange&);
     bool executeKeypressCommands(const Vector<WebCore::KeypressCommand>&);
@@ -550,7 +645,20 @@ public:
     void setAcceleratedCompositingRootLayer(LayerOrView*);
     LayerOrView* acceleratedCompositingRootLayer() const;
 
-#if USE(APPKIT)
+    void insertTextAsync(const String& text, const EditingRange& replacementRange);
+    void getMarkedRangeAsync(PassRefPtr<EditingRangeCallback>);
+    void getSelectedRangeAsync(PassRefPtr<EditingRangeCallback>);
+    void characterIndexForPointAsync(const WebCore::IntPoint&, PassRefPtr<UnsignedCallback>);
+    void firstRectForCharacterRangeAsync(const EditingRange&, PassRefPtr<RectForCharacterRangeCallback>);
+    void setCompositionAsync(const String& text, Vector<WebCore::CompositionUnderline> underlines, const EditingRange& selectionRange, const EditingRange& replacementRange);
+    void confirmCompositionAsync();
+
+#if PLATFORM(MAC)
+    bool insertDictatedText(const String& text, const EditingRange& replacementRange, const Vector<WebCore::TextAlternativeWithRange>& dictationAlternatives);
+    void insertDictatedTextAsync(const String& text, const EditingRange& replacementRange, const Vector<WebCore::TextAlternativeWithRange>& dictationAlternatives);
+    void getAttributedSubstringFromRange(const EditingRange&, AttributedString&);
+    void attributedSubstringForCharacterRangeAsync(const EditingRange&, PassRefPtr<AttributedStringForCharacterRangeCallback>);
+
     WKView* wkView() const;
     void intrinsicContentSizeDidChange(const WebCore::IntSize& intrinsicContentSize);
 #endif
@@ -1155,6 +1263,12 @@ private:
     void scriptValueCallback(const IPC::DataReference&, uint64_t);
     void computedPagesCallback(const Vector<WebCore::IntRect>&, double totalScaleFactorForPrinting, uint64_t);
     void validateCommandCallback(const String&, bool, int, uint64_t);
+    void unsignedCallback(uint64_t, uint64_t);
+    void editingRangeCallback(const EditingRange&, uint64_t);
+    void rectForCharacterRangeCallback(const WebCore::IntRect&, const EditingRange&, uint64_t);
+#if PLATFORM(MAC)
+    void attributedStringForCharacterRangeCallback(const AttributedString&, const EditingRange&, uint64_t);
+#endif
 #if PLATFORM(IOS)
     void gestureCallback(const WebCore::IntPoint&, uint32_t, uint32_t, uint32_t, uint64_t);
     void touchesCallback(const WebCore::IntPoint&, uint32_t, uint64_t);
@@ -1292,6 +1406,12 @@ private:
     HashMap<uint64_t, RefPtr<ScriptValueCallback>> m_scriptValueCallbacks;
     HashMap<uint64_t, RefPtr<ComputedPagesCallback>> m_computedPagesCallbacks;
     HashMap<uint64_t, RefPtr<ValidateCommandCallback>> m_validateCommandCallbacks;
+    HashMap<uint64_t, RefPtr<UnsignedCallback>> m_unsignedCallbacks;
+    HashMap<uint64_t, RefPtr<EditingRangeCallback>> m_editingRangeCallbacks;
+    HashMap<uint64_t, RefPtr<RectForCharacterRangeCallback>> m_rectForCharacterRangeCallbacks;
+#if PLATFORM(MAC)
+    HashMap<uint64_t, RefPtr<AttributedStringForCharacterRangeCallback>> m_attributedStringForCharacterRangeCallbacks;
+#endif
 #if PLATFORM(IOS)
     HashMap<uint64_t, RefPtr<GestureCallback>> m_gestureCallbacks;
     HashMap<uint64_t, RefPtr<TouchesCallback>> m_touchesCallbacks;
index ee859e8..850909a 100644 (file)
@@ -151,6 +151,12 @@ messages -> WebPageProxy {
     ScriptValueCallback(IPC::DataReference resultData, uint64_t callbackID)
     ComputedPagesCallback(Vector<WebCore::IntRect> pageRects, double totalScaleFactorForPrinting, uint64_t callbackID)
     ValidateCommandCallback(String command, bool isEnabled, int32_t state, uint64_t callbackID)
+    EditingRangeCallback(WebKit::EditingRange range, uint64_t callbackID)
+    UnsignedCallback(uint64_t result, uint64_t callbackID)
+    RectForCharacterRangeCallback(WebCore::IntRect rect, WebKit::EditingRange actualRange, uint64_t callbackID)
+#if PLATFORM(MAC)
+    AttributedStringForCharacterRangeCallback(WebKit::AttributedString string, WebKit::EditingRange actualRange, uint64_t callbackID)
+#endif
 #if PLATFORM(IOS)
     GestureCallback(WebCore::IntPoint point, uint32_t gestureType, uint32_t gestureState, uint32_t flags, uint64_t callbackID)
     TouchesCallback(WebCore::IntPoint point, uint32_t touches, uint64_t callbackID)
index 4f2bb79..2c3d9b6 100644 (file)
@@ -130,12 +130,6 @@ bool WebPageProxy::insertText(const String& text, const EditingRange& replacemen
     return true;
 }
 
-bool WebPageProxy::insertDictatedText(const String&, const EditingRange&, const Vector<WebCore::TextAlternativeWithRange>&)
-{
-    notImplemented();
-    return false;
-}
-
 void WebPageProxy::getMarkedRange(EditingRange&)
 {
     notImplemented();
@@ -146,11 +140,6 @@ void WebPageProxy::getSelectedRange(EditingRange&)
     notImplemented();
 }
 
-void WebPageProxy::getAttributedSubstringFromRange(const EditingRange&, AttributedString&)
-{
-    notImplemented();
-}
-
 uint64_t WebPageProxy::characterIndexForPoint(const IntPoint)
 {
     notImplemented();
index 7394f85..1f06d7c 100644 (file)
@@ -220,6 +220,31 @@ bool WebPageProxy::insertDictatedText(const String& text, const EditingRange& re
 #endif
 }
 
+void WebPageProxy::insertDictatedTextAsync(const String& text, const EditingRange& replacementRange, const Vector<TextAlternativeWithRange>& dictationAlternativesWithRange)
+{
+#if USE(DICTATION_ALTERNATIVES)
+    if (!isValid())
+        return;
+
+    Vector<DictationAlternative> dictationAlternatives;
+
+    for (const TextAlternativeWithRange& alternativeWithRange : dictationAlternativesWithRange) {
+        uint64_t dictationContext = m_pageClient.addDictationAlternatives(alternativeWithRange.alternatives);
+        if (dictationContext)
+            dictationAlternatives.append(DictationAlternative(alternativeWithRange.range.location, alternativeWithRange.range.length, dictationContext));
+    }
+
+    if (dictationAlternatives.isEmpty()) {
+        insertTextAsync(text, replacementRange);
+        return;
+    }
+
+    process().send(Messages::WebPage::InsertDictatedTextAsync(text, replacementRange, dictationAlternatives), m_pageID);
+#else
+    insertTextAsync(text, replacementRange);
+#endif
+}
+
 void WebPageProxy::getMarkedRange(EditingRange& result)
 {
     result = EditingRange();
@@ -249,6 +274,33 @@ void WebPageProxy::getAttributedSubstringFromRange(const EditingRange& range, At
     process().sendSync(Messages::WebPage::GetAttributedSubstringFromRange(range), Messages::WebPage::GetAttributedSubstringFromRange::Reply(result), m_pageID);
 }
 
+void WebPageProxy::attributedSubstringForCharacterRangeAsync(const EditingRange& range, PassRefPtr<AttributedStringForCharacterRangeCallback> callback)
+{
+    if (!isValid()) {
+        callback->invalidate();
+        return;
+    }
+
+    uint64_t callbackID = callback->callbackID();
+    m_attributedStringForCharacterRangeCallbacks.set(callbackID, callback);
+
+    process().send(Messages::WebPage::AttributedSubstringForCharacterRangeAsync(range, callbackID), m_pageID);
+}
+
+void WebPageProxy::attributedStringForCharacterRangeCallback(const AttributedString& string, const EditingRange& actualRange, uint64_t callbackID)
+{
+    MESSAGE_CHECK(actualRange.isValid());
+
+    RefPtr<AttributedStringForCharacterRangeCallback> callback = m_attributedStringForCharacterRangeCallbacks.take(callbackID);
+    if (!callback) {
+        // FIXME: Log error or assert.
+        // this can validly happen if a load invalidated the callback, though
+        return;
+    }
+
+    callback->performCallbackWithReturnValue(string, actualRange);
+}
+
 uint64_t WebPageProxy::characterIndexForPoint(const IntPoint point)
 {
     if (!isValid())
index 4d606b9..232586b 100644 (file)
@@ -3896,6 +3896,117 @@ bool WebPage::shouldUseCustomContentProviderForResponse(const ResourceResponse&
     return m_mimeTypesWithCustomContentProviders.contains(response.mimeType()) && !canPluginHandleResponse(response);
 }
 
+#if PLATFORM(COCOA)
+
+void WebPage::insertTextAsync(const String& text, const EditingRange& replacementEditingRange)
+{
+    Frame& frame = m_page->focusController().focusedOrMainFrame();
+
+    if (replacementEditingRange.location != notFound) {
+        RefPtr<Range> replacementRange = rangeFromEditingRange(frame, replacementEditingRange);
+        if (replacementRange)
+            frame.selection().setSelection(VisibleSelection(replacementRange.get(), SEL_DEFAULT_AFFINITY));
+    }
+
+    if (!frame.editor().hasComposition()) {
+        // An insertText: might be handled by other responders in the chain if we don't handle it.
+        // One example is space bar that results in scrolling down the page.
+        frame.editor().insertText(text, nullptr);
+    } else
+        frame.editor().confirmComposition(text);
+}
+
+void WebPage::getMarkedRangeAsync(uint64_t callbackID)
+{
+    Frame& frame = m_page->focusController().focusedOrMainFrame();
+
+    RefPtr<Range> range = frame.editor().compositionRange();
+    size_t location;
+    size_t length;
+    if (!range || !TextIterator::getLocationAndLengthFromRange(frame.selection().rootEditableElementOrDocumentElement(), range.get(), location, length)) {
+        location = notFound;
+        length = 0;
+    }
+
+    send(Messages::WebPageProxy::EditingRangeCallback(EditingRange(location, length), callbackID));
+}
+
+void WebPage::getSelectedRangeAsync(uint64_t callbackID)
+{
+    Frame& frame = m_page->focusController().focusedOrMainFrame();
+
+    size_t location;
+    size_t length;
+    RefPtr<Range> range = frame.selection().toNormalizedRange();
+    if (!range || !TextIterator::getLocationAndLengthFromRange(frame.selection().rootEditableElementOrDocumentElement(), range.get(), location, length)) {
+        location = notFound;
+        length = 0;
+    }
+
+    send(Messages::WebPageProxy::EditingRangeCallback(EditingRange(location, length), callbackID));
+}
+
+void WebPage::characterIndexForPointAsync(const WebCore::IntPoint& point, uint64_t callbackID)
+{
+    uint64_t index = notFound;
+
+    HitTestResult result = m_page->mainFrame().eventHandler().hitTestResultAtPoint(point);
+    Frame* frame = result.innerNonSharedNode() ? result.innerNodeFrame() : &m_page->focusController().focusedOrMainFrame();
+    
+    RefPtr<Range> range = frame->rangeForPoint(result.roundedPointInInnerNodeFrame());
+    if (range) {
+        size_t location;
+        size_t length;
+        if (TextIterator::getLocationAndLengthFromRange(frame->selection().rootEditableElementOrDocumentElement(), range.get(), location, length))
+            index = static_cast<uint64_t>(location);
+    }
+
+    send(Messages::WebPageProxy::UnsignedCallback(index, callbackID));
+}
+
+void WebPage::firstRectForCharacterRangeAsync(const EditingRange& editingRange, uint64_t callbackID)
+{
+    Frame& frame = m_page->focusController().focusedOrMainFrame();
+    IntRect result(IntPoint(0, 0), IntSize(0, 0));
+    
+    RefPtr<Range> range = rangeFromEditingRange(frame, editingRange);
+    if (!range) {
+        send(Messages::WebPageProxy::RectForCharacterRangeCallback(result, EditingRange(notFound, 0), callbackID));
+        return;
+    }
+
+    ASSERT(range->startContainer());
+    ASSERT(range->endContainer());
+
+    result = frame.view()->contentsToWindow(frame.editor().firstRectForRange(range.get()));
+
+    // FIXME: Update actualRange to match the range of first rect.
+    send(Messages::WebPageProxy::RectForCharacterRangeCallback(result, editingRange, callbackID));
+}
+
+void WebPage::setCompositionAsync(const String& text, Vector<CompositionUnderline> underlines, const EditingRange& selection, const EditingRange& replacementEditingRange)
+{
+    Frame& frame = m_page->focusController().focusedOrMainFrame();
+
+    if (frame.selection().selection().isContentEditable()) {
+        RefPtr<Range> replacementRange;
+        if (replacementEditingRange.location != notFound) {
+            replacementRange = rangeFromEditingRange(frame, replacementEditingRange);
+            frame.selection().setSelection(VisibleSelection(replacementRange.get(), SEL_DEFAULT_AFFINITY));
+        }
+
+        frame.editor().setComposition(text, underlines, selection.location, selection.location + selection.length);
+    }
+}
+
+void WebPage::confirmCompositionAsync()
+{
+    Frame& frame = m_page->focusController().focusedOrMainFrame();
+    frame.editor().confirmComposition();
+}
+
+#endif // PLATFORM(COCOA)
+
 #if PLATFORM(GTK)
 static Frame* targetFrameForEditing(WebPage* page)
 {
index 5a6150d..44b6cbf 100644 (file)
@@ -538,15 +538,27 @@ public:
     
     void sendComplexTextInputToPlugin(uint64_t pluginComplexTextInputIdentifier, const String& textInput);
 
-    void cancelComposition(EditorState& newState);
-#if !PLATFORM(IOS)
+#if PLATFORM(MAC)
     void insertText(const String& text, const EditingRange& replacementRange, bool& handled, EditorState& newState);
     void setComposition(const String& text, Vector<WebCore::CompositionUnderline> underlines, const EditingRange& selectionRange, const EditingRange& replacementRange, EditorState& newState);
     void confirmComposition(EditorState& newState);
+    void insertDictatedText(const String& text, const EditingRange& replacementRange, const Vector<WebCore::DictationAlternative>& dictationAlternativeLocations, bool& handled, EditorState& newState);
+    void insertDictatedTextAsync(const String& text, const EditingRange& replacementRange, const Vector<WebCore::DictationAlternative>& dictationAlternativeLocations);
+    void getAttributedSubstringFromRange(const EditingRange&, AttributedString&);
+    void attributedSubstringForCharacterRangeAsync(const EditingRange&, uint64_t callbackID);
 #endif
+
+    void insertTextAsync(const String& text, const EditingRange& replacementRange);
+    void getMarkedRangeAsync(uint64_t callbackID);
+    void getSelectedRangeAsync(uint64_t callbackID);
+    void characterIndexForPointAsync(const WebCore::IntPoint&, uint64_t callbackID);
+    void firstRectForCharacterRangeAsync(const EditingRange&, uint64_t callbackID);
+    void setCompositionAsync(const String& text, Vector<WebCore::CompositionUnderline> underlines, const EditingRange& selectionRange, const EditingRange& replacementRange);
+    void confirmCompositionAsync();
+
+    void cancelComposition(EditorState& newState);
     void getMarkedRange(EditingRange&);
     void getSelectedRange(EditingRange&);
-    void getAttributedSubstringFromRange(const EditingRange&, AttributedString&);
     void characterIndexForPoint(const WebCore::IntPoint point, uint64_t& result);
     void firstRectForCharacterRange(const EditingRange&, WebCore::IntRect& resultRect);
     void executeKeypressCommands(const Vector<WebCore::KeypressCommand>&, bool& handled, EditorState& newState);
@@ -556,7 +568,6 @@ public:
     void shouldDelayWindowOrderingEvent(const WebKit::WebMouseEvent&, bool& result);
     void acceptsFirstMouse(int eventNumber, const WebKit::WebMouseEvent&, bool& result);
     bool performNonEditingBehaviorForSelector(const String&, WebCore::KeyboardEvent*);
-    void insertDictatedText(const String& text, const EditingRange& replacementRange, const Vector<WebCore::DictationAlternative>& dictationAlternativeLocations, bool& handled, EditorState& newState);
 #elif PLATFORM(EFL)
     void confirmComposition(const String& compositionString);
     void setComposition(const WTF::String& compositionString, const WTF::Vector<WebCore::CompositionUnderline>& underlines, uint64_t cursorPosition);
index 6ff1df1..6a60800 100644 (file)
@@ -56,6 +56,7 @@ messages -> WebPage LegacyReceiver {
     SyncApplyAutocorrection(String correction, String originalText) -> (bool autocorrectionApplied)
     RequestAutocorrectionContext(uint64_t callbackID)
     GetAutocorrectionContext() -> (String beforeContext, String markedText, String selectedText, String afterContext, uint64_t location, uint64_t length) 
+    # FIXME: Use shared COCOA text input methods like insertTextAsync.
     InsertText(String text, WebKit::EditingRange replacementRange)
     SetComposition(String text, Vector<WebCore::CompositionUnderline> underlines, WebKit::EditingRange selectionRange)
     ConfirmComposition()
@@ -298,18 +299,28 @@ messages -> WebPage LegacyReceiver {
     CancelComposition() -> (WebKit::EditorState newState)
     GetMarkedRange() -> (WebKit::EditingRange range)
     GetSelectedRange() -> (WebKit::EditingRange range)
-    GetAttributedSubstringFromRange(WebKit::EditingRange range) -> (WebKit::AttributedString result)
     CharacterIndexForPoint(WebCore::IntPoint point) -> (uint64_t result)
     FirstRectForCharacterRange(WebKit::EditingRange range) -> (WebCore::IntRect resultRect)
     ExecuteKeypressCommands(Vector<WebCore::KeypressCommand> savedCommands) -> (bool handled, WebKit::EditorState newState)
     ShouldDelayWindowOrderingEvent(WebKit::WebMouseEvent event) -> (bool result)
     AcceptsFirstMouse(int eventNumber, WebKit::WebMouseEvent event) -> (bool result)
-    InsertDictatedText(String text, WebKit::EditingRange replacementRange, Vector<WebCore::DictationAlternative> dictationAlternatives) -> (bool handled, WebKit::EditorState newState)
+
+    InsertTextAsync(String text, WebKit::EditingRange replacementRange)
+    GetMarkedRangeAsync(uint64_t callbackID)
+    GetSelectedRangeAsync(uint64_t callbackID)
+    CharacterIndexForPointAsync(WebCore::IntPoint point, uint64_t callbackID);
+    FirstRectForCharacterRangeAsync(WebKit::EditingRange range, uint64_t callbackID);
+    SetCompositionAsync(String text, Vector<WebCore::CompositionUnderline> underlines, WebKit::EditingRange selectionRange, WebKit::EditingRange replacementRange)
+    ConfirmCompositionAsync()
 #endif
 #if PLATFORM(MAC)
     InsertText(String text, WebKit::EditingRange replacementRange) -> (bool handled, WebKit::EditorState newState)
     SetComposition(String text, Vector<WebCore::CompositionUnderline> underlines, WebKit::EditingRange selectionRange, WebKit::EditingRange replacementRange) -> (WebKit::EditorState newState)
     ConfirmComposition() -> (WebKit::EditorState newState)
+    InsertDictatedText(String text, WebKit::EditingRange replacementRange, Vector<WebCore::DictationAlternative> dictationAlternatives) -> (bool handled, WebKit::EditorState newState)
+    InsertDictatedTextAsync(String text, WebKit::EditingRange replacementRange, Vector<WebCore::DictationAlternative> dictationAlternatives)
+    GetAttributedSubstringFromRange(WebKit::EditingRange range) -> (WebKit::AttributedString result)
+    AttributedSubstringForCharacterRangeAsync(WebKit::EditingRange range, uint64_t callbackID);
 #endif
     SetMinimumLayoutSize(WebCore::IntSize minimumLayoutSize)
     SetAutoSizingShouldExpandToViewHeight(bool shouldExpand)
index a4ea0f0..f9f6858 100644 (file)
@@ -204,11 +204,6 @@ void WebPage::insertText(const String& text, const EditingRange& replacementEdit
         frame.editor().confirmComposition(text);
 }
 
-void WebPage::insertDictatedText(const String&, const EditingRange&, const Vector<WebCore::DictationAlternative>&, bool&, EditorState&)
-{
-    notImplemented();
-}
-
 void WebPage::getMarkedRange(EditingRange&)
 {
     notImplemented();
@@ -219,11 +214,6 @@ void WebPage::getSelectedRange(EditingRange&)
     notImplemented();
 }
 
-void WebPage::getAttributedSubstringFromRange(const EditingRange&, AttributedString&)
-{
-    notImplemented();
-}
-
 void WebPage::characterIndexForPoint(IntPoint, uint64_t&)
 {
     notImplemented();
index 80fd2c9..75b1192 100644 (file)
@@ -314,6 +314,20 @@ void WebPage::insertDictatedText(const String& text, const EditingRange& replace
     newState = editorState();
 }
 
+void WebPage::insertDictatedTextAsync(const String& text, const EditingRange& replacementEditingRange, const Vector<WebCore::DictationAlternative>& dictationAlternativeLocations)
+{
+    Frame& frame = m_page->focusController().focusedOrMainFrame();
+
+    if (replacementEditingRange.location != notFound) {
+        RefPtr<Range> replacementRange = rangeFromEditingRange(frame, replacementEditingRange);
+        if (replacementRange)
+            frame.selection().setSelection(VisibleSelection(replacementRange.get(), SEL_DEFAULT_AFFINITY));
+    }
+
+    ASSERT(!frame.editor().hasComposition());
+    frame.editor().insertDictatedText(text, dictationAlternativeLocations, nullptr);
+}
+
 void WebPage::getMarkedRange(EditingRange& result)
 {
     Frame& frame = m_page->focusController().focusedOrMainFrame();
@@ -365,6 +379,39 @@ void WebPage::getAttributedSubstringFromRange(const EditingRange& editingRange,
     }
 }
 
+void WebPage::attributedSubstringForCharacterRangeAsync(const EditingRange& editingRange, uint64_t callbackID)
+{
+    AttributedString result;
+
+    Frame& frame = m_page->focusController().focusedOrMainFrame();
+
+    const VisibleSelection& selection = frame.selection().selection();
+    if (selection.isNone() || !selection.isContentEditable() || selection.isInPasswordField()) {
+        send(Messages::WebPageProxy::AttributedStringForCharacterRangeCallback(result, EditingRange(), callbackID));
+        return;
+    }
+
+    RefPtr<Range> range = rangeFromEditingRange(frame, editingRange);
+    if (!range) {
+        send(Messages::WebPageProxy::AttributedStringForCharacterRangeCallback(result, EditingRange(), callbackID));
+        return;
+    }
+
+    result.string = [WebHTMLConverter editingAttributedStringFromRange:range.get()];
+    NSAttributedString* attributedString = result.string.get();
+    
+    // [WebHTMLConverter editingAttributedStringFromRange:] insists on inserting a trailing 
+    // whitespace at the end of the string which breaks the ATOK input method.  <rdar://problem/5400551>
+    // To work around this we truncate the resultant string to the correct length.
+    if ([attributedString length] > editingRange.length) {
+        ASSERT([attributedString length] == editingRange.length + 1);
+        ASSERT([[attributedString string] characterAtIndex:editingRange.length] == '\n' || [[attributedString string] characterAtIndex:editingRange.length] == ' ');
+        result.string = [attributedString attributedSubstringFromRange:NSMakeRange(0, editingRange.length)];
+    }
+
+    send(Messages::WebPageProxy::AttributedStringForCharacterRangeCallback(result, EditingRange(editingRange.location, [result.string length]), callbackID));
+}
+
 void WebPage::characterIndexForPoint(IntPoint point, uint64_t& index)
 {
     index = notFound;