360 lines
13 KiB
Plaintext
360 lines
13 KiB
Plaintext
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
|
|
// reserved. Use of this source code is governed by a BSD-style license that
|
|
// can be found in the LICENSE file.
|
|
|
|
#include "libcef/browser/text_input_client_osr_mac.h"
|
|
#include "libcef/browser/browser_host_impl.h"
|
|
|
|
#include "base/strings/sys_string_conversions.h"
|
|
#import "content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper.h"
|
|
#import "content/browser/renderer_host/text_input_client_mac.h"
|
|
#include "content/common/input_messages.h"
|
|
|
|
namespace {
|
|
|
|
// TODO(suzhe): Upstream this function.
|
|
blink::WebColor WebColorFromNSColor(NSColor *color) {
|
|
CGFloat r, g, b, a;
|
|
[color getRed:&r green:&g blue:&b alpha:&a];
|
|
|
|
return
|
|
std::max(0, std::min(static_cast<int>(lroundf(255.0f * a)), 255)) << 24 |
|
|
std::max(0, std::min(static_cast<int>(lroundf(255.0f * r)), 255)) << 16 |
|
|
std::max(0, std::min(static_cast<int>(lroundf(255.0f * g)), 255)) << 8 |
|
|
std::max(0, std::min(static_cast<int>(lroundf(255.0f * b)), 255));
|
|
}
|
|
|
|
// Extract underline information from an attributed string. Mostly copied from
|
|
// third_party/WebKit/Source/WebKit/mac/WebView/WebHTMLView.mm
|
|
void ExtractUnderlines(NSAttributedString* string,
|
|
std::vector<blink::WebCompositionUnderline>* underlines) {
|
|
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)];
|
|
NSNumber *style = [attrs objectForKey: NSUnderlineStyleAttributeName];
|
|
if (style) {
|
|
blink::WebColor color = SK_ColorBLACK;
|
|
if (NSColor *colorAttr =
|
|
[attrs objectForKey:NSUnderlineColorAttributeName]) {
|
|
color = WebColorFromNSColor(
|
|
[colorAttr colorUsingColorSpaceName:NSDeviceRGBColorSpace]);
|
|
}
|
|
underlines->push_back(blink::WebCompositionUnderline(
|
|
range.location, NSMaxRange(range), color, [style intValue] > 1, 0));
|
|
}
|
|
i = range.location + range.length;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" {
|
|
extern NSString* NSTextInputReplacementRangeAttributeName;
|
|
}
|
|
|
|
@implementation CefTextInputClientOSRMac
|
|
|
|
@synthesize selectedRange = selectedRange_;
|
|
@synthesize handlingKeyDown = handlingKeyDown_;
|
|
|
|
- (id)initWithRenderWidgetHostViewOSR:(CefRenderWidgetHostViewOSR*)rwhv {
|
|
self = [super init];
|
|
renderWidgetHostView_ = rwhv;
|
|
|
|
return self;
|
|
}
|
|
|
|
- (NSArray*)validAttributesForMarkedText {
|
|
if (!validAttributesForMarkedText_) {
|
|
validAttributesForMarkedText_.reset([[NSArray alloc] initWithObjects:
|
|
NSUnderlineStyleAttributeName,
|
|
NSUnderlineColorAttributeName,
|
|
NSMarkedClauseSegmentAttributeName,
|
|
NSTextInputReplacementRangeAttributeName,
|
|
nil]);
|
|
}
|
|
return validAttributesForMarkedText_.get();
|
|
}
|
|
|
|
- (NSRange)markedRange {
|
|
return hasMarkedText_ ? markedRange_ : NSMakeRange(NSNotFound, 0);
|
|
}
|
|
|
|
- (BOOL)hasMarkedText {
|
|
return hasMarkedText_;
|
|
}
|
|
|
|
- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange {
|
|
BOOL isAttributedString = [aString isKindOfClass:[NSAttributedString class]];
|
|
NSString* im_text = isAttributedString ? [aString string] : aString;
|
|
if (handlingKeyDown_) {
|
|
textToBeInserted_.append(base::SysNSStringToUTF16(im_text));
|
|
} else {
|
|
gfx::Range replacement_range(replacementRange);
|
|
|
|
renderWidgetHostView_->render_widget_host()->ImeConfirmComposition(
|
|
base::SysNSStringToUTF16(im_text), replacement_range, false);
|
|
}
|
|
|
|
// Inserting text will delete all marked text automatically.
|
|
hasMarkedText_ = NO;
|
|
}
|
|
|
|
- (void)doCommandBySelector:(SEL)aSelector {
|
|
// An input method calls this function to dispatch an editing command to be
|
|
// handled by this view.
|
|
if (aSelector == @selector(noop:))
|
|
return;
|
|
std::string command([content::RenderWidgetHostViewMacEditCommandHelper::
|
|
CommandNameForSelector(aSelector) UTF8String]);
|
|
|
|
// If this method is called when handling a key down event, then we need to
|
|
// handle the command in the key event handler. Otherwise we can just handle
|
|
// it here.
|
|
if (handlingKeyDown_) {
|
|
hasEditCommands_ = YES;
|
|
// We ignore commands that insert characters, because this was causing
|
|
// strange behavior (e.g. tab always inserted a tab rather than moving to
|
|
// the next field on the page).
|
|
if (!StartsWithASCII(command, "insert", false))
|
|
editCommands_.push_back(content::EditCommand(command, ""));
|
|
} else {
|
|
renderWidgetHostView_->render_widget_host()->Send(
|
|
new InputMsg_ExecuteEditCommand(
|
|
renderWidgetHostView_->render_widget_host()->GetRoutingID(),
|
|
command, ""));
|
|
}
|
|
}
|
|
|
|
- (void)setMarkedText:(id)aString selectedRange:(NSRange)newSelRange
|
|
replacementRange:(NSRange)replacementRange {
|
|
// An input method updates the composition string.
|
|
// We send the given text and range to the renderer so it can update the
|
|
// composition node of WebKit.
|
|
|
|
BOOL isAttributedString = [aString isKindOfClass:[NSAttributedString class]];
|
|
NSString* im_text = isAttributedString ? [aString string] : aString;
|
|
int length = [im_text length];
|
|
|
|
// |markedRange_| will get set on a callback from ImeSetComposition().
|
|
selectedRange_ = newSelRange;
|
|
markedText_ = base::SysNSStringToUTF16(im_text);
|
|
hasMarkedText_ = (length > 0);
|
|
underlines_.clear();
|
|
|
|
if (isAttributedString) {
|
|
ExtractUnderlines(aString, &underlines_);
|
|
} else {
|
|
// Use a thin black underline by default.
|
|
underlines_.push_back(blink::WebCompositionUnderline(0, length,
|
|
SK_ColorBLACK, false, 0));
|
|
}
|
|
|
|
// If we are handling a key down event, then SetComposition() 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 input method state. (Our input method 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.)
|
|
if (!handlingKeyDown_) {
|
|
renderWidgetHostView_->render_widget_host()->ImeSetComposition(
|
|
markedText_, underlines_, newSelRange.location,
|
|
NSMaxRange(newSelRange));
|
|
}
|
|
}
|
|
|
|
- (void)unmarkText {
|
|
// Delete the composition node of the renderer and finish an ongoing
|
|
// composition.
|
|
// 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.
|
|
hasMarkedText_ = NO;
|
|
markedText_.clear();
|
|
underlines_.clear();
|
|
|
|
// If we are handling a key down event, then ConfirmComposition() will be
|
|
// called in keyEvent: method.
|
|
if (!handlingKeyDown_) {
|
|
renderWidgetHostView_->render_widget_host()->ImeConfirmComposition(
|
|
base::string16(), gfx::Range::InvalidRange(), false);
|
|
} else {
|
|
unmarkTextCalled_ = YES;
|
|
}
|
|
}
|
|
|
|
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range
|
|
actualRange:(NSRangePointer)actualRange {
|
|
if (actualRange)
|
|
*actualRange = range;
|
|
NSAttributedString* str = content::TextInputClientMac::GetInstance()->
|
|
GetAttributedSubstringFromRange(
|
|
renderWidgetHostView_->GetRenderWidgetHost(), range);
|
|
return str;
|
|
}
|
|
|
|
- (NSRect)firstViewRectForCharacterRange:(NSRange)theRange
|
|
actualRange:(NSRangePointer)actualRange {
|
|
NSRect rect;
|
|
gfx::Rect gfxRect;
|
|
gfx::Range range(theRange);
|
|
gfx::Range actual_range;
|
|
if (!renderWidgetHostView_->GetCachedFirstRectForCharacterRange(range,
|
|
&gfxRect, &actual_range)) {
|
|
rect = content::TextInputClientMac::GetInstance()->
|
|
GetFirstRectForRange(renderWidgetHostView_->GetRenderWidgetHost(),
|
|
range.ToNSRange());
|
|
|
|
if (actualRange)
|
|
*actualRange = range.ToNSRange();
|
|
} else {
|
|
rect = NSRectFromCGRect(gfxRect.ToCGRect());
|
|
}
|
|
|
|
return rect;
|
|
}
|
|
|
|
- (NSRect) screenRectFromViewRect:(NSRect)rect {
|
|
NSRect screenRect;
|
|
|
|
int screenX, screenY;
|
|
renderWidgetHostView_->browser_impl()->GetClient()->GetRenderHandler()->
|
|
GetScreenPoint(renderWidgetHostView_->browser_impl()->GetBrowser(),
|
|
rect.origin.x, rect.origin.y, screenX, screenY);
|
|
screenRect.origin = NSMakePoint(screenX, screenY);
|
|
screenRect.size = rect.size;
|
|
|
|
return screenRect;
|
|
}
|
|
|
|
- (NSRect)firstRectForCharacterRange:(NSRange)theRange
|
|
actualRange:(NSRangePointer)actualRange {
|
|
NSRect rect = [self firstViewRectForCharacterRange:theRange
|
|
actualRange:actualRange];
|
|
|
|
// Convert into screen coordinates for return.
|
|
rect = [self screenRectFromViewRect:rect];
|
|
|
|
if (rect.origin.y >= rect.size.height)
|
|
rect.origin.y -= rect.size.height;
|
|
else
|
|
rect.origin.y = 0;
|
|
|
|
return rect;
|
|
}
|
|
|
|
- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint {
|
|
// |thePoint| is in screen coordinates, but needs to be converted to WebKit
|
|
// coordinates (upper left origin). Scroll offsets will be taken care of in
|
|
// the renderer.
|
|
|
|
CefRect view_rect;
|
|
renderWidgetHostView_->browser_impl()->GetClient()->GetRenderHandler()->
|
|
GetViewRect(renderWidgetHostView_->browser_impl()->GetBrowser(),
|
|
view_rect);
|
|
|
|
thePoint.x -= view_rect.x;
|
|
thePoint.y -= view_rect.y;
|
|
thePoint.y = view_rect.height - thePoint.y;
|
|
|
|
NSUInteger index = content::TextInputClientMac::GetInstance()->
|
|
GetCharacterIndexAtPoint(renderWidgetHostView_->GetRenderWidgetHost(),
|
|
gfx::Point(thePoint.x, thePoint.y));
|
|
return index;
|
|
}
|
|
|
|
- (void)HandleKeyEventBeforeTextInputClient:(NSEvent*)keyEvent {
|
|
DCHECK([keyEvent type] == NSKeyDown);
|
|
// Don't call this method recursively.
|
|
DCHECK(!handlingKeyDown_);
|
|
|
|
oldHasMarkedText_ = hasMarkedText_;
|
|
handlingKeyDown_ = YES;
|
|
|
|
// These variables might be set when handling the keyboard event.
|
|
// Clear them here so that we can know whether they have changed afterwards.
|
|
textToBeInserted_.clear();
|
|
markedText_.clear();
|
|
underlines_.clear();
|
|
unmarkTextCalled_ = NO;
|
|
hasEditCommands_ = NO;
|
|
editCommands_.clear();
|
|
}
|
|
|
|
- (void)HandleKeyEventAfterTextInputClient:(NSEvent*)keyEvent {
|
|
handlingKeyDown_ = NO;
|
|
|
|
// 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 ConfirmComposition().
|
|
// 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.
|
|
|
|
if (!hasMarkedText_ && !oldHasMarkedText_ &&
|
|
textToBeInserted_.length() <= 1) {
|
|
content::NativeWebKeyboardEvent event(keyEvent);
|
|
if (textToBeInserted_.length() == 1) {
|
|
event.type = blink::WebInputEvent::Type::Char;
|
|
event.text[0] = textToBeInserted_[0];
|
|
event.text[1] = 0;
|
|
}
|
|
renderWidgetHostView_->SendKeyEvent(event);
|
|
}
|
|
|
|
BOOL textInserted = NO;
|
|
if (textToBeInserted_.length() >
|
|
((hasMarkedText_ || oldHasMarkedText_) ? 0u : 1u)) {
|
|
renderWidgetHostView_->render_widget_host()->ImeConfirmComposition(
|
|
textToBeInserted_, gfx::Range::InvalidRange(), false);
|
|
textToBeInserted_ = YES;
|
|
}
|
|
|
|
// Updates or cancels the composition. If some text has been inserted, then
|
|
// we don't need to cancel the composition explicitly.
|
|
if (hasMarkedText_ && markedText_.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.
|
|
renderWidgetHostView_->render_widget_host()->ImeSetComposition(
|
|
markedText_, underlines_, selectedRange_.location,
|
|
NSMaxRange(selectedRange_));
|
|
} else if (oldHasMarkedText_ && !hasMarkedText_ && !textInserted) {
|
|
if (unmarkTextCalled_) {
|
|
renderWidgetHostView_->render_widget_host()->ImeConfirmComposition(
|
|
base::string16(), gfx::Range::InvalidRange(), false);
|
|
} else {
|
|
renderWidgetHostView_->render_widget_host()->ImeCancelComposition();
|
|
}
|
|
}
|
|
}
|
|
|
|
- (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];
|
|
|
|
hasMarkedText_ = NO;
|
|
// Should not call [self unmarkText] here, because it'll send unnecessary
|
|
// cancel composition IPC message to the renderer.
|
|
}
|
|
|
|
@end
|