Index: chrome/browser/renderer_host/render_widget_host_view_mac.mm |
diff --git a/chrome/browser/renderer_host/render_widget_host_view_mac.mm b/chrome/browser/renderer_host/render_widget_host_view_mac.mm |
index 18ffb5f3dab36166d2068357a6eb4202dc81ba1b..104b3468463735d512d97273d41f77ab0a28bdad 100644 |
--- a/chrome/browser/renderer_host/render_widget_host_view_mac.mm |
+++ b/chrome/browser/renderer_host/render_widget_host_view_mac.mm |
@@ -131,8 +131,6 @@ RenderWidgetHostViewMac::RenderWidgetHostViewMac(RenderWidgetHost* widget) |
: render_widget_host_(widget), |
about_to_validate_and_paint_(false), |
call_set_needs_display_in_rect_pending_(false), |
- im_attributes_(nil), |
- im_composing_(false), |
is_loading_(false), |
is_hidden_(false), |
is_popup_menu_(false), |
@@ -147,7 +145,6 @@ RenderWidgetHostViewMac::RenderWidgetHostViewMac(RenderWidgetHost* widget) |
} |
RenderWidgetHostViewMac::~RenderWidgetHostViewMac() { |
- [im_attributes_ release]; |
} |
/////////////////////////////////////////////////////////////////////////////// |
@@ -321,18 +318,14 @@ void RenderWidgetHostViewMac::IMEUpdateStatus(int control, |
const gfx::Rect& caret_rect) { |
// Reset the IME state and finish an ongoing composition in the renderer. |
if (control == IME_DISABLE || control == IME_COMPLETE_COMPOSITION) |
- IMECleanupComposition(); |
+ [cocoa_view_ cancelComposition]; |
// We need to convert the coordinate of the cursor rectangle sent from the |
// renderer and save it. Our IME backend uses a coordinate system whose |
// origin is the upper-left corner of this view. On the other hand, Cocoa |
// uses a coordinate system whose origin is the lower-left corner of this |
// view. So, we convert the cursor rectangle and save it. |
- NSRect view_rect = [cocoa_view_ bounds]; |
- const int y_offset = static_cast<int>(view_rect.size.height); |
- im_caret_rect_ = NSMakeRect(caret_rect.x(), |
- y_offset - caret_rect.y() - caret_rect.height(), |
- caret_rect.width(), caret_rect.height()); |
+ [cocoa_view_ setCaretRect:[cocoa_view_ RectToNSRect:caret_rect]]; |
} |
void RenderWidgetHostViewMac::DidPaintBackingStoreRects( |
@@ -544,22 +537,6 @@ void RenderWidgetHostViewMac::KillSelf() { |
} |
} |
-void RenderWidgetHostViewMac::IMECleanupComposition() { |
- if (!im_composing_) |
- return; |
- |
- // Cancel the ongoing composition. [NSInputManager markedTextAbandoned:] |
- // doesn't call any NSTextInput functions, such as setMarkedText or |
- // insertText. So, we need to send an IPC message to a renderer so it can |
- // delete the composition node. |
- NSInputManager *currentInputManager = [NSInputManager currentInputManager]; |
- [currentInputManager markedTextAbandoned:[cocoa_view_ self]]; |
- |
- render_widget_host_->ImeCancelComposition(); |
- im_text_.clear(); |
- im_composing_ = false; |
-} |
- |
gfx::PluginWindowHandle |
RenderWidgetHostViewMac::AllocateFakePluginWindowHandle(bool opaque) { |
// Make sure we have a layer for the plugin to draw into. |
@@ -780,6 +757,8 @@ bool RenderWidgetHostViewMac::ContainsNativeView( |
@implementation RenderWidgetHostViewCocoa |
+@synthesize caretRect = caretRect_; |
+ |
- (id)initWithRenderWidgetHostViewMac:(RenderWidgetHostViewMac*)r { |
self = [super initWithFrame:NSZeroRect]; |
if (self != nil) { |
@@ -814,7 +793,7 @@ bool RenderWidgetHostViewMac::ContainsNativeView( |
renderWidgetHostView_->render_widget_host_->ForwardMouseEvent(event); |
if ([theEvent type] == NSLeftMouseDown) { |
- renderWidgetHostView_->IMECleanupComposition(); |
+ [self cancelComposition]; |
hasOpenMouseDown_ = YES; |
} |
@@ -881,74 +860,125 @@ bool RenderWidgetHostViewMac::ContainsNativeView( |
return; |
} |
- scoped_nsobject<RenderWidgetHostViewCocoa> keepSelfAlive([self retain]); |
- |
// Don't cancel child popups; the key events are probably what's triggering |
// the popup in the first place. |
+ RenderWidgetHost* widgetHost = renderWidgetHostView_->render_widget_host_; |
+ DCHECK(widgetHost); |
+ |
NativeWebKeyboardEvent event(theEvent); |
- // Save the modifier keys so the insertText method can use it when it sends |
- // a Char event, which is dispatched as an onkeypress() event of JavaScript. |
- renderWidgetHostView_->im_modifiers_ = event.modifiers; |
+ // We only handle key down events and just simply forward other events. |
+ if ([theEvent type] != NSKeyDown) { |
+ widgetHost->ForwardKeyboardEvent(event); |
+ |
+ // Possibly autohide the cursor. |
+ if ([RenderWidgetHostViewCocoa shouldAutohideCursorForEvent:theEvent]) |
+ [NSCursor setHiddenUntilMouseMoves:YES]; |
+ |
+ return; |
+ } |
+ |
+ scoped_nsobject<RenderWidgetHostViewCocoa> keepSelfAlive([self retain]); |
+ |
+ // Records the current marked text state, so that we can know if the marked |
+ // text was deleted or not after handling the key down event. |
+ BOOL oldHasMarkedText = hasMarkedText_; |
+ |
+ // This method should not be called recursively. |
+ DCHECK(!handlingKeyDown_); |
+ |
+ // Tells insertText: and doCommandBySelector: that we are handling a key |
+ // down event. |
+ handlingKeyDown_ = YES; |
+ |
+ // These two variables might be set when handling the keyboard event. |
+ // Clear them here so that we can know whether they have changed afterwards. |
+ textToBeInserted_.clear(); |
+ newMarkedText_.clear(); |
+ |
+ // Sends key down events to input method first, then we can decide what should |
+ // be done according to input method's feedback. |
+ [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]]; |
+ |
+ handlingKeyDown_ = NO; |
// To emulate Windows, over-write |event.windowsKeyCode| to VK_PROCESSKEY |
- // while an input method is composing a text. |
+ // while an input method is composing or inserting a text. |
// Gmail checks this code in its onkeydown handler to stop auto-completing |
// e-mail addresses while composing a CJK text. |
- if ([theEvent type] == NSKeyDown && renderWidgetHostView_->im_composing_) { |
+ // If the text to be inserted has only one character, then we don't need this |
+ // trick, because we'll send the text as a key press event instead. |
+ if (hasMarkedText_ || oldHasMarkedText || textToBeInserted_.length() > 1) { |
event.windowsKeyCode = 0xE5; // VKEY_PROCESSKEY |
event.setKeyIdentifierFromWindowsKeyCode(); |
event.skip_in_browser = true; |
- } |
- |
- // Dispatch this keyboard event to the renderer. |
- if (renderWidgetHostView_->render_widget_host_) { |
- RenderWidgetHost* widgetHost = renderWidgetHostView_->render_widget_host_; |
+ } else { |
// Look up shortcut, if any, for this key combination. |
EditCommands editCommands; |
[EditCommandMatcher matchEditCommands:&editCommands forEvent:theEvent]; |
if (!editCommands.empty()) |
widgetHost->ForwardEditCommandsForNextKeyEvent(editCommands); |
- widgetHost->ForwardKeyboardEvent(event); |
} |
- currentKeyEvent_.reset([theEvent retain]); |
- textInserted_ = NO; |
- |
- // Dispatch a NSKeyDown event to an input method. |
- // To send an onkeydown() event before an onkeypress() event, we should |
- // dispatch this NSKeyDown event AFTER sending it to the renderer. |
- // (See <https://bugs.webkit.org/show_bug.cgi?id=25119>). |
- // |
- // If this object's retainCount is 1, the only reference is the one held by |
- // keepSelfAlive. All other references may have been destroyed in the |
- // RenderWidgetHost::ForwardKeyboardEvent call above if it resulted in tab |
- // closure. Were it not for that single reference, this object would |
- // already be deallocated. In that case, there's no point in calling |
- // -interpretKeyEvents:. |
- if ([self retainCount] > 1 && [theEvent type] == NSKeyDown) { |
- [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]]; |
+ // Forward the key down event first. |
+ widgetHost->ForwardKeyboardEvent(event); |
+ // Calling ForwardKeyboardEvent() could have destroyed the widget. When the |
+ // widget was destroyed, |renderWidgetHostView_->render_widget_host_| will |
+ // be set to NULL. So we check it here and return immediately if it's NULL. |
+ if (!renderWidgetHostView_->render_widget_host_) |
+ return; |
+ |
+ // Then send keypress and/or composition related events. |
+ // If there was a marked text or the text to be inserted is longer than 1 |
+ // character, then we send the text by calling ImeConfirmComposition(). |
+ // Otherwise, if the text to be inserted only contains 1 character, then we |
+ // can just send a keypress event which is fabricated by changing the type of |
+ // the keydown event, so that we can retain all necessary informations, such |
+ // as unmodifiedText, etc. And we need to set event.skip_in_browser to true to |
+ // prevent the browser from handling it again. |
+ // Note that, |textToBeInserted_| is a UTF-16 string, but it's fine to only |
+ // handle BMP characters here, as we can always insert non-BMP characters as |
+ // text. |
+ const NSUInteger kCtrlCmdKeyMask = NSControlKeyMask | NSCommandKeyMask; |
+ BOOL textInserted = NO; |
+ if (textToBeInserted_.length() > (oldHasMarkedText ? 0 : 1)) { |
+ widgetHost->ImeConfirmComposition(textToBeInserted_); |
+ textInserted = YES; |
+ } else if (textToBeInserted_.length() == 1) { |
+ event.type = WebKit::WebInputEvent::Char; |
+ event.text[0] = textToBeInserted_[0]; |
+ event.text[1] = 0; |
+ event.skip_in_browser = true; |
+ widgetHost->ForwardKeyboardEvent(event); |
+ } else if (([theEvent modifierFlags] & kCtrlCmdKeyMask) && |
+ [[theEvent characters] length] > 0) { |
// We don't get insertText: calls if ctrl is down and not even a keyDown: |
// call if cmd is down, so synthesize a keypress event for these cases. |
// Note that this makes our behavior deviate from the windows and linux |
// versions of chrome (however, see http://crbug.com/13891 ), but it makes |
// us behave similar to how Safari behaves. |
- if ([theEvent modifierFlags] & (NSControlKeyMask | NSCommandKeyMask) && |
- !textInserted_ && [[theEvent characters] length] > 0 && |
- renderWidgetHostView_->render_widget_host_) { |
- // Just fabricate a Char event by changing the type of the RawKeyDown |
- // event, to retain all necessary informations, such as unmodifiedText. |
- event.type = WebKit::WebInputEvent::Char; |
- // We fire menu items on keydown, we don't want to activate menu items |
- // twice. |
- event.skip_in_browser = true; |
- renderWidgetHostView_->render_widget_host_->ForwardKeyboardEvent(event); |
- } |
+ event.type = WebKit::WebInputEvent::Char; |
+ event.skip_in_browser = true; |
+ widgetHost->ForwardKeyboardEvent(event); |
} |
- currentKeyEvent_.reset(); |
+ // Updates or cancels the composition. If some text has been inserted, then |
+ // we don't need to cancel the composition explicitly. |
+ if (hasMarkedText_ && newMarkedText_.length()) { |
+ // Sends the updated marked text to the renderer so it can update the |
+ // composition node in WebKit. |
+ // When marked text is available, |selectedRange_| will be the range being |
+ // selected inside the marked text. We put the cursor at the beginning of |
+ // the selected range. |
+ widgetHost->ImeSetComposition(newMarkedText_, |
+ selectedRange_.location, |
+ selectedRange_.location, |
+ NSMaxRange(selectedRange_)); |
+ } else if (oldHasMarkedText && !hasMarkedText_ && !textInserted) { |
+ widgetHost->ImeCancelComposition(); |
+ } |
// Possibly autohide the cursor. |
if ([RenderWidgetHostViewCocoa shouldAutohideCursorForEvent:theEvent]) |
@@ -1148,6 +1178,7 @@ bool RenderWidgetHostViewMac::ContainsNativeView( |
return NO; |
renderWidgetHostView_->render_widget_host_->Focus(); |
+ renderWidgetHostView_->render_widget_host_->ImeSetInputMode(true); |
return YES; |
} |
@@ -1158,8 +1189,8 @@ bool RenderWidgetHostViewMac::ContainsNativeView( |
if (closeOnDeactivate_) |
renderWidgetHostView_->KillSelf(); |
+ renderWidgetHostView_->render_widget_host_->ImeSetInputMode(false); |
renderWidgetHostView_->render_widget_host_->Blur(); |
- |
return YES; |
} |
@@ -1494,15 +1525,15 @@ extern NSString *NSTextInputReplacementRangeAttributeName; |
- (NSArray *)validAttributesForMarkedText { |
// This code is just copied from WebKit except renaming variables. |
- if (!renderWidgetHostView_->im_attributes_) { |
- renderWidgetHostView_->im_attributes_ = [[NSArray alloc] initWithObjects: |
+ if (!validAttributesForMarkedText_) { |
+ validAttributesForMarkedText_.reset([[NSArray alloc] initWithObjects: |
NSUnderlineStyleAttributeName, |
NSUnderlineColorAttributeName, |
NSMarkedClauseSegmentAttributeName, |
NSTextInputReplacementRangeAttributeName, |
- nil]; |
+ nil]); |
} |
- return renderWidgetHostView_->im_attributes_; |
+ return validAttributesForMarkedText_.get(); |
} |
- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint { |
@@ -1518,8 +1549,7 @@ extern NSString *NSTextInputReplacementRangeAttributeName; |
// Since this window may be moved since we receive the cursor rectangle last |
// time we sent the cursor rectangle to the IME, so we should map from the |
// view coordinate to the screen coordinate every time when an IME need it. |
- NSRect resultRect = renderWidgetHostView_->im_caret_rect_; |
- resultRect = [self convertRect:resultRect toView:nil]; |
+ NSRect resultRect = [self convertRect:caretRect_ toView:nil]; |
NSWindow* window = [self window]; |
if (window) |
resultRect.origin = [window convertBaseToScreen:resultRect.origin]; |
@@ -1528,7 +1558,7 @@ extern NSString *NSTextInputReplacementRangeAttributeName; |
- (NSRange)selectedRange { |
// Return the selected range saved in the setMarkedText method. |
- return renderWidgetHostView_->im_selected_range_; |
+ return hasMarkedText_ ? selectedRange_ : NSMakeRange(NSNotFound, 0); |
} |
- (NSRange)markedRange { |
@@ -1538,7 +1568,7 @@ extern NSString *NSTextInputReplacementRangeAttributeName; |
// calls the setMarkedText method and we can update the composition node |
// there. (When this method returns an empty range, the input method doesn't |
// call the setMarkedText method.) |
- return renderWidgetHostView_->im_marked_range_; |
+ return hasMarkedText_ ? markedRange_ : NSMakeRange(NSNotFound, 0); |
} |
- (NSAttributedString *)attributedSubstringFromRange:(NSRange)nsRange { |
@@ -1556,11 +1586,11 @@ extern NSString *NSTextInputReplacementRangeAttributeName; |
- (BOOL)hasMarkedText { |
// An input method calls this function to figure out whether or not an |
// application is really composing a text. If it is composing, it calls |
- // the markedRange method, and maybe calls the setMarkedTest method. |
+ // the markedRange method, and maybe calls the setMarkedText method. |
// It seems an input method usually calls this function when it is about to |
// cancel an ongoing composition. If an application has a non-empty marked |
// range, it calls the setMarkedText method to delete the range. |
- return renderWidgetHostView_->im_composing_ ? YES : NO; |
+ return hasMarkedText_; |
} |
- (void)unmarkText { |
@@ -1569,8 +1599,12 @@ extern NSString *NSTextInputReplacementRangeAttributeName; |
// It seems an input method calls the setMarkedText method and set an empty |
// text when it cancels an ongoing composition, i.e. I have never seen an |
// input method calls this method. |
- renderWidgetHostView_->render_widget_host_->ImeCancelComposition(); |
- renderWidgetHostView_->im_composing_ = false; |
+ hasMarkedText_ = NO; |
+ |
+ // If we are handling a key down event, then ImeCancelComposition() will be |
+ // called in keyEvent: method. |
+ if (!handlingKeyDown_) |
+ renderWidgetHostView_->render_widget_host_->ImeCancelComposition(); |
} |
- (void)setMarkedText:(id)string selectedRange:(NSRange)newSelRange { |
@@ -1580,39 +1614,24 @@ extern NSString *NSTextInputReplacementRangeAttributeName; |
BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]]; |
NSString* im_text = isAttributedString ? [string string] : string; |
int length = [im_text length]; |
- int cursor = newSelRange.location; |
- |
- int target_start; |
- int target_end; |
- if (!newSelRange.length) { |
- // The given text doesn't have any range to be highlighted. Clear the |
- // selection range. |
- target_start = 0; |
- target_end = 0; |
- } else { |
- // The given text has a range to be highlighted. |
- // Set the selection range to the given one and put the cursor at the |
- // beginning of the selection range. |
- target_start = newSelRange.location; |
- target_end = NSMaxRange(newSelRange); |
- } |
- // Dispatch this IME event to the renderer and update the IME state of this |
- // object. |
+ markedRange_ = NSMakeRange(0, length); |
+ selectedRange_ = newSelRange; |
+ newMarkedText_ = base::SysNSStringToUTF16(im_text); |
+ hasMarkedText_ = (length > 0); |
+ |
+ // If we are handling a key down event, then ImeSetComposition() will be |
+ // called in keyEvent: method. |
// Input methods of Mac use setMarkedText calls with an empty text to cancel |
// an ongoing composition. So, we should check whether or not the given text |
// is empty to update the IME state. (Our IME backend can automatically |
// cancels an ongoing composition when we send an empty text. So, it is OK |
// to send an empty text to the renderer.) |
- renderWidgetHostView_->im_text_ = UTF8ToUTF16([im_text UTF8String]); |
- renderWidgetHostView_->render_widget_host_->ImeSetComposition( |
- renderWidgetHostView_->im_text_, cursor, target_start, target_end); |
- renderWidgetHostView_->GetRenderWidgetHost()->ImeSetInputMode(true); |
- renderWidgetHostView_->im_composing_ = length > 0; |
- renderWidgetHostView_->im_marked_range_.location = 0; |
- renderWidgetHostView_->im_marked_range_.length = length; |
- renderWidgetHostView_->im_selected_range_.location = newSelRange.location; |
- renderWidgetHostView_->im_selected_range_.length = newSelRange.length; |
+ if (!handlingKeyDown_) { |
+ renderWidgetHostView_->render_widget_host_->ImeSetComposition( |
+ newMarkedText_, newSelRange.location, newSelRange.location, |
+ NSMaxRange(newSelRange)); |
+ } |
} |
- (void)doCommandBySelector:(SEL)selector { |
@@ -1624,22 +1643,17 @@ extern NSString *NSTextInputReplacementRangeAttributeName; |
// character to onkeypress() event handlers. |
// TODO(hbono): need to handle more commands? |
if (selector == @selector(insertNewline:)) { |
- if (currentKeyEvent_.get()) { |
- // Create the Char event from the NSEvent object, so that we can retain |
- // necessary informations, especially unmodifiedText. |
- NativeWebKeyboardEvent event(currentKeyEvent_.get()); |
- event.type = WebKit::WebInputEvent::Char; |
- event.text[0] = '\r'; |
- event.text[1] = 0; |
- event.skip_in_browser = true; |
- renderWidgetHostView_->render_widget_host_->ForwardKeyboardEvent(event); |
+ if (handlingKeyDown_) { |
+ // If we are handling a key down event, then we just need to append a '\r' |
+ // character to |textToBeInserted_| which will then be handled by |
+ // keyEvent: method. |
+ textToBeInserted_.push_back('\r'); |
} else { |
// This call is not initiated by a key event, so just executed the |
// corresponding editor command. |
renderWidgetHostView_->render_widget_host_->ForwardEditCommand( |
"InsertNewline", ""); |
} |
- textInserted_ = YES; |
} |
} |
@@ -1659,23 +1673,15 @@ extern NSString *NSTextInputReplacementRangeAttributeName; |
// sent as an IME event as well. |
BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]]; |
NSString* im_text = isAttributedString ? [string string] : string; |
- if (!renderWidgetHostView_->im_composing_ && [im_text length] == 1 && |
- currentKeyEvent_.get()) { |
- // Create the Char event from the NSEvent object, so that we can retain |
- // necessary informations, especially unmodifiedText. |
- NativeWebKeyboardEvent event(currentKeyEvent_.get()); |
- event.type = WebKit::WebInputEvent::Char; |
- event.text[0] = [im_text characterAtIndex:0]; |
- event.text[1] = 0; |
- event.skip_in_browser = true; |
- renderWidgetHostView_->render_widget_host_->ForwardKeyboardEvent(event); |
+ if (handlingKeyDown_) { |
+ textToBeInserted_.append(base::SysNSStringToUTF16(im_text)); |
} else { |
renderWidgetHostView_->render_widget_host_->ImeConfirmComposition( |
- UTF8ToUTF16([im_text UTF8String])); |
+ base::SysNSStringToUTF16(im_text)); |
} |
- renderWidgetHostView_->im_text_.clear(); |
- renderWidgetHostView_->im_composing_ = false; |
- textInserted_ = YES; |
+ |
+ // Inserting text will delete all marked text automatically. |
+ hasMarkedText_ = NO; |
} |
- (void)viewDidMoveToWindow { |
@@ -1801,4 +1807,18 @@ extern NSString *NSTextInputReplacementRangeAttributeName; |
[acceleratedPluginLayer_.get() setNeedsDisplay]; |
} |
+- (void)cancelComposition { |
+ if (!hasMarkedText_) |
+ return; |
+ |
+ // Cancel the ongoing composition. [NSInputManager markedTextAbandoned:] |
+ // doesn't call any NSTextInput functions, such as setMarkedText or |
+ // insertText. So, we need to send an IPC message to a renderer so it can |
+ // delete the composition node. |
+ NSInputManager *currentInputManager = [NSInputManager currentInputManager]; |
+ [currentInputManager markedTextAbandoned:self]; |
+ |
+ [self unmarkText]; |
+} |
+ |
@end |