• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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