a09397f65c53518bd8f2f3d21ad88b728b3c07a2
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / location_bar / autocomplete_text_field_editor.mm
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
19 namespace {
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.
31 const NSUInteger kMaxPasteLength = 10000;
32
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;
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