Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / location_bar / autocomplete_text_field_editor.mm
blob1d8b1da169c10bd15cf2d4ff8bd393a0c521ee26
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.
5 #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
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"
19 namespace {
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.
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;
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;
41   return [[pb stringForType:type] length] > kMaxPasteLength;
44 }  // namespace
46 @implementation AutocompleteTextFieldEditor
48 - (BOOL)shouldDrawInsertionPoint {
49   return [super shouldDrawInsertionPoint] &&
50          ![[[self delegate] cell] hideFocusState];
53 - (id)initWithFrame:(NSRect)frameRect {
54   if ((self = [super initWithFrame:frameRect])) {
55     dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
57     forbiddenCharacters_.reset([[NSCharacterSet controlCharacterSet] retain]);
59     // Disable all substitutions by default. In regular NSTextFields a user may
60     // selectively enable them via context menu, but that submenu is not enabled
61     // for the omnibox. The substitutions are unlikely to be useful in any case.
62     //
63     // Also see http://crbug.com/173405 and http://crbug.com/528014.
64     NSTextCheckingTypes checkingTypes = 0;
65     [self setEnabledTextCheckingTypes:checkingTypes];
66   }
67   return self;
70 // If the entire field is selected, drag the same data as would be
71 // dragged from the field's location icon.  In some cases the textual
72 // contents will not contain relevant data (for instance, "http://" is
73 // stripped from URLs).
74 - (BOOL)dragSelectionWithEvent:(NSEvent *)event
75                         offset:(NSSize)mouseOffset
76                      slideBack:(BOOL)slideBack {
77   AutocompleteTextFieldObserver* observer = [self observer];
78   DCHECK(observer);
79   if (observer && observer->CanCopy()) {
80     NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
81     observer->CopyToPasteboard(pboard);
83     NSPoint p;
84     NSImage* image = [self dragImageForSelectionWithEvent:event origin:&p];
86     [self dragImage:image
87                  at:p
88              offset:mouseOffset
89               event:event
90          pasteboard:pboard
91              source:self
92           slideBack:slideBack];
93     return YES;
94   }
95   return [super dragSelectionWithEvent:event
96                                 offset:mouseOffset
97                              slideBack:slideBack];
100 - (void)copy:(id)sender {
101   AutocompleteTextFieldObserver* observer = [self observer];
102   DCHECK(observer);
103   if (observer && observer->CanCopy())
104     observer->CopyToPasteboard([NSPasteboard generalPasteboard]);
107 - (void)cut:(id)sender {
108   [self copy:sender];
109   [self delete:nil];
112 - (void)showURL:(id)sender {
113   AutocompleteTextFieldObserver* observer = [self observer];
114   DCHECK(observer);
115   observer->ShowURL();
118 // This class assumes that the delegate is an AutocompleteTextField.
119 // Enforce that assumption.
120 - (AutocompleteTextField*)delegate {
121   AutocompleteTextField* delegate =
122       static_cast<AutocompleteTextField*>([super delegate]);
123   DCHECK(delegate == nil ||
124          [delegate isKindOfClass:[AutocompleteTextField class]]);
125   return delegate;
128 - (void)setDelegate:(AutocompleteTextField*)delegate {
129   DCHECK(delegate == nil ||
130          [delegate isKindOfClass:[AutocompleteTextField class]]);
132   // Unregister from any previously registered undo and redo notifications.
133   NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
134   [nc removeObserver:self
135                 name:NSUndoManagerDidUndoChangeNotification
136               object:nil];
137   [nc removeObserver:self
138                 name:NSUndoManagerDidRedoChangeNotification
139               object:nil];
141   // Set the delegate.
142   [super setDelegate:delegate];
144   // Register for undo and redo notifications from the new |delegate|, if it is
145   // non-nil.
146   if ([self delegate]) {
147     NSUndoManager* undo_manager = [self undoManager];
148     [nc addObserver:self
149            selector:@selector(didUndoOrRedo:)
150                name:NSUndoManagerDidUndoChangeNotification
151              object:undo_manager];
152     [nc addObserver:self
153            selector:@selector(didUndoOrRedo:)
154                name:NSUndoManagerDidRedoChangeNotification
155              object:undo_manager];
156   }
159 - (void)didUndoOrRedo:(NSNotification *)aNotification {
160   AutocompleteTextFieldObserver* observer = [self observer];
161   if (observer)
162     observer->OnDidChange();
165 // Convenience method for retrieving the observer from the delegate.
166 - (AutocompleteTextFieldObserver*)observer {
167   return [[self delegate] observer];
170 - (void)paste:(id)sender {
171   if (ThePasteboardIsTooDamnBig()) {
172     NSBeep();
173     return;
174   }
176   AutocompleteTextFieldObserver* observer = [self observer];
177   DCHECK(observer);
178   if (observer) {
179     observer->OnPaste();
180   }
183 - (void)pasteAndMatchStyle:(id)sender {
184   [self paste:sender];
187 - (void)pasteAndGo:sender {
188   if (ThePasteboardIsTooDamnBig()) {
189     NSBeep();
190     return;
191   }
193   AutocompleteTextFieldObserver* observer = [self observer];
194   DCHECK(observer);
195   if (observer) {
196     observer->OnPasteAndGo();
197   }
200 // We have rich text, but it shouldn't be modified by the user, so
201 // don't update the font panel.  In theory, -setUsesFontPanel: should
202 // accomplish this, but that gets called frequently with YES when
203 // NSTextField and NSTextView synchronize their contents.  That is
204 // probably unavoidable because in most cases having rich text in the
205 // field you probably would expect it to update the font panel.
206 - (void)updateFontPanel {}
208 // No ruler bar, so don't update any of that state, either.
209 - (void)updateRuler {}
211 - (NSMenu*)menuForEvent:(NSEvent*)event {
212   // Give the control a chance to provide page-action menus.
213   // NOTE: Note that page actions aren't even in the editor's
214   // boundaries!  The Cocoa control implementation seems to do a
215   // blanket forward to here if nothing more specific is returned from
216   // the control and cell calls.
217   // TODO(shess): Determine if the page-action part of this can be
218   // moved to the cell.
219   NSMenu* actionMenu = [[self delegate] decorationMenuForEvent:event];
220   if (actionMenu)
221     return actionMenu;
223   NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"TITLE"] autorelease];
224   [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CUT)
225                   action:@selector(cut:)
226            keyEquivalent:@""];
227   [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_COPY)
228                   action:@selector(copy:)
229            keyEquivalent:@""];
231   [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_PASTE)
232                   action:@selector(paste:)
233            keyEquivalent:@""];
235   // TODO(shess): If the control is not editable, should we show a
236   // greyed-out "Paste and Go"?
237   if ([self isEditable]) {
238     // Paste and go/search.
239     AutocompleteTextFieldObserver* observer = [self observer];
240     DCHECK(observer);
241     if (!ThePasteboardIsTooDamnBig()) {
242       NSString* pasteAndGoLabel =
243           l10n_util::GetNSStringWithFixup(observer->GetPasteActionStringId());
244       DCHECK([pasteAndGoLabel length]);
245       [menu addItemWithTitle:pasteAndGoLabel
246                       action:@selector(pasteAndGo:)
247                keyEquivalent:@""];
248     }
250     [menu addItem:[NSMenuItem separatorItem]];
252     // Display a "Show URL" option if search term replacement is active.
253     if (observer->ShouldEnableShowURL()) {
254       NSString* showURLLabel =
255           l10n_util::GetNSStringWithFixup(IDS_SHOW_URL_MAC);
256       DCHECK([showURLLabel length]);
257       [menu addItemWithTitle:showURLLabel
258                       action:@selector(showURL:)
259                keyEquivalent:@""];
260     }
262     NSString* searchEngineLabel =
263         l10n_util::GetNSStringWithFixup(IDS_EDIT_SEARCH_ENGINES);
264     DCHECK([searchEngineLabel length]);
265     NSMenuItem* item = [menu addItemWithTitle:searchEngineLabel
266                                        action:@selector(commandDispatch:)
267                                 keyEquivalent:@""];
268     [item setTag:IDC_EDIT_SEARCH_ENGINES];
269   }
271   return menu;
274 // (Overridden from NSResponder)
275 - (BOOL)becomeFirstResponder {
276   BOOL doAccept = [super becomeFirstResponder];
277   AutocompleteTextField* field = [self delegate];
278   // Only lock visibility if we've been set up with a delegate (the text field).
279   if (doAccept && field) {
280     // Give the text field ownership of the visibility lock. (The first
281     // responder dance between the field and the field editor is a little
282     // weird.)
283     [[BrowserWindowController browserWindowControllerForView:field]
284         lockBarVisibilityForOwner:field withAnimation:YES delay:NO];
285   }
286   return doAccept;
289 // (Overridden from NSResponder)
290 - (BOOL)resignFirstResponder {
291   BOOL doResign = [super resignFirstResponder];
292   AutocompleteTextField* field = [self delegate];
293   // Only lock visibility if we've been set up with a delegate (the text field).
294   if (doResign && field) {
295     // Give the text field ownership of the visibility lock.
296     [[BrowserWindowController browserWindowControllerForView:field]
297         releaseBarVisibilityForOwner:field withAnimation:YES delay:YES];
299     AutocompleteTextFieldObserver* observer = [self observer];
300     if (observer)
301       observer->OnKillFocus();
302   }
303   return doResign;
306 - (void)mouseDown:(NSEvent*)event {
307   AutocompleteTextFieldObserver* observer = [self observer];
308   if (observer)
309     observer->OnMouseDown([event buttonNumber]);
310   [super mouseDown:event];
313 - (void)rightMouseDown:(NSEvent *)event {
314   AutocompleteTextFieldObserver* observer = [self observer];
315   if (observer)
316     observer->OnMouseDown([event buttonNumber]);
317   [super rightMouseDown:event];
320 - (void)otherMouseDown:(NSEvent *)event {
321   AutocompleteTextFieldObserver* observer = [self observer];
322   if (observer)
323     observer->OnMouseDown([event buttonNumber]);
324   [super otherMouseDown:event];
327 // (URLDropTarget protocol)
328 - (id<URLDropTargetController>)urlDropController {
329   BrowserWindowController* windowController =
330       [BrowserWindowController browserWindowControllerForView:self];
331   return [windowController toolbarController];
334 // (URLDropTarget protocol)
335 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
336   // Make ourself the first responder (even though we're presumably already the
337   // first responder), which will select the text to indicate that our contents
338   // would be replaced by a drop.
339   [[self window] makeFirstResponder:self];
340   return [dropHandler_ draggingEntered:sender];
343 // (URLDropTarget protocol)
344 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
345   return [dropHandler_ draggingUpdated:sender];
348 // (URLDropTarget protocol)
349 - (void)draggingExited:(id<NSDraggingInfo>)sender {
350   return [dropHandler_ draggingExited:sender];
353 // (URLDropTarget protocol)
354 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
355   return [dropHandler_ performDragOperation:sender];
358 // Prevent control characters from being entered into the Omnibox.
359 // This is invoked for keyboard entry, not for pasting.
360 - (void)insertText:(id)aString {
361   AutocompleteTextFieldObserver* observer = [self observer];
362   if (observer)
363     observer->OnInsertText();
365   // Repeatedly remove control characters.  The loop will only ever
366   // execute at all when the user enters control characters (using
367   // Ctrl-Alt- or Ctrl-Q).  Making this generally efficient would
368   // probably be a loss, since the input always seems to be a single
369   // character.
370   if ([aString isKindOfClass:[NSAttributedString class]]) {
371     NSRange range =
372         [[aString string] rangeOfCharacterFromSet:forbiddenCharacters_];
373     while (range.location != NSNotFound) {
374       aString = [[aString mutableCopy] autorelease];
375       [aString deleteCharactersInRange:range];
376       range = [[aString string] rangeOfCharacterFromSet:forbiddenCharacters_];
377     }
378     DCHECK_EQ(range.length, 0U);
379   } else {
380     NSRange range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
381     while (range.location != NSNotFound) {
382       aString =
383           [aString stringByReplacingCharactersInRange:range withString:@""];
384       range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
385     }
386     DCHECK_EQ(range.length, 0U);
387   }
389   // NOTE: If |aString| is empty, this intentionally replaces the
390   // selection with empty.  This seems consistent with the case where
391   // the input contained a mixture of characters and the string ended
392   // up not empty.
393   [super insertText:aString];
396 - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange {
397   [super setMarkedText:aString selectedRange:selRange];
399   // Because the OmniboxViewMac class treats marked text as content,
400   // we need to treat the change to marked text as content change as well.
401   [self didChangeText];
404 - (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange
405                               granularity:(NSSelectionGranularity)granularity {
406   AutocompleteTextFieldObserver* observer = [self observer];
407   NSRange modifiedRange = [super selectionRangeForProposedRange:proposedSelRange
408                                                     granularity:granularity];
409   if (observer)
410     return observer->SelectionRangeForProposedRange(modifiedRange);
411   return modifiedRange;
414 - (void)setSelectedRange:(NSRange)charRange
415                 affinity:(NSSelectionAffinity)affinity
416           stillSelecting:(BOOL)flag {
417   [super setSelectedRange:charRange affinity:affinity stillSelecting:flag];
419   // We're only interested in selection changes directly caused by keyboard
420   // input from the user.
421   if (interpretingKeyEvents_)
422     textChangedByKeyEvents_ = YES;
425 - (void)interpretKeyEvents:(NSArray *)eventArray {
426   DCHECK(!interpretingKeyEvents_);
427   interpretingKeyEvents_ = YES;
428   textChangedByKeyEvents_ = NO;
429   AutocompleteTextFieldObserver* observer = [self observer];
431   if (observer)
432     observer->OnBeforeChange();
434   [super interpretKeyEvents:eventArray];
436   if (textChangedByKeyEvents_ && observer)
437     observer->OnDidChange();
439   DCHECK(interpretingKeyEvents_);
440   interpretingKeyEvents_ = NO;
443 - (BOOL)shouldChangeTextInRange:(NSRange)affectedCharRange
444               replacementString:(NSString *)replacementString {
445   BOOL ret = [super shouldChangeTextInRange:affectedCharRange
446                           replacementString:replacementString];
448   if (ret && !interpretingKeyEvents_) {
449     AutocompleteTextFieldObserver* observer = [self observer];
450     if (observer)
451       observer->OnBeforeChange();
452   }
453   return ret;
456 - (void)didChangeText {
457   [super didChangeText];
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   }
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];
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   }
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   }
496   [super doCommandBySelector:cmd];
499 - (void)setAttributedString:(NSAttributedString*)aString {
500   NSTextStorage* textStorage = [self textStorage];
501   DCHECK(textStorage);
502   [textStorage setAttributedString:aString];
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;
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];
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;
538   [[FindPasteboard sharedInstance] setFindText:[selection string]];
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   AutocompleteTextFieldObserver* observer = [self observer];
550   if (observer)
551     observer->OnDidDrawRect();
554 @end