Chromium Code Reviews| Index: chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm |
| diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm |
| index 83126cf2e5eee10622c834fac7b0c7ca7142aed2..34e49af86680cbf4eef2cf914a9f7995c773bbd7 100644 |
| --- a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm |
| +++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm |
| @@ -4,6 +4,8 @@ |
| #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h" |
| +#include "base/i18n/break_iterator.h" |
| +#include "base/mac/foundation_util.h" |
| #include "base/mac/sdk_forward_declarations.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| @@ -49,13 +51,58 @@ BOOL ThePasteboardIsTooDamnBig() { |
| return [[pb stringForType:type] length] > kMaxPasteLength; |
| } |
| +NSRange VisualSelectedRangeFromRange(NSRange range, NSString* string) { |
| + if (range.location >= string.length) |
| + return range; |
| + base::string16 text = base::SysNSStringToUTF16(string); |
| + base::i18n::BreakIterator grapheme_iterator( |
| + text, base::i18n::BreakIterator::BREAK_CHARACTER); |
| + if (!grapheme_iterator.Init()) |
| + return range; |
| + // This works because NSString uses UTF-16 code units. |
| + while (range.location < text.length() && |
| + !grapheme_iterator.IsGraphemeBoundary( |
| + static_cast<size_t>(range.location))) { |
| + range.location++; |
| + if (range.length > 0) |
| + range.length--; |
| + } |
| + return range; |
| +} |
| + |
| } // namespace |
| +// Method exposed for the purpose of overriding. |
| +// Used to restore model's selection range when the view doesn't |
| +// match the model due to combining characters. |
| +// |
| +// In some cases, (completing 'y' to 'ÿour', for example), the autocomplete |
| +// system requests a selection range that begins on a combining character. |
| +// setSelectedRange: and friends document that the range passed to them |
| +// "must begin and end on glyph boundaries and not split base glyphs and |
| +// their nonspacing marks". If passed such a range, the selection is |
| +// expanded to include the original user input, preventing the user |
| +// from being able to type other words beginning with that letter. |
| +// |
| +// To resolve this, the field editor modifies the selection to start |
| +// on the next glyph boundary, then keeps track of the original and |
| +// modified selections, substituting the original when the user takes |
| +// actions that operate on the selection. Since there are many methods |
| +// in NSResponder (for example deleteToBeginningOfLine:) that operate |
| +// on the selection, rather than shimming them all, we override this |
| +// private method that they're implemented in terms of. |
| +// |
| +@interface NSTextView (PrivateTextEditing) |
| +- (void)_userReplaceRange:(NSRange)range withString:(NSString*)string; |
| +@end |
| + |
| @interface AutocompleteTextFieldEditor ()<NSDraggingSource> |
| @end |
| @implementation AutocompleteTextFieldEditor |
| +@synthesize actualSelectedRange = actualSelectedRange_; |
| + |
| - (BOOL)shouldDrawInsertionPoint { |
| return [super shouldDrawInsertionPoint] && |
| ![[[self delegate] cell] hideFocusState]; |
| @@ -400,6 +447,14 @@ BOOL ThePasteboardIsTooDamnBig() { |
| return [dropHandler_ performDragOperation:sender]; |
| } |
| +- (void)_userReplaceRange:(NSRange)range withString:(NSString*)string { |
| + if (NSEqualRanges(visualSelectedRange_, range) && |
| + !NSEqualRanges(visualSelectedRange_, actualSelectedRange_)) { |
| + range = actualSelectedRange_; |
| + } |
| + [super _userReplaceRange:range withString:string]; |
| +} |
| + |
| // Prevent control characters from being entered into the Omnibox. |
| // This is invoked for keyboard entry, not for pasting. |
| - (void)insertText:(id)aString { |
| @@ -431,11 +486,15 @@ BOOL ThePasteboardIsTooDamnBig() { |
| DCHECK_EQ(range.length, 0U); |
| } |
| - // NOTE: If |aString| is empty, this intentionally replaces the |
| - // selection with empty. This seems consistent with the case where |
| - // the input contained a mixture of characters and the string ended |
| - // up not empty. |
| - [super insertText:aString]; |
| + if (!NSEqualRanges(visualSelectedRange_, actualSelectedRange_)) { |
| + [super replaceCharactersInRange:actualSelectedRange_ withString:aString]; |
| + } else { |
| + // NOTE: If |aString| is empty, this intentionally replaces the |
| + // selection with empty. This seems consistent with the case where |
| + // the input contained a mixture of characters and the string ended |
| + // up not empty. |
| + [super insertText:aString]; |
| + } |
| } |
| - (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange |
| @@ -459,6 +518,24 @@ BOOL ThePasteboardIsTooDamnBig() { |
| textChangedByKeyEvents_ = YES; |
| } |
| +- (void)setSelectedRanges:(NSArray*)ranges |
| + affinity:(NSSelectionAffinity)affinity |
| + stillSelecting:(BOOL)flag { |
| + DCHECK(ranges.count > 0); |
| + NSMutableArray* mutableRanges = [ranges mutableCopy]; |
|
erikchen
2016/10/14 21:06:07
gotta throw this in a scoped_nsobject to avoid a l
lgrey
2016/10/17 13:30:28
Thanks, I'm a little rusty on pre-ARC :) Done
|
| + // |ranges| is sorted, and empirically, the first range passed is returned |
| + // as selectedRange. |
| + NSRange firstRange = [base::mac::ObjCCastStrict<NSValue>( |
| + [mutableRanges firstObject]) rangeValue]; |
| + actualSelectedRange_ = firstRange; |
| + visualSelectedRange_ = |
| + VisualSelectedRangeFromRange(firstRange, [self string]); |
| + NSValue* boxedVisualRange = [NSValue valueWithRange:visualSelectedRange_]; |
| + [mutableRanges replaceObjectAtIndex:0 withObject:boxedVisualRange]; |
| + |
| + [super setSelectedRanges:mutableRanges affinity:affinity stillSelecting:flag]; |
| +} |
| + |
| - (void)interpretKeyEvents:(NSArray *)eventArray { |
| DCHECK(!interpretingKeyEvents_); |
| interpretingKeyEvents_ = YES; |
| @@ -564,7 +641,7 @@ BOOL ThePasteboardIsTooDamnBig() { |
| - (BOOL)validateMenuItem:(NSMenuItem*)item { |
| if ([item action] == @selector(copyToFindPboard:)) |
| - return [self selectedRange].length > 0; |
| + return actualSelectedRange_.length > 0; |
| if ([item action] == @selector(pasteAndGo:)) { |
| // TODO(rohitrao): If the clipboard is empty, should we show a |
| // greyed-out "Paste and Go" or nothing at all? |
| @@ -576,11 +653,10 @@ BOOL ThePasteboardIsTooDamnBig() { |
| } |
| - (void)copyToFindPboard:(id)sender { |
| - NSRange selectedRange = [self selectedRange]; |
| - if (selectedRange.length == 0) |
| + if (actualSelectedRange_.length == 0) |
| return; |
| NSAttributedString* selection = |
| - [self attributedSubstringForProposedRange:selectedRange |
| + [self attributedSubstringForProposedRange:actualSelectedRange_ |
| actualRange:NULL]; |
| if (!selection) |
| return; |