| // 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. |
| |
| #include "text_input_client_osr_mac.h" |
| #include "include/cef_client.h" |
| |
| #define ColorBLACK 0xFF000000 // Same as Blink SKColor. |
| |
| namespace { |
| |
| // TODO(suzhe): Upstream this function. |
| cef_color_t CefColorFromNSColor(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<CefCompositionUnderline>* 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) { |
| cef_color_t color = ColorBLACK; |
| if (NSColor* colorAttr = |
| [attrs objectForKey:NSUnderlineColorAttributeName]) { |
| color = CefColorFromNSColor( |
| [colorAttr colorUsingColorSpaceName:NSDeviceRGBColorSpace]); |
| } |
| cef_composition_underline_t line = { |
| {range.location, NSMaxRange(range)}, color, 0, [style intValue] > 1}; |
| 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; |
| } |
| |
| - (void)detach { |
| browser_ = NULL; |
| } |
| |
| - (NSArray*)validAttributesForMarkedText { |
| if (!validAttributesForMarkedText_) { |
| validAttributesForMarkedText_ = [[NSArray alloc] |
| initWithObjects:NSUnderlineStyleAttributeName, |
| NSUnderlineColorAttributeName, |
| NSMarkedClauseSegmentAttributeName, |
| NSTextInputReplacementRangeAttributeName, nil]; |
| } |
| return validAttributesForMarkedText_; |
| } |
| |
| - (NSRange)selectedRange { |
| if (selectedRange_.location == NSNotFound || selectedRange_.length == 0) |
| return NSMakeRange(NSNotFound, 0); |
| return selectedRange_; |
| } |
| |
| - (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 { |
| cef_range_t range = {replacementRange.location, |
| NSMaxRange(replacementRange)}; |
| 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. |
| cef_composition_underline_t line = {{0, length}, ColorBLACK, 0, false}; |
| 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_) { |
| setMarkedTextReplacementRange_ = {replacementRange.location, |
| NSMaxRange(replacementRange)}; |
| } else if (!handlingKeyDown_) { |
| CefRange replacement_range(replacementRange.location, |
| NSMaxRange(replacementRange)); |
| CefRange selection_range(newSelRange.location, NSMaxRange(newSelRange)); |
| |
| browser_->GetHost()->ImeSetComposition(markedText_, underlines_, |
| replacement_range, selection_range); |
| } |
| } |
| |
| - (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; |
| } |
| |
| - (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range |
| actualRange: |
| (NSRangePointer)actualRange { |
| // 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 |
| actualRange:(NSRangePointer)actualRange { |
| 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; |
| |
| if (location < composition_bounds_.size()) { |
| 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 { |
| 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 { |
| 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)) { |
| if (keyEvent.native_key_code == 71) |
| 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() > |
| ((hasMarkedText_ || oldHasMarkedText_) ? 0u : 1u)) { |
| browser_->GetHost()->ImeCommitText(textToBeInserted_, |
| CefRange(UINT32_MAX, UINT32_MAX), 0); |
| 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 |
| character_bounds:(const CefRenderHandler::RectList&)bounds { |
| composition_range_ = range; |
| markedRange_ = NSMakeRange(range.from, range.to - range.from); |
| composition_bounds_ = bounds; |
| } |
| |
| - (void)cancelComposition { |
| if (!hasMarkedText_) |
| return; |
| |
| // 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. |
| #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 |