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 "grit/generated_resources.h"
16 #import "ui/base/cocoa/find_pasteboard.h"
17 #include "ui/base/l10n/l10n_util_mac.h"
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
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];
37 [pb availableTypeFromArray:[NSArray arrayWithObject:NSStringPboardType]];
41 return [[pb stringForType:type] length] > kMaxPasteLength;
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 // These checks seem inappropriate to the omnibox, and also
60 // unlikely to work reliably due to our autocomplete interfering.
62 // Also see <http://crbug.com/173405>.
63 NSTextCheckingTypes checkingTypes = [self enabledTextCheckingTypes];
64 checkingTypes &= ~NSTextCheckingTypeReplacement;
65 checkingTypes &= ~NSTextCheckingTypeCorrection;
66 [self setEnabledTextCheckingTypes:checkingTypes];
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];
80 if (observer && observer->CanCopy()) {
81 NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
82 observer->CopyToPasteboard(pboard);
85 NSImage* image = [self dragImageForSelectionWithEvent:event origin:&p];
96 return [super dragSelectionWithEvent:event
101 - (void)copy:(id)sender {
102 AutocompleteTextFieldObserver* observer = [self observer];
104 if (observer && observer->CanCopy())
105 observer->CopyToPasteboard([NSPasteboard generalPasteboard]);
108 - (void)cut:(id)sender {
113 - (void)showURL:(id)sender {
114 AutocompleteTextFieldObserver* observer = [self observer];
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]]);
129 - (void)setDelegate:(AutocompleteTextField*)delegate {
130 DCHECK(delegate == nil ||
131 [delegate isKindOfClass:[AutocompleteTextField class]]);
133 // Unregister from any previously registered undo and redo notifications.
134 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
135 [nc removeObserver:self
136 name:NSUndoManagerDidUndoChangeNotification
138 [nc removeObserver:self
139 name:NSUndoManagerDidRedoChangeNotification
143 [super setDelegate:delegate];
145 // Register for undo and redo notifications from the new |delegate|, if it is
147 if ([self delegate]) {
148 NSUndoManager* undo_manager = [self undoManager];
150 selector:@selector(didUndoOrRedo:)
151 name:NSUndoManagerDidUndoChangeNotification
152 object:undo_manager];
154 selector:@selector(didUndoOrRedo:)
155 name:NSUndoManagerDidRedoChangeNotification
156 object:undo_manager];
160 - (void)didUndoOrRedo:(NSNotification *)aNotification {
161 AutocompleteTextFieldObserver* observer = [self observer];
163 observer->OnDidChange();
166 // Convenience method for retrieving the observer from the delegate.
167 - (AutocompleteTextFieldObserver*)observer {
168 return [[self delegate] observer];
171 - (void)paste:(id)sender {
172 if (ThePasteboardIsTooDamnBig()) {
177 AutocompleteTextFieldObserver* observer = [self observer];
184 - (void)pasteAndMatchStyle:(id)sender {
188 - (void)pasteAndGo:sender {
189 if (ThePasteboardIsTooDamnBig()) {
194 AutocompleteTextFieldObserver* observer = [self observer];
197 observer->OnPasteAndGo();
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 {}
209 // No ruler bar, so don't update any of that state, either.
210 - (void)updateRuler {}
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];
224 NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"TITLE"] autorelease];
225 [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CUT)
226 action:@selector(cut:)
228 [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_COPY)
229 action:@selector(copy:)
232 [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_PASTE)
233 action:@selector(paste:)
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];
242 if (!ThePasteboardIsTooDamnBig()) {
243 NSString* pasteAndGoLabel =
244 l10n_util::GetNSStringWithFixup(observer->GetPasteActionStringId());
245 DCHECK([pasteAndGoLabel length]);
246 [menu addItemWithTitle:pasteAndGoLabel
247 action:@selector(pasteAndGo:)
251 [menu addItem:[NSMenuItem separatorItem]];
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:)
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:)
269 [item setTag:IDC_EDIT_SEARCH_ENGINES];
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
284 [[BrowserWindowController browserWindowControllerForView:field]
285 lockBarVisibilityForOwner:field withAnimation:YES delay:NO];
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];
300 AutocompleteTextFieldObserver* observer = [self observer];
302 observer->OnKillFocus();
307 - (void)mouseDown:(NSEvent*)event {
308 AutocompleteTextFieldObserver* observer = [self observer];
310 observer->OnMouseDown([event buttonNumber]);
311 [super mouseDown:event];
314 - (void)rightMouseDown:(NSEvent *)event {
315 AutocompleteTextFieldObserver* observer = [self observer];
317 observer->OnMouseDown([event buttonNumber]);
318 [super rightMouseDown:event];
321 - (void)otherMouseDown:(NSEvent *)event {
322 AutocompleteTextFieldObserver* observer = [self observer];
324 observer->OnMouseDown([event buttonNumber]);
325 [super otherMouseDown:event];
328 // (URLDropTarget protocol)
329 - (id<URLDropTargetController>)urlDropController {
330 BrowserWindowController* windowController =
331 [BrowserWindowController browserWindowControllerForView:self];
332 return [windowController toolbarController];
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];
344 // (URLDropTarget protocol)
345 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
346 return [dropHandler_ draggingUpdated:sender];
349 // (URLDropTarget protocol)
350 - (void)draggingExited:(id<NSDraggingInfo>)sender {
351 return [dropHandler_ draggingExited:sender];
354 // (URLDropTarget protocol)
355 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
356 return [dropHandler_ performDragOperation:sender];
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
367 if ([aString isKindOfClass:[NSAttributedString class]]) {
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_];
375 DCHECK_EQ(range.length, 0U);
377 NSRange range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
378 while (range.location != NSNotFound) {
380 [aString stringByReplacingCharactersInRange:range withString:@""];
381 range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
383 DCHECK_EQ(range.length, 0U);
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
390 [super insertText:aString];
393 - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange {
394 [super setMarkedText:aString selectedRange:selRange];
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];
401 - (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange
402 granularity:(NSSelectionGranularity)granularity {
403 AutocompleteTextFieldObserver* observer = [self observer];
404 NSRange modifiedRange = [super selectionRangeForProposedRange:proposedSelRange
405 granularity:granularity];
407 return observer->SelectionRangeForProposedRange(modifiedRange);
408 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];
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];
451 observer->OnBeforeChange();
456 - (void)didChangeText {
457 [super didChangeText];
459 AutocompleteTextFieldObserver* observer = [self observer];
461 if (!interpretingKeyEvents_ &&
462 ![[self undoManager] isUndoing] && ![[self undoManager] isRedoing]) {
463 observer->OnDidChange();
464 } else if (interpretingKeyEvents_) {
465 textChangedByKeyEvents_ = YES;
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;
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
486 if (cmd == @selector(cancelOperation:)) {
487 BrowserWindowController* windowController =
488 [BrowserWindowController browserWindowControllerForView:self];
489 if ([windowController isFullscreen]) {
490 [windowController focusTabContents];
491 textChangedByKeyEvents_ = NO;
496 [super doCommandBySelector:cmd];
499 - (void)setAttributedString:(NSAttributedString*)aString {
500 NSTextStorage* textStorage = [self 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];
518 return observer->CanPasteAndGo();
520 if ([item action] == @selector(showURL:)) {
521 AutocompleteTextFieldObserver* observer = [self observer];
523 return observer->ShouldEnableShowURL();
525 return [super validateMenuItem:item];
528 - (void)copyToFindPboard:(id)sender {
529 NSRange selectedRange = [self selectedRange];
530 if (selectedRange.length == 0)
532 NSAttributedString* selection =
533 [self attributedSubstringForProposedRange:selectedRange
538 [[FindPasteboard sharedInstance] setFindText:[selection string]];
541 - (void)drawRect:(NSRect)rect {
542 [super drawRect:rect];
543 autocomplete_text_field::DrawGrayTextAutocompletion(
545 [[self delegate] suggestText],
546 [[self delegate] suggestColor],