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"
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 // 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.
63 // Also see http://crbug.com/173405 and http://crbug.com/528014.
64 NSTextCheckingTypes checkingTypes = 0;
65 [self setEnabledTextCheckingTypes:checkingTypes];
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];
79 if (observer && observer->CanCopy()) {
80 NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
81 observer->CopyToPasteboard(pboard);
84 NSImage* image = [self dragImageForSelectionWithEvent:event origin:&p];
95 return [super dragSelectionWithEvent:event
100 - (void)copy:(id)sender {
101 AutocompleteTextFieldObserver* observer = [self observer];
103 if (observer && observer->CanCopy())
104 observer->CopyToPasteboard([NSPasteboard generalPasteboard]);
107 - (void)cut:(id)sender {
112 - (void)showURL:(id)sender {
113 AutocompleteTextFieldObserver* observer = [self observer];
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]]);
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
137 [nc removeObserver:self
138 name:NSUndoManagerDidRedoChangeNotification
142 [super setDelegate:delegate];
144 // Register for undo and redo notifications from the new |delegate|, if it is
146 if ([self delegate]) {
147 NSUndoManager* undo_manager = [self undoManager];
149 selector:@selector(didUndoOrRedo:)
150 name:NSUndoManagerDidUndoChangeNotification
151 object:undo_manager];
153 selector:@selector(didUndoOrRedo:)
154 name:NSUndoManagerDidRedoChangeNotification
155 object:undo_manager];
159 - (void)didUndoOrRedo:(NSNotification *)aNotification {
160 AutocompleteTextFieldObserver* observer = [self 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()) {
176 AutocompleteTextFieldObserver* observer = [self observer];
183 - (void)pasteAndMatchStyle:(id)sender {
187 - (void)pasteAndGo:sender {
188 if (ThePasteboardIsTooDamnBig()) {
193 AutocompleteTextFieldObserver* observer = [self observer];
196 observer->OnPasteAndGo();
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];
223 NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"TITLE"] autorelease];
224 [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CUT)
225 action:@selector(cut:)
227 [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_COPY)
228 action:@selector(copy:)
231 [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_PASTE)
232 action:@selector(paste:)
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];
241 if (!ThePasteboardIsTooDamnBig()) {
242 NSString* pasteAndGoLabel =
243 l10n_util::GetNSStringWithFixup(observer->GetPasteActionStringId());
244 DCHECK([pasteAndGoLabel length]);
245 [menu addItemWithTitle:pasteAndGoLabel
246 action:@selector(pasteAndGo:)
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:)
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:)
268 [item setTag:IDC_EDIT_SEARCH_ENGINES];
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
283 [[BrowserWindowController browserWindowControllerForView:field]
284 lockBarVisibilityForOwner:field withAnimation:YES delay:NO];
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];
301 observer->OnKillFocus();
306 - (void)mouseDown:(NSEvent*)event {
307 AutocompleteTextFieldObserver* observer = [self observer];
309 observer->OnMouseDown([event buttonNumber]);
310 [super mouseDown:event];
313 - (void)rightMouseDown:(NSEvent *)event {
314 AutocompleteTextFieldObserver* observer = [self observer];
316 observer->OnMouseDown([event buttonNumber]);
317 [super rightMouseDown:event];
320 - (void)otherMouseDown:(NSEvent *)event {
321 AutocompleteTextFieldObserver* observer = [self 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];
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
370 if ([aString isKindOfClass:[NSAttributedString class]]) {
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_];
378 DCHECK_EQ(range.length, 0U);
380 NSRange range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
381 while (range.location != NSNotFound) {
383 [aString stringByReplacingCharactersInRange:range withString:@""];
384 range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
386 DCHECK_EQ(range.length, 0U);
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
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];
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];
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 isInAnyFullscreenMode]) {
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],
549 AutocompleteTextFieldObserver* observer = [self observer];
551 observer->OnDidDrawRect();