2016-10-28 18:11:24 +02:00
|
|
|
// Copyright 2016 The Chromium Embedded Framework Authors. Portions copyright
|
|
|
|
// 2013 The Chromium Authors. All rights reserved. Use of this source code is
|
|
|
|
// governed by a BSD-style license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
// Implementation based on
|
|
|
|
// content/browser/renderer_host/render_widget_host_view_mac.mm from Chromium.
|
|
|
|
|
2017-05-17 11:29:28 +02:00
|
|
|
#include "text_input_client_osr_mac.h"
|
2017-05-19 11:06:00 +02:00
|
|
|
#include "include/cef_client.h"
|
2016-10-28 18:11:24 +02:00
|
|
|
|
2017-05-17 11:29:28 +02:00
|
|
|
#define ColorBLACK 0xFF000000 // Same as Blink SKColor.
|
2016-10-28 18:11:24 +02:00
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
// TODO(suzhe): Upstream this function.
|
2017-05-17 11:29:28 +02:00
|
|
|
cef_color_t CefColorFromNSColor(NSColor* color) {
|
2016-10-28 18:11:24 +02:00
|
|
|
CGFloat r, g, b, a;
|
|
|
|
[color getRed:&r green:&g blue:&b alpha:&a];
|
|
|
|
|
2017-05-17 11:29:28 +02:00
|
|
|
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));
|
2016-10-28 18:11:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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<CefCompositionUnderline>* underlines) {
|
|
|
|
int length = [[string string] length];
|
|
|
|
int i = 0;
|
|
|
|
while (i < length) {
|
|
|
|
NSRange range;
|
|
|
|
NSDictionary* attrs = [string attributesAtIndex:i
|
2017-05-17 11:29:28 +02:00
|
|
|
longestEffectiveRange:&range
|
|
|
|
inRange:NSMakeRange(i, length - i)];
|
|
|
|
NSNumber* style = [attrs objectForKey:NSUnderlineStyleAttributeName];
|
2016-10-28 18:11:24 +02:00
|
|
|
if (style) {
|
|
|
|
cef_color_t color = ColorBLACK;
|
2017-05-17 11:29:28 +02:00
|
|
|
if (NSColor* colorAttr =
|
2016-10-28 18:11:24 +02:00
|
|
|
[attrs objectForKey:NSUnderlineColorAttributeName]) {
|
|
|
|
color = CefColorFromNSColor(
|
|
|
|
[colorAttr colorUsingColorSpaceName:NSDeviceRGBColorSpace]);
|
|
|
|
}
|
2021-07-23 18:40:13 +02:00
|
|
|
cef_composition_underline_t line = {{static_cast<int>(range.location),
|
|
|
|
static_cast<int>(NSMaxRange(range))},
|
|
|
|
color,
|
|
|
|
0,
|
|
|
|
[style intValue] > 1};
|
2016-10-28 18:11:24 +02:00
|
|
|
underlines->push_back(line);
|
|
|
|
}
|
|
|
|
i = range.location + range.length;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
extern NSString* NSTextInputReplacementRangeAttributeName;
|
|
|
|
}
|
|
|
|
|
|
|
|
@implementation CefTextInputClientOSRMac
|
|
|
|
|
|
|
|
@synthesize selectedRange = selectedRange_;
|
|
|
|
@synthesize handlingKeyDown = handlingKeyDown_;
|
|
|
|
|
|
|
|
- (id)initWithBrowser:(CefRefPtr<CefBrowser>)browser {
|
|
|
|
self = [super init];
|
|
|
|
browser_ = browser;
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2017-04-26 21:05:12 +02:00
|
|
|
- (void)detach {
|
2021-06-17 22:08:01 +02:00
|
|
|
browser_ = nullptr;
|
2017-04-26 21:05:12 +02:00
|
|
|
}
|
|
|
|
|
2016-10-28 18:11:24 +02:00
|
|
|
- (NSArray*)validAttributesForMarkedText {
|
|
|
|
if (!validAttributesForMarkedText_) {
|
2017-05-17 11:29:28 +02:00
|
|
|
validAttributesForMarkedText_ = [[NSArray alloc]
|
|
|
|
initWithObjects:NSUnderlineStyleAttributeName,
|
|
|
|
NSUnderlineColorAttributeName,
|
|
|
|
NSMarkedClauseSegmentAttributeName,
|
|
|
|
NSTextInputReplacementRangeAttributeName, nil];
|
2016-10-28 18:11:24 +02:00
|
|
|
}
|
|
|
|
return validAttributesForMarkedText_;
|
|
|
|
}
|
|
|
|
|
2016-11-10 22:13:14 +01:00
|
|
|
- (NSRange)selectedRange {
|
|
|
|
if (selectedRange_.location == NSNotFound || selectedRange_.length == 0)
|
|
|
|
return NSMakeRange(NSNotFound, 0);
|
|
|
|
return selectedRange_;
|
|
|
|
}
|
|
|
|
|
2016-10-28 18:11:24 +02:00
|
|
|
- (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([im_text UTF8String]);
|
|
|
|
} else {
|
2021-07-23 18:40:13 +02:00
|
|
|
cef_range_t range = {static_cast<int>(replacementRange.location),
|
|
|
|
static_cast<int>(NSMaxRange(replacementRange))};
|
2016-10-28 18:11:24 +02:00
|
|
|
browser_->GetHost()->ImeCommitText([im_text UTF8String], range, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)setMarkedText:(id)aString
|
|
|
|
selectedRange:(NSRange)newSelRange
|
|
|
|
replacementRange:(NSRange)replacementRange {
|
|
|
|
// An input method has updated the composition string. We send the given text
|
|
|
|
// and range to the browser so it can update the composition node of Blink.
|
|
|
|
|
|
|
|
BOOL isAttributedString = [aString isKindOfClass:[NSAttributedString class]];
|
|
|
|
NSString* im_text = isAttributedString ? [aString string] : aString;
|
|
|
|
int length = [im_text length];
|
|
|
|
|
|
|
|
// |markedRange_| will get set in a callback from ImeSetComposition().
|
|
|
|
selectedRange_ = newSelRange;
|
|
|
|
markedText_ = [im_text UTF8String];
|
|
|
|
hasMarkedText_ = (length > 0);
|
|
|
|
underlines_.clear();
|
|
|
|
|
|
|
|
if (isAttributedString) {
|
|
|
|
ExtractUnderlines(aString, &underlines_);
|
|
|
|
} else {
|
|
|
|
// Use a thin black underline by default.
|
2017-05-17 11:29:28 +02:00
|
|
|
cef_composition_underline_t line = {{0, length}, ColorBLACK, 0, false};
|
2016-10-28 18:11:24 +02:00
|
|
|
underlines_.push_back(line);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we are handling a key down event then ImeSetComposition() will be
|
|
|
|
// called from the keyEvent: method.
|
|
|
|
// Input methods of Mac use setMarkedText calls with empty text to cancel an
|
|
|
|
// ongoing composition. Our input method backend will automatically cancel an
|
|
|
|
// ongoing composition when we send empty text.
|
|
|
|
if (handlingKeyDown_) {
|
2021-07-23 18:40:13 +02:00
|
|
|
setMarkedTextReplacementRange_ = {
|
|
|
|
static_cast<int>(replacementRange.location),
|
|
|
|
static_cast<int>(NSMaxRange(replacementRange))};
|
2016-10-28 18:11:24 +02:00
|
|
|
} else if (!handlingKeyDown_) {
|
|
|
|
CefRange replacement_range(replacementRange.location,
|
|
|
|
NSMaxRange(replacementRange));
|
|
|
|
CefRange selection_range(newSelRange.location, NSMaxRange(newSelRange));
|
|
|
|
|
2017-05-17 11:29:28 +02:00
|
|
|
browser_->GetHost()->ImeSetComposition(markedText_, underlines_,
|
|
|
|
replacement_range, selection_range);
|
2016-10-28 18:11:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)unmarkText {
|
|
|
|
// Delete the composition node of the browser and finish an ongoing
|
|
|
|
// composition.
|
|
|
|
// It seems that, instead of calling this method, an input method will call
|
|
|
|
// the setMarkedText method with empty text to cancel ongoing composition.
|
|
|
|
// Implement this method even though we don't expect it to be called.
|
|
|
|
hasMarkedText_ = NO;
|
|
|
|
markedText_.clear();
|
|
|
|
underlines_.clear();
|
|
|
|
|
|
|
|
// If we are handling a key down event then ImeFinishComposingText() will be
|
|
|
|
// called from the keyEvent: method.
|
|
|
|
if (!handlingKeyDown_)
|
|
|
|
browser_->GetHost()->ImeFinishComposingText(false);
|
|
|
|
else
|
|
|
|
unmarkTextCalled_ = YES;
|
|
|
|
}
|
|
|
|
|
2017-05-17 11:29:28 +02:00
|
|
|
- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
|
|
|
|
actualRange:
|
|
|
|
(NSRangePointer)actualRange {
|
2016-10-28 18:11:24 +02:00
|
|
|
// Modify the attributed string if required.
|
|
|
|
// Not implemented here as we do not want to control the IME window view.
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSRect)firstViewRectForCharacterRange:(NSRange)theRange
|
2017-05-17 11:29:28 +02:00
|
|
|
actualRange:(NSRangePointer)actualRange {
|
2016-10-28 18:11:24 +02:00
|
|
|
NSRect rect;
|
|
|
|
|
|
|
|
NSUInteger location = theRange.location;
|
|
|
|
|
|
|
|
// If location is not specified fall back to the composition range start.
|
|
|
|
if (location == NSNotFound)
|
|
|
|
location = markedRange_.location;
|
|
|
|
|
|
|
|
// Offset location by the composition range start if required.
|
|
|
|
if (location >= markedRange_.location)
|
|
|
|
location -= markedRange_.location;
|
|
|
|
|
2017-05-17 11:29:28 +02:00
|
|
|
if (location < composition_bounds_.size()) {
|
2016-10-28 18:11:24 +02:00
|
|
|
const CefRect& rc = composition_bounds_[location];
|
|
|
|
rect = NSMakeRect(rc.x, rc.y, rc.width, rc.height);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (actualRange)
|
|
|
|
*actualRange = NSMakeRange(location, theRange.length);
|
|
|
|
|
|
|
|
return rect;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSRect)screenRectFromViewRect:(NSRect)rect {
|
|
|
|
NSRect screenRect;
|
|
|
|
|
|
|
|
int screenX, screenY;
|
|
|
|
browser_->GetHost()->GetClient()->GetRenderHandler()->GetScreenPoint(
|
|
|
|
browser_, 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 {
|
2021-06-17 22:08:01 +02:00
|
|
|
NSRect rect = [self firstViewRectForCharacterRange:theRange
|
|
|
|
actualRange:actualRange];
|
2016-10-28 18:11:24 +02:00
|
|
|
|
|
|
|
// 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 {
|
|
|
|
return NSNotFound;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (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();
|
|
|
|
setMarkedTextReplacementRange_ = CefRange(UINT32_MAX, UINT32_MAX);
|
|
|
|
unmarkTextCalled_ = NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)HandleKeyEventAfterTextInputClient:(CefKeyEvent)keyEvent {
|
|
|
|
handlingKeyDown_ = NO;
|
|
|
|
|
|
|
|
// Send keypress and/or composition related events.
|
|
|
|
// 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 the text to be inserted only contains 1 character then we can just send
|
|
|
|
// a keypress event.
|
|
|
|
if (!hasMarkedText_ && !oldHasMarkedText_ &&
|
|
|
|
textToBeInserted_.length() <= 1) {
|
|
|
|
keyEvent.type = KEYEVENT_KEYDOWN;
|
|
|
|
|
|
|
|
browser_->GetHost()->SendKeyEvent(keyEvent);
|
|
|
|
|
|
|
|
// Don't send a CHAR event for non-char keys like arrows, function keys and
|
|
|
|
// clear.
|
|
|
|
if (keyEvent.modifiers & (EVENTFLAG_IS_KEY_PAD)) {
|
2017-05-17 11:29:28 +02:00
|
|
|
if (keyEvent.native_key_code == 71)
|
2016-10-28 18:11:24 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
keyEvent.type = KEYEVENT_CHAR;
|
|
|
|
browser_->GetHost()->SendKeyEvent(keyEvent);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the text to be inserted contains multiple characters then send the text
|
|
|
|
// to the browser using ImeCommitText().
|
|
|
|
BOOL textInserted = NO;
|
|
|
|
if (textToBeInserted_.length() >
|
2017-05-17 11:29:28 +02:00
|
|
|
((hasMarkedText_ || oldHasMarkedText_) ? 0u : 1u)) {
|
|
|
|
browser_->GetHost()->ImeCommitText(textToBeInserted_,
|
|
|
|
CefRange(UINT32_MAX, UINT32_MAX), 0);
|
2016-10-28 18:11:24 +02:00
|
|
|
textToBeInserted_.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update or cancel the composition. If some text has been inserted then we
|
|
|
|
// don't need to explicitly cancel the composition.
|
|
|
|
if (hasMarkedText_ && markedText_.length()) {
|
|
|
|
// Update the composition by sending marked text to the browser.
|
|
|
|
// |selectedRange_| is the range being selected inside the marked text.
|
|
|
|
browser_->GetHost()->ImeSetComposition(
|
|
|
|
markedText_, underlines_, setMarkedTextReplacementRange_,
|
|
|
|
CefRange(selectedRange_.location, NSMaxRange(selectedRange_)));
|
|
|
|
} else if (oldHasMarkedText_ && !hasMarkedText_ && !textInserted) {
|
|
|
|
// There was no marked text or inserted text. Complete or cancel the
|
|
|
|
// composition.
|
|
|
|
if (unmarkTextCalled_)
|
|
|
|
browser_->GetHost()->ImeFinishComposingText(false);
|
|
|
|
else
|
|
|
|
browser_->GetHost()->ImeCancelComposition();
|
|
|
|
}
|
|
|
|
|
|
|
|
setMarkedTextReplacementRange_ = CefRange(UINT32_MAX, UINT32_MAX);
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)ChangeCompositionRange:(CefRange)range
|
2017-05-17 11:29:28 +02:00
|
|
|
character_bounds:(const CefRenderHandler::RectList&)bounds {
|
2016-10-28 18:11:24 +02:00
|
|
|
composition_range_ = range;
|
|
|
|
markedRange_ = NSMakeRange(range.from, range.to - range.from);
|
|
|
|
composition_bounds_ = bounds;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)cancelComposition {
|
|
|
|
if (!hasMarkedText_)
|
|
|
|
return;
|
|
|
|
|
2017-05-17 11:29:28 +02:00
|
|
|
// Cancel the ongoing composition. [NSInputManager markedTextAbandoned:]
|
|
|
|
// doesn't call any NSTextInput functions, such as setMarkedText or
|
|
|
|
// insertText.
|
|
|
|
// TODO(erikchen): NSInputManager is deprecated since OSX 10.6. Switch to
|
|
|
|
// NSTextInputContext. http://www.crbug.com/479010.
|
2016-10-28 18:11:24 +02:00
|
|
|
#pragma clang diagnostic push
|
|
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
|
|
NSInputManager* currentInputManager = [NSInputManager currentInputManager];
|
|
|
|
[currentInputManager markedTextAbandoned:self];
|
|
|
|
#pragma clang diagnostic pop
|
|
|
|
|
|
|
|
hasMarkedText_ = NO;
|
|
|
|
// Should not call [self unmarkText] here because it'll send unnecessary
|
|
|
|
// cancel composition messages to the browser.
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|