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/string_util.h"
      8 #include "base/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 "grit/generated_resources.h"
     16 #include "ui/base/l10n/l10n_util_mac.h"
     17 
     18 namespace {
     19 
     20 // When too much data is put into a single-line text field, things get
     21 // janky due to the cost of computing the blink rect.  Sometimes users
     22 // accidentally paste large amounts, so place a limit on what will be
     23 // accepted.
     24 //
     25 // 10k characters was arbitrarily chosen by seeing how much a text
     26 // field could handle in a single line before it started getting too
     27 // janky to recover from (jankiness was detectable around 5k).
     28 // www.google.com returns an error for searches around 2k characters,
     29 // so this is conservative.
     30 const NSUInteger kMaxPasteLength = 10000;
     31 
     32 // Returns |YES| if too much text would be pasted.
     33 BOOL ThePasteboardIsTooDamnBig() {
     34   NSPasteboard* pb = [NSPasteboard generalPasteboard];
     35   NSString* type =
     36       [pb availableTypeFromArray:[NSArray arrayWithObject:NSStringPboardType]];
     37   if (!type)
     38     return NO;
     39 
     40   return [[pb stringForType:type] length] > kMaxPasteLength;
     41 }
     42 
     43 }  // namespace
     44 
     45 @implementation AutocompleteTextFieldEditor
     46 
     47 - (id)initWithFrame:(NSRect)frameRect {
     48   if ((self = [super initWithFrame:frameRect])) {
     49     dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
     50 
     51     forbiddenCharacters_.reset([[NSCharacterSet controlCharacterSet] retain]);
     52   }
     53   return self;
     54 }
     55 
     56 // If the entire field is selected, drag the same data as would be
     57 // dragged from the field's location icon.  In some cases the textual
     58 // contents will not contain relevant data (for instance, "http://" is
     59 // stripped from URLs).
     60 - (BOOL)dragSelectionWithEvent:(NSEvent *)event
     61                         offset:(NSSize)mouseOffset
     62                      slideBack:(BOOL)slideBack {
     63   AutocompleteTextFieldObserver* observer = [self observer];
     64   DCHECK(observer);
     65   if (observer && observer->CanCopy()) {
     66     NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
     67     observer->CopyToPasteboard(pboard);
     68 
     69     NSPoint p;
     70     NSImage* image = [self dragImageForSelectionWithEvent:event origin:&p];
     71 
     72     [self dragImage:image
     73                  at:p
     74              offset:mouseOffset
     75               event:event
     76          pasteboard:pboard
     77              source:self
     78           slideBack:slideBack];
     79     return YES;
     80   }
     81   return [super dragSelectionWithEvent:event
     82                                 offset:mouseOffset
     83                              slideBack:slideBack];
     84 }
     85 
     86 - (void)copy:(id)sender {
     87   AutocompleteTextFieldObserver* observer = [self observer];
     88   DCHECK(observer);
     89   if (observer && observer->CanCopy())
     90     observer->CopyToPasteboard([NSPasteboard generalPasteboard]);
     91 }
     92 
     93 - (void)cut:(id)sender {
     94   [self copy:sender];
     95   [self delete:nil];
     96 }
     97 
     98 // This class assumes that the delegate is an AutocompleteTextField.
     99 // Enforce that assumption.
    100 - (AutocompleteTextField*)delegate {
    101   AutocompleteTextField* delegate =
    102       static_cast<AutocompleteTextField*>([super delegate]);
    103   DCHECK(delegate == nil ||
    104          [delegate isKindOfClass:[AutocompleteTextField class]]);
    105   return delegate;
    106 }
    107 
    108 - (void)setDelegate:(AutocompleteTextField*)delegate {
    109   DCHECK(delegate == nil ||
    110          [delegate isKindOfClass:[AutocompleteTextField class]]);
    111   [super setDelegate:delegate];
    112 }
    113 
    114 // Convenience method for retrieving the observer from the delegate.
    115 - (AutocompleteTextFieldObserver*)observer {
    116   return [[self delegate] observer];
    117 }
    118 
    119 - (void)paste:(id)sender {
    120   if (ThePasteboardIsTooDamnBig()) {
    121     NSBeep();
    122     return;
    123   }
    124 
    125   AutocompleteTextFieldObserver* observer = [self observer];
    126   DCHECK(observer);
    127   if (observer) {
    128     observer->OnPaste();
    129   }
    130 }
    131 
    132 - (void)pasteAndGo:sender {
    133   if (ThePasteboardIsTooDamnBig()) {
    134     NSBeep();
    135     return;
    136   }
    137 
    138   AutocompleteTextFieldObserver* observer = [self observer];
    139   DCHECK(observer);
    140   if (observer) {
    141     observer->OnPasteAndGo();
    142   }
    143 }
    144 
    145 // We have rich text, but it shouldn't be modified by the user, so
    146 // don't update the font panel.  In theory, -setUsesFontPanel: should
    147 // accomplish this, but that gets called frequently with YES when
    148 // NSTextField and NSTextView synchronize their contents.  That is
    149 // probably unavoidable because in most cases having rich text in the
    150 // field you probably would expect it to update the font panel.
    151 - (void)updateFontPanel {}
    152 
    153 // No ruler bar, so don't update any of that state, either.
    154 - (void)updateRuler {}
    155 
    156 - (NSMenu*)menuForEvent:(NSEvent*)event {
    157   // Give the control a chance to provide page-action menus.
    158   // NOTE: Note that page actions aren't even in the editor's
    159   // boundaries!  The Cocoa control implementation seems to do a
    160   // blanket forward to here if nothing more specific is returned from
    161   // the control and cell calls.
    162   // TODO(shess): Determine if the page-action part of this can be
    163   // moved to the cell.
    164   NSMenu* actionMenu = [[self delegate] decorationMenuForEvent:event];
    165   if (actionMenu)
    166     return actionMenu;
    167 
    168   NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"TITLE"] autorelease];
    169   [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CUT)
    170                   action:@selector(cut:)
    171            keyEquivalent:@""];
    172   [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_COPY)
    173                   action:@selector(copy:)
    174            keyEquivalent:@""];
    175   [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_PASTE)
    176                   action:@selector(paste:)
    177            keyEquivalent:@""];
    178 
    179   // TODO(shess): If the control is not editable, should we show a
    180   // greyed-out "Paste and Go"?
    181   if ([self isEditable]) {
    182     // Paste and go/search.
    183     AutocompleteTextFieldObserver* observer = [self observer];
    184     DCHECK(observer);
    185     if (observer && observer->CanPasteAndGo()) {
    186       const int string_id = observer->GetPasteActionStringId();
    187       NSString* label = l10n_util::GetNSStringWithFixup(string_id);
    188 
    189       // TODO(rohitrao): If the clipboard is empty, should we show a
    190       // greyed-out "Paste and Go" or nothing at all?
    191       if ([label length]) {
    192         [menu addItemWithTitle:label
    193                         action:@selector(pasteAndGo:)
    194                  keyEquivalent:@""];
    195       }
    196     }
    197 
    198     NSString* label = l10n_util::GetNSStringWithFixup(IDS_EDIT_SEARCH_ENGINES);
    199     DCHECK([label length]);
    200     if ([label length]) {
    201       [menu addItem:[NSMenuItem separatorItem]];
    202       NSMenuItem* item = [menu addItemWithTitle:label
    203                                          action:@selector(commandDispatch:)
    204                                   keyEquivalent:@""];
    205       [item setTag:IDC_EDIT_SEARCH_ENGINES];
    206     }
    207   }
    208 
    209   return menu;
    210 }
    211 
    212 // (Overridden from NSResponder)
    213 - (BOOL)becomeFirstResponder {
    214   BOOL doAccept = [super becomeFirstResponder];
    215   AutocompleteTextField* field = [self delegate];
    216   // Only lock visibility if we've been set up with a delegate (the text field).
    217   if (doAccept && field) {
    218     // Give the text field ownership of the visibility lock. (The first
    219     // responder dance between the field and the field editor is a little
    220     // weird.)
    221     [[BrowserWindowController browserWindowControllerForView:field]
    222         lockBarVisibilityForOwner:field withAnimation:YES delay:NO];
    223   }
    224   return doAccept;
    225 }
    226 
    227 // (Overridden from NSResponder)
    228 - (BOOL)resignFirstResponder {
    229   BOOL doResign = [super resignFirstResponder];
    230   AutocompleteTextField* field = [self delegate];
    231   // Only lock visibility if we've been set up with a delegate (the text field).
    232   if (doResign && field) {
    233     // Give the text field ownership of the visibility lock.
    234     [[BrowserWindowController browserWindowControllerForView:field]
    235         releaseBarVisibilityForOwner:field withAnimation:YES delay:YES];
    236 
    237     AutocompleteTextFieldObserver* observer = [self observer];
    238     if (observer)
    239       observer->OnKillFocus();
    240   }
    241   return doResign;
    242 }
    243 
    244 // (URLDropTarget protocol)
    245 - (id<URLDropTargetController>)urlDropController {
    246   BrowserWindowController* windowController =
    247       [BrowserWindowController browserWindowControllerForView:self];
    248   return [windowController toolbarController];
    249 }
    250 
    251 // (URLDropTarget protocol)
    252 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
    253   // Make ourself the first responder (even though we're presumably already the
    254   // first responder), which will select the text to indicate that our contents
    255   // would be replaced by a drop.
    256   [[self window] makeFirstResponder:self];
    257   return [dropHandler_ draggingEntered:sender];
    258 }
    259 
    260 // (URLDropTarget protocol)
    261 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
    262   return [dropHandler_ draggingUpdated:sender];
    263 }
    264 
    265 // (URLDropTarget protocol)
    266 - (void)draggingExited:(id<NSDraggingInfo>)sender {
    267   return [dropHandler_ draggingExited:sender];
    268 }
    269 
    270 // (URLDropTarget protocol)
    271 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
    272   return [dropHandler_ performDragOperation:sender];
    273 }
    274 
    275 // Prevent control characters from being entered into the Omnibox.
    276 // This is invoked for keyboard entry, not for pasting.
    277 - (void)insertText:(id)aString {
    278   // This method is documented as received either |NSString| or
    279   // |NSAttributedString|.  The autocomplete code will restyle the
    280   // results in any case, so simplify by always using |NSString|.
    281   if ([aString isKindOfClass:[NSAttributedString class]])
    282     aString = [aString string];
    283 
    284   // Repeatedly remove control characters.  The loop will only ever
    285   // execute at allwhen the user enters control characters (using
    286   // Ctrl-Alt- or Ctrl-Q).  Making this generally efficient would
    287   // probably be a loss, since the input always seems to be a single
    288   // character.
    289   NSRange range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
    290   while (range.location != NSNotFound) {
    291     aString = [aString stringByReplacingCharactersInRange:range withString:@""];
    292     range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
    293   }
    294   DCHECK_EQ(range.length, 0U);
    295 
    296   // NOTE: If |aString| is empty, this intentionally replaces the
    297   // selection with empty.  This seems consistent with the case where
    298   // the input contained a mixture of characters and the string ended
    299   // up not empty.
    300   [super insertText:aString];
    301 }
    302 
    303 - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange {
    304   if (![self hasMarkedText]) {
    305     // Before input methods set composition text in the omnibox, we need to
    306     // examine whether the autocompletion controller accepts the keyword to
    307     // avoid committing the current composition text wrongly.
    308     AutocompleteTextFieldObserver* observer = [self observer];
    309     if (observer)
    310       observer->OnStartingIME();
    311   }
    312 
    313   [super setMarkedText:aString selectedRange:selRange];
    314 
    315   // Because the AutocompleteEditViewMac class treats marked text as content,
    316   // we need to treat the change to marked text as content change as well.
    317   [self didChangeText];
    318 }
    319 
    320 - (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange
    321                               granularity:(NSSelectionGranularity)granularity {
    322   AutocompleteTextFieldObserver* observer = [self observer];
    323   NSRange modifiedRange = [super selectionRangeForProposedRange:proposedSelRange
    324                                                     granularity:granularity];
    325   if (observer)
    326     return observer->SelectionRangeForProposedRange(modifiedRange);
    327   return modifiedRange;
    328 }
    329 
    330 
    331 
    332 
    333 - (void)setSelectedRange:(NSRange)charRange
    334                 affinity:(NSSelectionAffinity)affinity
    335           stillSelecting:(BOOL)flag {
    336   [super setSelectedRange:charRange affinity:affinity stillSelecting:flag];
    337 
    338   // We're only interested in selection changes directly caused by keyboard
    339   // input from the user.
    340   if (interpretingKeyEvents_)
    341     textChangedByKeyEvents_ = YES;
    342 }
    343 
    344 - (void)interpretKeyEvents:(NSArray *)eventArray {
    345   DCHECK(!interpretingKeyEvents_);
    346   interpretingKeyEvents_ = YES;
    347   textChangedByKeyEvents_ = NO;
    348   AutocompleteTextFieldObserver* observer = [self observer];
    349 
    350   if (observer)
    351     observer->OnBeforeChange();
    352 
    353   [super interpretKeyEvents:eventArray];
    354 
    355   if (textChangedByKeyEvents_ && observer)
    356     observer->OnDidChange();
    357 
    358   DCHECK(interpretingKeyEvents_);
    359   interpretingKeyEvents_ = NO;
    360 }
    361 
    362 - (BOOL)shouldChangeTextInRange:(NSRange)affectedCharRange
    363               replacementString:(NSString *)replacementString {
    364   BOOL ret = [super shouldChangeTextInRange:affectedCharRange
    365                           replacementString:replacementString];
    366 
    367   if (ret && !interpretingKeyEvents_) {
    368     AutocompleteTextFieldObserver* observer = [self observer];
    369     if (observer)
    370       observer->OnBeforeChange();
    371   }
    372   return ret;
    373 }
    374 
    375 - (void)didChangeText {
    376   [super didChangeText];
    377 
    378   AutocompleteTextFieldObserver* observer = [self observer];
    379   if (observer) {
    380     if (!interpretingKeyEvents_)
    381       observer->OnDidChange();
    382     else
    383       textChangedByKeyEvents_ = YES;
    384   }
    385 }
    386 
    387 - (void)doCommandBySelector:(SEL)cmd {
    388   // TODO(shess): Review code for cases where we're fruitlessly attempting to
    389   // work in spite of not having an observer.
    390   AutocompleteTextFieldObserver* observer = [self observer];
    391 
    392   if (observer && observer->OnDoCommandBySelector(cmd)) {
    393     // The observer should already be aware of any changes to the text, so
    394     // setting |textChangedByKeyEvents_| to NO to prevent its OnDidChange()
    395     // method from being called unnecessarily.
    396     textChangedByKeyEvents_ = NO;
    397     return;
    398   }
    399 
    400   // If the escape key was pressed and no revert happened and we're in
    401   // fullscreen mode, make it resign key.
    402   if (cmd == @selector(cancelOperation:)) {
    403     BrowserWindowController* windowController =
    404         [BrowserWindowController browserWindowControllerForView:self];
    405     if ([windowController isFullscreen]) {
    406       [windowController focusTabContents];
    407       return;
    408     }
    409   }
    410 
    411   [super doCommandBySelector:cmd];
    412 }
    413 
    414 - (void)setAttributedString:(NSAttributedString*)aString {
    415   NSTextStorage* textStorage = [self textStorage];
    416   DCHECK(textStorage);
    417   [textStorage setAttributedString:aString];
    418 
    419   // The text has been changed programmatically. The observer should know
    420   // this change, so setting |textChangedByKeyEvents_| to NO to
    421   // prevent its OnDidChange() method from being called unnecessarily.
    422   textChangedByKeyEvents_ = NO;
    423 }
    424 
    425 - (void)mouseDown:(NSEvent*)theEvent {
    426   // Close the popup before processing the event.
    427   AutocompleteTextFieldObserver* observer = [self observer];
    428   if (observer)
    429     observer->ClosePopup();
    430 
    431   [super mouseDown:theEvent];
    432 }
    433 
    434 @end
    435