1// Copyright 2016 The Chromium Embedded Framework Authors. Portions copyright 2// 2013 The Chromium Authors. All rights reserved. Use of this source code is 3// governed by a BSD-style license that can be found in the LICENSE file. 4 5// Implementation based on 6// content/browser/renderer_host/render_widget_host_view_mac.mm from Chromium. 7 8#include "text_input_client_osr_mac.h" 9#include "include/cef_client.h" 10 11#define ColorBLACK 0xFF000000 // Same as Blink SKColor. 12 13namespace { 14 15// TODO(suzhe): Upstream this function. 16cef_color_t CefColorFromNSColor(NSColor* color) { 17 CGFloat r, g, b, a; 18 [color getRed:&r green:&g blue:&b alpha:&a]; 19 20 return std::max(0, std::min(static_cast<int>(lroundf(255.0f * a)), 255)) 21 << 24 | 22 std::max(0, std::min(static_cast<int>(lroundf(255.0f * r)), 255)) 23 << 16 | 24 std::max(0, std::min(static_cast<int>(lroundf(255.0f * g)), 255)) 25 << 8 | 26 std::max(0, std::min(static_cast<int>(lroundf(255.0f * b)), 255)); 27} 28 29// Extract underline information from an attributed string. Mostly copied from 30// third_party/WebKit/Source/WebKit/mac/WebView/WebHTMLView.mm 31void ExtractUnderlines(NSAttributedString* string, 32 std::vector<CefCompositionUnderline>* underlines) { 33 int length = [[string string] length]; 34 int i = 0; 35 while (i < length) { 36 NSRange range; 37 NSDictionary* attrs = [string attributesAtIndex:i 38 longestEffectiveRange:&range 39 inRange:NSMakeRange(i, length - i)]; 40 NSNumber* style = [attrs objectForKey:NSUnderlineStyleAttributeName]; 41 if (style) { 42 cef_color_t color = ColorBLACK; 43 if (NSColor* colorAttr = 44 [attrs objectForKey:NSUnderlineColorAttributeName]) { 45 color = CefColorFromNSColor( 46 [colorAttr colorUsingColorSpaceName:NSDeviceRGBColorSpace]); 47 } 48 cef_composition_underline_t line = {{static_cast<int>(range.location), 49 static_cast<int>(NSMaxRange(range))}, 50 color, 51 0, 52 [style intValue] > 1}; 53 underlines->push_back(line); 54 } 55 i = range.location + range.length; 56 } 57} 58 59} // namespace 60 61extern "C" { 62extern NSString* NSTextInputReplacementRangeAttributeName; 63} 64 65@implementation CefTextInputClientOSRMac 66 67@synthesize selectedRange = selectedRange_; 68@synthesize handlingKeyDown = handlingKeyDown_; 69 70- (id)initWithBrowser:(CefRefPtr<CefBrowser>)browser { 71 self = [super init]; 72 browser_ = browser; 73 return self; 74} 75 76- (void)detach { 77 browser_ = nullptr; 78} 79 80- (NSArray*)validAttributesForMarkedText { 81 if (!validAttributesForMarkedText_) { 82 validAttributesForMarkedText_ = [[NSArray alloc] 83 initWithObjects:NSUnderlineStyleAttributeName, 84 NSUnderlineColorAttributeName, 85 NSMarkedClauseSegmentAttributeName, 86 NSTextInputReplacementRangeAttributeName, nil]; 87 } 88 return validAttributesForMarkedText_; 89} 90 91- (NSRange)selectedRange { 92 if (selectedRange_.location == NSNotFound || selectedRange_.length == 0) 93 return NSMakeRange(NSNotFound, 0); 94 return selectedRange_; 95} 96 97- (NSRange)markedRange { 98 return hasMarkedText_ ? markedRange_ : NSMakeRange(NSNotFound, 0); 99} 100 101- (BOOL)hasMarkedText { 102 return hasMarkedText_; 103} 104 105- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange { 106 BOOL isAttributedString = [aString isKindOfClass:[NSAttributedString class]]; 107 NSString* im_text = isAttributedString ? [aString string] : aString; 108 if (handlingKeyDown_) { 109 textToBeInserted_.append([im_text UTF8String]); 110 } else { 111 cef_range_t range = {static_cast<int>(replacementRange.location), 112 static_cast<int>(NSMaxRange(replacementRange))}; 113 browser_->GetHost()->ImeCommitText([im_text UTF8String], range, 0); 114 } 115 116 // Inserting text will delete all marked text automatically. 117 hasMarkedText_ = NO; 118} 119 120- (void)doCommandBySelector:(SEL)aSelector { 121 // An input method calls this function to dispatch an editing command to be 122 // handled by this view. 123} 124 125- (void)setMarkedText:(id)aString 126 selectedRange:(NSRange)newSelRange 127 replacementRange:(NSRange)replacementRange { 128 // An input method has updated the composition string. We send the given text 129 // and range to the browser so it can update the composition node of Blink. 130 131 BOOL isAttributedString = [aString isKindOfClass:[NSAttributedString class]]; 132 NSString* im_text = isAttributedString ? [aString string] : aString; 133 int length = [im_text length]; 134 135 // |markedRange_| will get set in a callback from ImeSetComposition(). 136 selectedRange_ = newSelRange; 137 markedText_ = [im_text UTF8String]; 138 hasMarkedText_ = (length > 0); 139 underlines_.clear(); 140 141 if (isAttributedString) { 142 ExtractUnderlines(aString, &underlines_); 143 } else { 144 // Use a thin black underline by default. 145 cef_composition_underline_t line = {{0, length}, ColorBLACK, 0, false}; 146 underlines_.push_back(line); 147 } 148 149 // If we are handling a key down event then ImeSetComposition() will be 150 // called from the keyEvent: method. 151 // Input methods of Mac use setMarkedText calls with empty text to cancel an 152 // ongoing composition. Our input method backend will automatically cancel an 153 // ongoing composition when we send empty text. 154 if (handlingKeyDown_) { 155 setMarkedTextReplacementRange_ = { 156 static_cast<int>(replacementRange.location), 157 static_cast<int>(NSMaxRange(replacementRange))}; 158 } else if (!handlingKeyDown_) { 159 CefRange replacement_range(replacementRange.location, 160 NSMaxRange(replacementRange)); 161 CefRange selection_range(newSelRange.location, NSMaxRange(newSelRange)); 162 163 browser_->GetHost()->ImeSetComposition(markedText_, underlines_, 164 replacement_range, selection_range); 165 } 166} 167 168- (void)unmarkText { 169 // Delete the composition node of the browser and finish an ongoing 170 // composition. 171 // It seems that, instead of calling this method, an input method will call 172 // the setMarkedText method with empty text to cancel ongoing composition. 173 // Implement this method even though we don't expect it to be called. 174 hasMarkedText_ = NO; 175 markedText_.clear(); 176 underlines_.clear(); 177 178 // If we are handling a key down event then ImeFinishComposingText() will be 179 // called from the keyEvent: method. 180 if (!handlingKeyDown_) 181 browser_->GetHost()->ImeFinishComposingText(false); 182 else 183 unmarkTextCalled_ = YES; 184} 185 186- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range 187 actualRange: 188 (NSRangePointer)actualRange { 189 // Modify the attributed string if required. 190 // Not implemented here as we do not want to control the IME window view. 191 return nil; 192} 193 194- (NSRect)firstViewRectForCharacterRange:(NSRange)theRange 195 actualRange:(NSRangePointer)actualRange { 196 NSRect rect; 197 198 NSUInteger location = theRange.location; 199 200 // If location is not specified fall back to the composition range start. 201 if (location == NSNotFound) 202 location = markedRange_.location; 203 204 // Offset location by the composition range start if required. 205 if (location >= markedRange_.location) 206 location -= markedRange_.location; 207 208 if (location < composition_bounds_.size()) { 209 const CefRect& rc = composition_bounds_[location]; 210 rect = NSMakeRect(rc.x, rc.y, rc.width, rc.height); 211 } 212 213 if (actualRange) 214 *actualRange = NSMakeRange(location, theRange.length); 215 216 return rect; 217} 218 219- (NSRect)screenRectFromViewRect:(NSRect)rect { 220 NSRect screenRect; 221 222 int screenX, screenY; 223 browser_->GetHost()->GetClient()->GetRenderHandler()->GetScreenPoint( 224 browser_, rect.origin.x, rect.origin.y, screenX, screenY); 225 screenRect.origin = NSMakePoint(screenX, screenY); 226 screenRect.size = rect.size; 227 228 return screenRect; 229} 230 231- (NSRect)firstRectForCharacterRange:(NSRange)theRange 232 actualRange:(NSRangePointer)actualRange { 233 NSRect rect = [self firstViewRectForCharacterRange:theRange 234 actualRange:actualRange]; 235 236 // Convert into screen coordinates for return. 237 rect = [self screenRectFromViewRect:rect]; 238 239 if (rect.origin.y >= rect.size.height) 240 rect.origin.y -= rect.size.height; 241 else 242 rect.origin.y = 0; 243 244 return rect; 245} 246 247- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint { 248 return NSNotFound; 249} 250 251- (void)HandleKeyEventBeforeTextInputClient:(NSEvent*)keyEvent { 252 DCHECK([keyEvent type] == NSKeyDown); 253 // Don't call this method recursively. 254 DCHECK(!handlingKeyDown_); 255 256 oldHasMarkedText_ = hasMarkedText_; 257 handlingKeyDown_ = YES; 258 259 // These variables might be set when handling the keyboard event. 260 // Clear them here so that we can know whether they have changed afterwards. 261 textToBeInserted_.clear(); 262 markedText_.clear(); 263 underlines_.clear(); 264 setMarkedTextReplacementRange_ = CefRange(UINT32_MAX, UINT32_MAX); 265 unmarkTextCalled_ = NO; 266} 267 268- (void)HandleKeyEventAfterTextInputClient:(CefKeyEvent)keyEvent { 269 handlingKeyDown_ = NO; 270 271 // Send keypress and/or composition related events. 272 // Note that |textToBeInserted_| is a UTF-16 string but it's fine to only 273 // handle BMP characters here as we can always insert non-BMP characters as 274 // text. 275 276 // If the text to be inserted only contains 1 character then we can just send 277 // a keypress event. 278 if (!hasMarkedText_ && !oldHasMarkedText_ && 279 textToBeInserted_.length() <= 1) { 280 keyEvent.type = KEYEVENT_KEYDOWN; 281 282 browser_->GetHost()->SendKeyEvent(keyEvent); 283 284 // Don't send a CHAR event for non-char keys like arrows, function keys and 285 // clear. 286 if (keyEvent.modifiers & (EVENTFLAG_IS_KEY_PAD)) { 287 if (keyEvent.native_key_code == 71) 288 return; 289 } 290 291 keyEvent.type = KEYEVENT_CHAR; 292 browser_->GetHost()->SendKeyEvent(keyEvent); 293 } 294 295 // If the text to be inserted contains multiple characters then send the text 296 // to the browser using ImeCommitText(). 297 BOOL textInserted = NO; 298 if (textToBeInserted_.length() > 299 ((hasMarkedText_ || oldHasMarkedText_) ? 0u : 1u)) { 300 browser_->GetHost()->ImeCommitText(textToBeInserted_, 301 CefRange(UINT32_MAX, UINT32_MAX), 0); 302 textToBeInserted_.clear(); 303 } 304 305 // Update or cancel the composition. If some text has been inserted then we 306 // don't need to explicitly cancel the composition. 307 if (hasMarkedText_ && markedText_.length()) { 308 // Update the composition by sending marked text to the browser. 309 // |selectedRange_| is the range being selected inside the marked text. 310 browser_->GetHost()->ImeSetComposition( 311 markedText_, underlines_, setMarkedTextReplacementRange_, 312 CefRange(selectedRange_.location, NSMaxRange(selectedRange_))); 313 } else if (oldHasMarkedText_ && !hasMarkedText_ && !textInserted) { 314 // There was no marked text or inserted text. Complete or cancel the 315 // composition. 316 if (unmarkTextCalled_) 317 browser_->GetHost()->ImeFinishComposingText(false); 318 else 319 browser_->GetHost()->ImeCancelComposition(); 320 } 321 322 setMarkedTextReplacementRange_ = CefRange(UINT32_MAX, UINT32_MAX); 323} 324 325- (void)ChangeCompositionRange:(CefRange)range 326 character_bounds:(const CefRenderHandler::RectList&)bounds { 327 composition_range_ = range; 328 markedRange_ = NSMakeRange(range.from, range.to - range.from); 329 composition_bounds_ = bounds; 330} 331 332- (void)cancelComposition { 333 if (!hasMarkedText_) 334 return; 335 336// Cancel the ongoing composition. [NSInputManager markedTextAbandoned:] 337// doesn't call any NSTextInput functions, such as setMarkedText or 338// insertText. 339// TODO(erikchen): NSInputManager is deprecated since OSX 10.6. Switch to 340// NSTextInputContext. http://www.crbug.com/479010. 341#pragma clang diagnostic push 342#pragma clang diagnostic ignored "-Wdeprecated-declarations" 343 NSInputManager* currentInputManager = [NSInputManager currentInputManager]; 344 [currentInputManager markedTextAbandoned:self]; 345#pragma clang diagnostic pop 346 347 hasMarkedText_ = NO; 348 // Should not call [self unmarkText] here because it'll send unnecessary 349 // cancel composition messages to the browser. 350} 351 352@end 353