Add ICU message format support
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / location_bar / autocomplete_text_field_editor.mm
blobf8228b3e7a4b809b77f2a0616300f0ca361b21e9
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     // 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;
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);
84     NSPoint p;
85     NSImage* image = [self dragImageForSelectionWithEvent:event origin:&p];
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];
101 - (void)copy:(id)sender {
102   AutocompleteTextFieldObserver* observer = [self observer];
103   DCHECK(observer);
104   if (observer && observer->CanCopy())
105     observer->CopyToPasteboard([NSPasteboard generalPasteboard]);
108 - (void)cut:(id)sender {
109   [self copy:sender];
110   [self delete:nil];
113 - (void)showURL:(id)sender {
114   AutocompleteTextFieldObserver* observer = [self observer];
115   DCHECK(observer);
116   observer->ShowURL();
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;
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
137               object:nil];
138   [nc removeObserver:self
139                 name:NSUndoManagerDidRedoChangeNotification
140               object:nil];
142   // Set the delegate.
143   [super setDelegate:delegate];
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   }
160 - (void)didUndoOrRedo:(NSNotification *)aNotification {
161   AutocompleteTextFieldObserver* observer = [self observer];
162   if (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()) {
173     NSBeep();
174     return;
175   }
177   AutocompleteTextFieldObserver* observer = [self observer];
178   DCHECK(observer);
179   if (observer) {
180     observer->OnPaste();
181   }
184 - (void)pasteAndMatchStyle:(id)sender {
185   [self paste:sender];
188 - (void)pasteAndGo:sender {
189   if (ThePasteboardIsTooDamnBig()) {
190     NSBeep();
191     return;
192   }
194   AutocompleteTextFieldObserver* observer = [self observer];
195   DCHECK(observer);
196   if (observer) {
197     observer->OnPasteAndGo();
198   }
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];
221   if (actionMenu)
222     return actionMenu;
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:@""];
232   [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_PASTE)
233                   action:@selector(paste:)
234            keyEquivalent:@""];
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     }
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:)
260                keyEquivalent:@""];
261     }
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   }
272   return menu;
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;
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];
301     if (observer)
302       observer->OnKillFocus();
303   }
304   return doResign;
307 - (void)mouseDown:(NSEvent*)event {
308   AutocompleteTextFieldObserver* observer = [self observer];
309   if (observer)
310     observer->OnMouseDown([event buttonNumber]);
311   [super mouseDown:event];
314 - (void)rightMouseDown:(NSEvent *)event {
315   AutocompleteTextFieldObserver* observer = [self observer];
316   if (observer)
317     observer->OnMouseDown([event buttonNumber]);
318   [super rightMouseDown:event];
321 - (void)otherMouseDown:(NSEvent *)event {
322   AutocompleteTextFieldObserver* observer = [self observer];
323   if (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   AutocompleteTextFieldObserver* observer = [self observer];
363   if (observer)
364     observer->OnInsertText();
366   // Repeatedly remove control characters.  The loop will only ever
367   // execute at all when the user enters control characters (using
368   // Ctrl-Alt- or Ctrl-Q).  Making this generally efficient would
369   // probably be a loss, since the input always seems to be a single
370   // character.
371   if ([aString isKindOfClass:[NSAttributedString class]]) {
372     NSRange range =
373         [[aString string] rangeOfCharacterFromSet:forbiddenCharacters_];
374     while (range.location != NSNotFound) {
375       aString = [[aString mutableCopy] autorelease];
376       [aString deleteCharactersInRange:range];
377       range = [[aString string] rangeOfCharacterFromSet:forbiddenCharacters_];
378     }
379     DCHECK_EQ(range.length, 0U);
380   } else {
381     NSRange range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
382     while (range.location != NSNotFound) {
383       aString =
384           [aString stringByReplacingCharactersInRange:range withString:@""];
385       range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
386     }
387     DCHECK_EQ(range.length, 0U);
388   }
390   // NOTE: If |aString| is empty, this intentionally replaces the
391   // selection with empty.  This seems consistent with the case where
392   // the input contained a mixture of characters and the string ended
393   // up not empty.
394   [super insertText:aString];
397 - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange {
398   [super setMarkedText:aString selectedRange:selRange];
400   // Because the OmniboxViewMac class treats marked text as content,
401   // we need to treat the change to marked text as content change as well.
402   [self didChangeText];
405 - (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange
406                               granularity:(NSSelectionGranularity)granularity {
407   AutocompleteTextFieldObserver* observer = [self observer];
408   NSRange modifiedRange = [super selectionRangeForProposedRange:proposedSelRange
409                                                     granularity:granularity];
410   if (observer)
411     return observer->SelectionRangeForProposedRange(modifiedRange);
412   return modifiedRange;
415 - (void)setSelectedRange:(NSRange)charRange
416                 affinity:(NSSelectionAffinity)affinity
417           stillSelecting:(BOOL)flag {
418   [super setSelectedRange:charRange affinity:affinity stillSelecting:flag];
420   // We're only interested in selection changes directly caused by keyboard
421   // input from the user.
422   if (interpretingKeyEvents_)
423     textChangedByKeyEvents_ = YES;
426 - (void)interpretKeyEvents:(NSArray *)eventArray {
427   DCHECK(!interpretingKeyEvents_);
428   interpretingKeyEvents_ = YES;
429   textChangedByKeyEvents_ = NO;
430   AutocompleteTextFieldObserver* observer = [self observer];
432   if (observer)
433     observer->OnBeforeChange();
435   [super interpretKeyEvents:eventArray];
437   if (textChangedByKeyEvents_ && observer)
438     observer->OnDidChange();
440   DCHECK(interpretingKeyEvents_);
441   interpretingKeyEvents_ = NO;
444 - (BOOL)shouldChangeTextInRange:(NSRange)affectedCharRange
445               replacementString:(NSString *)replacementString {
446   BOOL ret = [super shouldChangeTextInRange:affectedCharRange
447                           replacementString:replacementString];
449   if (ret && !interpretingKeyEvents_) {
450     AutocompleteTextFieldObserver* observer = [self observer];
451     if (observer)
452       observer->OnBeforeChange();
453   }
454   return ret;
457 - (void)didChangeText {
458   [super didChangeText];
460   AutocompleteTextFieldObserver* observer = [self observer];
461   if (observer) {
462     if (!interpretingKeyEvents_ &&
463         ![[self undoManager] isUndoing] && ![[self undoManager] isRedoing]) {
464       observer->OnDidChange();
465     } else if (interpretingKeyEvents_) {
466       textChangedByKeyEvents_ = YES;
467     }
468   }
471 - (void)doCommandBySelector:(SEL)cmd {
472   // TODO(shess): Review code for cases where we're fruitlessly attempting to
473   // work in spite of not having an observer.
474   AutocompleteTextFieldObserver* observer = [self observer];
476   if (observer && observer->OnDoCommandBySelector(cmd)) {
477     // The observer should already be aware of any changes to the text, so
478     // setting |textChangedByKeyEvents_| to NO to prevent its OnDidChange()
479     // method from being called unnecessarily.
480     textChangedByKeyEvents_ = NO;
481     return;
482   }
484   // If the escape key was pressed and no revert happened and we're in
485   // fullscreen mode, give focus to the web contents, which may dismiss the
486   // overlay.
487   if (cmd == @selector(cancelOperation:)) {
488     BrowserWindowController* windowController =
489         [BrowserWindowController browserWindowControllerForView:self];
490     if ([windowController isInAnyFullscreenMode]) {
491       [windowController focusTabContents];
492       textChangedByKeyEvents_ = NO;
493       return;
494     }
495   }
497   [super doCommandBySelector:cmd];
500 - (void)setAttributedString:(NSAttributedString*)aString {
501   NSTextStorage* textStorage = [self textStorage];
502   DCHECK(textStorage);
503   [textStorage setAttributedString:aString];
505   // The text has been changed programmatically. The observer should know
506   // this change, so setting |textChangedByKeyEvents_| to NO to
507   // prevent its OnDidChange() method from being called unnecessarily.
508   textChangedByKeyEvents_ = NO;
511 - (BOOL)validateMenuItem:(NSMenuItem*)item {
512   if ([item action] == @selector(copyToFindPboard:))
513     return [self selectedRange].length > 0;
514   if ([item action] == @selector(pasteAndGo:)) {
515     // TODO(rohitrao): If the clipboard is empty, should we show a
516     // greyed-out "Paste and Go" or nothing at all?
517     AutocompleteTextFieldObserver* observer = [self observer];
518     DCHECK(observer);
519     return observer->CanPasteAndGo();
520   }
521   if ([item action] == @selector(showURL:)) {
522     AutocompleteTextFieldObserver* observer = [self observer];
523     DCHECK(observer);
524     return observer->ShouldEnableShowURL();
525   }
526   return [super validateMenuItem:item];
529 - (void)copyToFindPboard:(id)sender {
530   NSRange selectedRange = [self selectedRange];
531   if (selectedRange.length == 0)
532     return;
533   NSAttributedString* selection =
534       [self attributedSubstringForProposedRange:selectedRange
535                                     actualRange:NULL];
536   if (!selection)
537     return;
539   [[FindPasteboard sharedInstance] setFindText:[selection string]];
542 - (void)drawRect:(NSRect)rect {
543   [super drawRect:rect];
544   autocomplete_text_field::DrawGrayTextAutocompletion(
545       [self textStorage],
546       [[self delegate] suggestText],
547       [[self delegate] suggestColor],
548       self,
549       [self bounds]);
550   AutocompleteTextFieldObserver* observer = [self observer];
551   if (observer)
552     observer->OnDidDrawRect();
555 @end