mirror of
				https://bitbucket.org/chromiumembedded/cef
				synced 2025-06-05 21:39:12 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			349 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			349 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| // 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
 |