Home | History | Annotate | Download | only in location_bar
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
      6 
      7 #include "base/strings/string_util.h"
      8 #include "base/strings/sys_string_conversions.h"
      9 #include "chrome/app/chrome_command_ids.h"  // IDC_*
     10 #include "chrome/browser/ui/browser_list.h"
     11 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
     12 #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
     13 #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
     14 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
     15 #include "chrome/grit/generated_resources.h"
     16 #import "ui/base/cocoa/find_pasteboard.h"
     17 #include "ui/base/l10n/l10n_util_mac.h"
     18 
     19 namespace {
     20 
     21 // When too much data is put into a single-line text field, things get
     22 // janky due to the cost of computing the blink rect.  Sometimes users
     23 // accidentally paste large amounts, so place a limit on what will be
     24 // accepted.
     25 //
     26 // 10k characters was arbitrarily chosen by seeing how much a text
     27 // field could handle in a single line before it started getting too
     28 // janky to recover from (jankiness was detectable around 5k).
     29 // www.google.com returns an error for searches around 2k characters,
     30 // so this is conservative.
     31 const NSUInteger kMaxPasteLength = 10000;
     32 
     33 // Returns |YES| if too much text would be pasted.
     34 BOOL ThePasteboardIsTooDamnBig() {
     35   NSPasteboard* pb = [NSPasteboard generalPasteboard];
     36   NSString* type =
     37       [pb availableTypeFromArray:[NSArray arrayWithObject:NSStringPboardType]];
     38   if (!type)
     39     return NO;
     40 
     41   return [[pb stringForType:type] length] > kMaxPasteLength;
     42 }
     43 
     44 }  // namespace
     45 
     46 @implementation AutocompleteTextFieldEditor
     47 
     48 - (BOOL)shouldDrawInsertionPoint {
     49   return [super shouldDrawInsertionPoint] &&
     50          ![[[self delegate] cell] hideFocusState];
     51 }
     52 
     53 - (id)initWithFrame:(NSRect)frameRect {
     54   if ((self = [super initWithFrame:frameRect])) {
     55     dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
     56 
     57     forbiddenCharacters_.reset([[NSCharacterSet controlCharacterSet] retain]);
     58 
     59     // These checks seem inappropriate to the omnibox, and also
     60     // unlikely to work reliably due to our autocomplete interfering.
     61     //
     62     // Also see <http://crbug.com/173405>.
     63     NSTextCheckingTypes checkingTypes = [self enabledTextCheckingTypes];
     64     checkingTypes &= ~NSTextCheckingTypeReplacement;
     65     checkingTypes &= ~NSTextCheckingTypeCorrection;
     66     [self setEnabledTextCheckingTypes:checkingTypes];
     67   }
     68   return self;
     69 }
     70 
     71 // If the entire field is selected, drag the same data as would be
     72 // dragged from the field's location icon.  In some cases the textual
     73 // contents will not contain relevant data (for instance, "http://" is
     74 // stripped from URLs).
     75 - (BOOL)dragSelectionWithEvent:(NSEvent *)event
     76                         offset:(NSSize)mouseOffset
     77                      slideBack:(BOOL)slideBack {
     78   AutocompleteTextFieldObserver* observer = [self observer];
     79   DCHECK(observer);
     80   if (observer && observer->CanCopy()) {
     81     NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
     82     observer->CopyToPasteboard(pboard);
     83 
     84     NSPoint p;
     85     NSImage* image = [self dragImageForSelectionWithEvent:event origin:&p];
     86 
     87     [self dragImage:image
     88                  at:p
     89              offset:mouseOffset
     90               event:event
     91          pasteboard:pboard
     92              source:self
     93           slideBack:slideBack];
     94     return YES;
     95   }
     96   return [super dragSelectionWithEvent:event
     97                                 offset:mouseOffset
     98                              slideBack:slideBack];
     99 }
    100 
    101 - (void)copy:(id)sender {
    102   AutocompleteTextFieldObserver* observer = [self observer];
    103   DCHECK(observer);
    104   if (observer && observer->CanCopy())
    105     observer->CopyToPasteboard([NSPasteboard generalPasteboard]);
    106 }
    107 
    108 - (void)cut:(id)sender {
    109   [self copy:sender];
    110   [self delete:nil];
    111 }
    112 
    113 - (void)showURL:(id)sender {
    114   AutocompleteTextFieldObserver* observer = [self observer];
    115   DCHECK(observer);
    116   observer->ShowURL();
    117 }
    118 
    119 // This class assumes that the delegate is an AutocompleteTextField.
    120 // Enforce that assumption.
    121 - (AutocompleteTextField*)delegate {
    122   AutocompleteTextField* delegate =
    123       static_cast<AutocompleteTextField*>([super delegate]);
    124   DCHECK(delegate == nil ||
    125          [delegate isKindOfClass:[AutocompleteTextField class]]);
    126   return delegate;
    127 }
    128 
    129 - (void)setDelegate:(AutocompleteTextField*)delegate {
    130   DCHECK(delegate == nil ||
    131          [delegate isKindOfClass:[AutocompleteTextField class]]);
    132 
    133   // Unregister from any previously registered undo and redo notifications.
    134   NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
    135   [nc removeObserver:self
    136                 name:NSUndoManagerDidUndoChangeNotification
    137               object:nil];
    138   [nc removeObserver:self
    139                 name:NSUndoManagerDidRedoChangeNotification
    140               object:nil];
    141 
    142   // Set the delegate.
    143   [super setDelegate:delegate];
    144 
    145   // Register for undo and redo notifications from the new |delegate|, if it is
    146   // non-nil.
    147   if ([self delegate]) {
    148     NSUndoManager* undo_manager = [self undoManager];
    149     [nc addObserver:self
    150            selector:@selector(didUndoOrRedo:)
    151                name:NSUndoManagerDidUndoChangeNotification
    152              object:undo_manager];
    153     [nc addObserver:self
    154            selector:@selector(didUndoOrRedo:)
    155                name:NSUndoManagerDidRedoChangeNotification
    156              object:undo_manager];
    157   }
    158 }
    159 
    160 - (void)didUndoOrRedo:(NSNotification *)aNotification {
    161   AutocompleteTextFieldObserver* observer = [self observer];
    162   if (observer)
    163     observer->OnDidChange();
    164 }
    165 
    166 // Convenience method for retrieving the observer from the delegate.
    167 - (AutocompleteTextFieldObserver*)observer {
    168   return [[self delegate] observer];
    169 }
    170 
    171 - (void)paste:(id)sender {
    172   if (ThePasteboardIsTooDamnBig()) {
    173     NSBeep();
    174     return;
    175   }
    176 
    177   AutocompleteTextFieldObserver* observer = [self observer];
    178   DCHECK(observer);
    179   if (observer) {
    180     observer->OnPaste();
    181   }
    182 }
    183 
    184 - (void)pasteAndMatchStyle:(id)sender {
    185   [self paste:sender];
    186 }
    187 
    188 - (void)pasteAndGo:sender {
    189   if (ThePasteboardIsTooDamnBig()) {
    190     NSBeep();
    191     return;
    192   }
    193 
    194   AutocompleteTextFieldObserver* observer = [self observer];
    195   DCHECK(observer);
    196   if (observer) {
    197     observer->OnPasteAndGo();
    198   }
    199 }
    200 
    201 // We have rich text, but it shouldn't be modified by the user, so
    202 // don't update the font panel.  In theory, -setUsesFontPanel: should
    203 // accomplish this, but that gets called frequently with YES when
    204 // NSTextField and NSTextView synchronize their contents.  That is
    205 // probably unavoidable because in most cases having rich text in the
    206 // field you probably would expect it to update the font panel.
    207 - (void)updateFontPanel {}
    208 
    209 // No ruler bar, so don't update any of that state, either.
    210 - (void)updateRuler {}
    211 
    212 - (NSMenu*)menuForEvent:(NSEvent*)event {
    213   // Give the control a chance to provide page-action menus.
    214   // NOTE: Note that page actions aren't even in the editor's
    215   // boundaries!  The Cocoa control implementation seems to do a
    216   // blanket forward to here if nothing more specific is returned from
    217   // the control and cell calls.
    218   // TODO(shess): Determine if the page-action part of this can be
    219   // moved to the cell.
    220   NSMenu* actionMenu = [[self delegate] decorationMenuForEvent:event];
    221   if (actionMenu)
    222     return actionMenu;
    223 
    224   NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"TITLE"] autorelease];
    225   [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CUT)
    226                   action:@selector(cut:)
    227            keyEquivalent:@""];
    228   [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_COPY)
    229                   action:@selector(copy:)
    230            keyEquivalent:@""];
    231 
    232   [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_PASTE)
    233                   action:@selector(paste:)
    234            keyEquivalent:@""];
    235 
    236   // TODO(shess): If the control is not editable, should we show a
    237   // greyed-out "Paste and Go"?
    238   if ([self isEditable]) {
    239     // Paste and go/search.
    240     AutocompleteTextFieldObserver* observer = [self observer];
    241     DCHECK(observer);
    242     if (!ThePasteboardIsTooDamnBig()) {
    243       NSString* pasteAndGoLabel =
    244           l10n_util::GetNSStringWithFixup(observer->GetPasteActionStringId());
    245       DCHECK([pasteAndGoLabel length]);
    246       [menu addItemWithTitle:pasteAndGoLabel
    247                       action:@selector(pasteAndGo:)
    248                keyEquivalent:@""];
    249     }
    250 
    251     [menu addItem:[NSMenuItem separatorItem]];
    252 
    253     // Display a "Show URL" option if search term replacement is active.
    254     if (observer->ShouldEnableShowURL()) {
    255       NSString* showURLLabel =
    256           l10n_util::GetNSStringWithFixup(IDS_SHOW_URL_MAC);
    257       DCHECK([showURLLabel length]);
    258       [menu addItemWithTitle:showURLLabel
    259                       action:@selector(showURL:)
    260                keyEquivalent:@""];
    261     }
    262 
    263     NSString* searchEngineLabel =
    264         l10n_util::GetNSStringWithFixup(IDS_EDIT_SEARCH_ENGINES);
    265     DCHECK([searchEngineLabel length]);
    266     NSMenuItem* item = [menu addItemWithTitle:searchEngineLabel
    267                                        action:@selector(commandDispatch:)
    268                                 keyEquivalent:@""];
    269     [item setTag:IDC_EDIT_SEARCH_ENGINES];
    270   }
    271 
    272   return menu;
    273 }
    274 
    275 // (Overridden from NSResponder)
    276 - (BOOL)becomeFirstResponder {
    277   BOOL doAccept = [super becomeFirstResponder];
    278   AutocompleteTextField* field = [self delegate];
    279   // Only lock visibility if we've been set up with a delegate (the text field).
    280   if (doAccept && field) {
    281     // Give the text field ownership of the visibility lock. (The first
    282     // responder dance between the field and the field editor is a little
    283     // weird.)
    284     [[BrowserWindowController browserWindowControllerForView:field]
    285         lockBarVisibilityForOwner:field withAnimation:YES delay:NO];
    286   }
    287   return doAccept;
    288 }
    289 
    290 // (Overridden from NSResponder)
    291 - (BOOL)resignFirstResponder {
    292   BOOL doResign = [super resignFirstResponder];
    293   AutocompleteTextField* field = [self delegate];
    294   // Only lock visibility if we've been set up with a delegate (the text field).
    295   if (doResign && field) {
    296     // Give the text field ownership of the visibility lock.
    297     [[BrowserWindowController browserWindowControllerForView:field]
    298         releaseBarVisibilityForOwner:field withAnimation:YES delay:YES];
    299 
    300     AutocompleteTextFieldObserver* observer = [self observer];
    301     if (observer)
    302       observer->OnKillFocus();
    303   }
    304   return doResign;
    305 }
    306 
    307 - (void)mouseDown:(NSEvent*)event {
    308   AutocompleteTextFieldObserver* observer = [self observer];
    309   if (observer)
    310     observer->OnMouseDown([event buttonNumber]);
    311   [super mouseDown:event];
    312 }
    313 
    314 - (void)rightMouseDown:(NSEvent *)event {
    315   AutocompleteTextFieldObserver* observer = [self observer];
    316   if (observer)
    317     observer->OnMouseDown([event buttonNumber]);
    318   [super rightMouseDown:event];
    319 }
    320 
    321 - (void)otherMouseDown:(NSEvent *)event {
    322   AutocompleteTextFieldObserver* observer = [self observer];
    323   if (observer)
    324     observer->OnMouseDown([event buttonNumber]);
    325   [super otherMouseDown:event];
    326 }
    327 
    328 // (URLDropTarget protocol)
    329 - (id<URLDropTargetController>)urlDropController {
    330   BrowserWindowController* windowController =
    331       [BrowserWindowController browserWindowControllerForView:self];
    332   return [windowController toolbarController];
    333 }
    334 
    335 // (URLDropTarget protocol)
    336 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
    337   // Make ourself the first responder (even though we're presumably already the
    338   // first responder), which will select the text to indicate that our contents
    339   // would be replaced by a drop.
    340   [[self window] makeFirstResponder:self];
    341   return [dropHandler_ draggingEntered:sender];
    342 }
    343 
    344 // (URLDropTarget protocol)
    345 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
    346   return [dropHandler_ draggingUpdated:sender];
    347 }
    348 
    349 // (URLDropTarget protocol)
    350 - (void)draggingExited:(id<NSDraggingInfo>)sender {
    351   return [dropHandler_ draggingExited:sender];
    352 }
    353 
    354 // (URLDropTarget protocol)
    355 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
    356   return [dropHandler_ performDragOperation:sender];
    357 }
    358 
    359 // Prevent control characters from being entered into the Omnibox.
    360 // This is invoked for keyboard entry, not for pasting.
    361 - (void)insertText:(id)aString {
    362   // Repeatedly remove control characters.  The loop will only ever
    363   // execute at all when the user enters control characters (using
    364   // Ctrl-Alt- or Ctrl-Q).  Making this generally efficient would
    365   // probably be a loss, since the input always seems to be a single
    366   // character.
    367   if ([aString isKindOfClass:[NSAttributedString class]]) {
    368     NSRange range =
    369         [[aString string] rangeOfCharacterFromSet:forbiddenCharacters_];
    370     while (range.location != NSNotFound) {
    371       aString = [[aString mutableCopy] autorelease];
    372       [aString deleteCharactersInRange:range];
    373       range = [[aString string] rangeOfCharacterFromSet:forbiddenCharacters_];
    374     }
    375     DCHECK_EQ(range.length, 0U);
    376   } else {
    377     NSRange range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
    378     while (range.location != NSNotFound) {
    379       aString =
    380           [aString stringByReplacingCharactersInRange:range withString:@""];
    381       range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
    382     }
    383     DCHECK_EQ(range.length, 0U);
    384   }
    385 
    386   // NOTE: If |aString| is empty, this intentionally replaces the
    387   // selection with empty.  This seems consistent with the case where
    388   // the input contained a mixture of characters and the string ended
    389   // up not empty.
    390   [super insertText:aString];
    391 }
    392 
    393 - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange {
    394   [super setMarkedText:aString selectedRange:selRange];
    395 
    396   // Because the OmniboxViewMac class treats marked text as content,
    397   // we need to treat the change to marked text as content change as well.
    398   [self didChangeText];
    399 }
    400 
    401 - (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange
    402                               granularity:(NSSelectionGranularity)granularity {
    403   AutocompleteTextFieldObserver* observer = [self observer];
    404   NSRange modifiedRange = [super selectionRangeForProposedRange:proposedSelRange
    405                                                     granularity:granularity];
    406   if (observer)
    407     return observer->SelectionRangeForProposedRange(modifiedRange);
    408   return modifiedRange;
    409 }
    410 
    411 
    412 
    413 
    414 - (void)setSelectedRange:(NSRange)charRange
    415                 affinity:(NSSelectionAffinity)affinity
    416           stillSelecting:(BOOL)flag {
    417   [super setSelectedRange:charRange affinity:affinity stillSelecting:flag];
    418 
    419   // We're only interested in selection changes directly caused by keyboard
    420   // input from the user.
    421   if (interpretingKeyEvents_)
    422     textChangedByKeyEvents_ = YES;
    423 }
    424 
    425 - (void)interpretKeyEvents:(NSArray *)eventArray {
    426   DCHECK(!interpretingKeyEvents_);
    427   interpretingKeyEvents_ = YES;
    428   textChangedByKeyEvents_ = NO;
    429   AutocompleteTextFieldObserver* observer = [self observer];
    430 
    431   if (observer)
    432     observer->OnBeforeChange();
    433 
    434   [super interpretKeyEvents:eventArray];
    435 
    436   if (textChangedByKeyEvents_ && observer)
    437     observer->OnDidChange();
    438 
    439   DCHECK(interpretingKeyEvents_);
    440   interpretingKeyEvents_ = NO;
    441 }
    442 
    443 - (BOOL)shouldChangeTextInRange:(NSRange)affectedCharRange
    444               replacementString:(NSString *)replacementString {
    445   BOOL ret = [super shouldChangeTextInRange:affectedCharRange
    446                           replacementString:replacementString];
    447 
    448   if (ret && !interpretingKeyEvents_) {
    449     AutocompleteTextFieldObserver* observer = [self observer];
    450     if (observer)
    451       observer->OnBeforeChange();
    452   }
    453   return ret;
    454 }
    455 
    456 - (void)didChangeText {
    457   [super didChangeText];
    458 
    459   AutocompleteTextFieldObserver* observer = [self observer];
    460   if (observer) {
    461     if (!interpretingKeyEvents_ &&
    462         ![[self undoManager] isUndoing] && ![[self undoManager] isRedoing]) {
    463       observer->OnDidChange();
    464     } else if (interpretingKeyEvents_) {
    465       textChangedByKeyEvents_ = YES;
    466     }
    467   }
    468 }
    469 
    470 - (void)doCommandBySelector:(SEL)cmd {
    471   // TODO(shess): Review code for cases where we're fruitlessly attempting to
    472   // work in spite of not having an observer.
    473   AutocompleteTextFieldObserver* observer = [self observer];
    474 
    475   if (observer && observer->OnDoCommandBySelector(cmd)) {
    476     // The observer should already be aware of any changes to the text, so
    477     // setting |textChangedByKeyEvents_| to NO to prevent its OnDidChange()
    478     // method from being called unnecessarily.
    479     textChangedByKeyEvents_ = NO;
    480     return;
    481   }
    482 
    483   // If the escape key was pressed and no revert happened and we're in
    484   // fullscreen mode, give focus to the web contents, which may dismiss the
    485   // overlay.
    486   if (cmd == @selector(cancelOperation:)) {
    487     BrowserWindowController* windowController =
    488         [BrowserWindowController browserWindowControllerForView:self];
    489     if ([windowController isInAnyFullscreenMode]) {
    490       [windowController focusTabContents];
    491       textChangedByKeyEvents_ = NO;
    492       return;
    493     }
    494   }
    495 
    496   [super doCommandBySelector:cmd];
    497 }
    498 
    499 - (void)setAttributedString:(NSAttributedString*)aString {
    500   NSTextStorage* textStorage = [self textStorage];
    501   DCHECK(textStorage);
    502   [textStorage setAttributedString:aString];
    503 
    504   // The text has been changed programmatically. The observer should know
    505   // this change, so setting |textChangedByKeyEvents_| to NO to
    506   // prevent its OnDidChange() method from being called unnecessarily.
    507   textChangedByKeyEvents_ = NO;
    508 }
    509 
    510 - (BOOL)validateMenuItem:(NSMenuItem*)item {
    511   if ([item action] == @selector(copyToFindPboard:))
    512     return [self selectedRange].length > 0;
    513   if ([item action] == @selector(pasteAndGo:)) {
    514     // TODO(rohitrao): If the clipboard is empty, should we show a
    515     // greyed-out "Paste and Go" or nothing at all?
    516     AutocompleteTextFieldObserver* observer = [self observer];
    517     DCHECK(observer);
    518     return observer->CanPasteAndGo();
    519   }
    520   if ([item action] == @selector(showURL:)) {
    521     AutocompleteTextFieldObserver* observer = [self observer];
    522     DCHECK(observer);
    523     return observer->ShouldEnableShowURL();
    524   }
    525   return [super validateMenuItem:item];
    526 }
    527 
    528 - (void)copyToFindPboard:(id)sender {
    529   NSRange selectedRange = [self selectedRange];
    530   if (selectedRange.length == 0)
    531     return;
    532   NSAttributedString* selection =
    533       [self attributedSubstringForProposedRange:selectedRange
    534                                     actualRange:NULL];
    535   if (!selection)
    536     return;
    537 
    538   [[FindPasteboard sharedInstance] setFindText:[selection string]];
    539 }
    540 
    541 - (void)drawRect:(NSRect)rect {
    542   [super drawRect:rect];
    543   autocomplete_text_field::DrawGrayTextAutocompletion(
    544       [self textStorage],
    545       [[self delegate] suggestText],
    546       [[self delegate] suggestColor],
    547       self,
    548       [self bounds]);
    549 }
    550 
    551 @end
    552