• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 "grit/generated_resources.h"
16#import "ui/base/cocoa/find_pasteboard.h"
17#include "ui/base/l10n/l10n_util_mac.h"
18
19namespace {
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.
31const NSUInteger kMaxPasteLength = 10000;
32
33// Returns |YES| if too much text would be pasted.
34BOOL 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 isFullscreen]) {
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