Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / location_bar / autocomplete_text_field.mm
blob56532c076eb1cde7fb1360987e2e1292447ff3d9
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.h"
7 #include "base/logging.h"
8 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
9 #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
10 #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
11 #import "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
12 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
13 #import "chrome/browser/ui/cocoa/url_drop_target.h"
14 #import "chrome/browser/ui/cocoa/view_id_util.h"
15 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
17 namespace {
18 const CGFloat kAnimationDuration = 0.2;
21 @implementation AutocompleteTextField
23 @synthesize observer = observer_;
25 + (Class)cellClass {
26   return [AutocompleteTextFieldCell class];
29 - (void)dealloc {
30   [[NSNotificationCenter defaultCenter] removeObserver:self];
31   [super dealloc];
34 - (void)awakeFromNib {
35   DCHECK([[self cell] isKindOfClass:[AutocompleteTextFieldCell class]]);
36   [[self cell] setTruncatesLastVisibleLine:YES];
37   [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
38   currentToolTips_.reset([[NSMutableArray alloc] init]);
39   resizeAnimation_.reset([[NSViewAnimation alloc] init]);
40   [resizeAnimation_ setDuration:kAnimationDuration];
41   [resizeAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
44 - (void)flagsChanged:(NSEvent*)theEvent {
45   if (observer_) {
46     const bool controlFlag = ([theEvent modifierFlags]&NSControlKeyMask) != 0;
47     observer_->OnControlKeyChanged(controlFlag);
48   }
51 - (AutocompleteTextFieldCell*)cell {
52   NSCell* cell = [super cell];
53   if (!cell)
54     return nil;
56   DCHECK([cell isKindOfClass:[AutocompleteTextFieldCell class]]);
57   return static_cast<AutocompleteTextFieldCell*>(cell);
60 // Reroute events for the decoration area to the field editor.  This
61 // will cause the cursor to be moved as close to the edge where the
62 // event was seen as possible.
64 // The reason for this code's existence is subtle.  NSTextField
65 // implements text selection and editing in terms of a "field editor".
66 // This is an NSTextView which is installed as a subview of the
67 // control when the field becomes first responder.  When the field
68 // editor is installed, it will get -mouseDown: events and handle
69 // them, rather than the text field - EXCEPT for the event which
70 // caused the change in first responder, or events which fall in the
71 // decorations outside the field editor's area.  In that case, the
72 // default NSTextField code will setup the field editor all over
73 // again, which has the side effect of doing "select all" on the text.
74 // This effect can be observed with a normal NSTextField if you click
75 // in the narrow border area, and is only really a problem because in
76 // our case the focus ring surrounds decorations which look clickable.
78 // When the user first clicks on the field, after installing the field
79 // editor the default NSTextField code detects if the hit is in the
80 // field editor area, and if so sets the selection to {0,0} to clear
81 // the selection before forwarding the event to the field editor for
82 // processing (it will set the cursor position).  This also starts the
83 // click-drag selection machinery.
85 // This code does the same thing for cases where the click was in the
86 // decoration area.  This allows the user to click-drag starting from
87 // a decoration area and get the expected selection behaviour,
88 // likewise for multiple clicks in those areas.
89 - (void)mouseDown:(NSEvent*)theEvent {
90   // TODO(groby): Figure out if OnMouseDown needs to be postponed/skipped
91   // for button decorations.
92   if (observer_)
93     observer_->OnMouseDown([theEvent buttonNumber]);
95   // If the click was a Control-click, bring up the context menu.
96   // |NSTextField| handles these cases inconsistently if the field is
97   // not already first responder.
98   if (([theEvent modifierFlags] & NSControlKeyMask) != 0) {
99     NSText* editor = [self currentEditor];
100     NSMenu* menu = [editor menuForEvent:theEvent];
101     [NSMenu popUpContextMenu:menu withEvent:theEvent forView:editor];
102     return;
103   }
105   const NSPoint location =
106       [self convertPoint:[theEvent locationInWindow] fromView:nil];
107   const NSRect bounds([self bounds]);
109   AutocompleteTextFieldCell* cell = [self cell];
110   const NSRect textFrame([cell textFrameForFrame:bounds]);
112   // A version of the textFrame which extends across the field's
113   // entire width.
115   const NSRect fullFrame(NSMakeRect(bounds.origin.x, textFrame.origin.y,
116                                     bounds.size.width, textFrame.size.height));
118   // If the mouse is in the editing area, or above or below where the
119   // editing area would be if we didn't add decorations, forward to
120   // NSTextField -mouseDown: because it does the right thing.  The
121   // above/below test is needed because NSTextView treats mouse events
122   // above/below as select-to-end-in-that-direction, which makes
123   // things janky.
124   BOOL flipped = [self isFlipped];
125   if (NSMouseInRect(location, textFrame, flipped) ||
126       !NSMouseInRect(location, fullFrame, flipped)) {
127     [super mouseDown:theEvent];
129     // After the event has been handled, if the current event is a
130     // mouse up and no selection was created (the mouse didn't move),
131     // select the entire field.
132     // NOTE(shess): This does not interfere with single-clicking to
133     // place caret after a selection is made.  An NSTextField only has
134     // a selection when it has a field editor.  The field editor is an
135     // NSText subview, which will receive the -mouseDown: in that
136     // case, and this code will never fire.
137     NSText* editor = [self currentEditor];
138     if (editor) {
139       NSEvent* currentEvent = [NSApp currentEvent];
140       if ([currentEvent type] == NSLeftMouseUp &&
141           ![editor selectedRange].length &&
142           (!observer_ || observer_->ShouldSelectAllOnMouseDown())) {
143         [editor selectAll:nil];
144       }
145     }
147     return;
148   }
150   // Give the cell a chance to intercept clicks in page-actions and
151   // other decorative items.
152   if ([cell mouseDown:theEvent inRect:bounds ofView:self]) {
153     return;
154   }
156   NSText* editor = [self currentEditor];
158   // We should only be here if we accepted first-responder status and
159   // have a field editor.  If one of these fires, it means some
160   // assumptions are being broken.
161   DCHECK(editor != nil);
162   DCHECK([editor isDescendantOf:self]);
164   // -becomeFirstResponder does a select-all, which we don't want
165   // because it can lead to a dragged-text situation.  Clear the
166   // selection (any valid empty selection will do).
167   [editor setSelectedRange:NSMakeRange(0, 0)];
169   // If the event is to the right of the editing area, scroll the
170   // field editor to the end of the content so that the selection
171   // doesn't initiate from somewhere in the middle of the text.
172   if (location.x > NSMaxX(textFrame)) {
173     [editor scrollRangeToVisible:NSMakeRange([[self stringValue] length], 0)];
174   }
176   [editor mouseDown:theEvent];
179 - (void)rightMouseDown:(NSEvent*)event {
180   if (observer_)
181     observer_->OnMouseDown([event buttonNumber]);
182   [super rightMouseDown:event];
185 - (void)otherMouseDown:(NSEvent *)event {
186   if (observer_)
187     observer_->OnMouseDown([event buttonNumber]);
188   [super otherMouseDown:event];
191 // Overridden so that cursor and tooltip rects can be updated.
192 - (void)setFrame:(NSRect)frameRect {
193   [super setFrame:frameRect];
194   if (observer_) {
195     observer_->OnFrameChanged();
196   }
197   [self updateMouseTracking];
200 - (void)setAttributedStringValue:(NSAttributedString*)aString {
201   AutocompleteTextFieldEditor* editor =
202       static_cast<AutocompleteTextFieldEditor*>([self currentEditor]);
204   if (!editor) {
205     [super setAttributedStringValue:aString];
206   } else {
207     // The type of the field editor must be AutocompleteTextFieldEditor,
208     // otherwise things won't work.
209     DCHECK([editor isKindOfClass:[AutocompleteTextFieldEditor class]]);
211     [editor setAttributedString:aString];
212   }
215 - (NSUndoManager*)undoManagerForTextView:(NSTextView*)textView {
216   if (!undoManager_.get())
217     undoManager_.reset([[NSUndoManager alloc] init]);
218   return undoManager_.get();
221 - (void)animateToFrame:(NSRect)frame {
222   [self stopAnimation];
223   NSDictionary* animationDictionary = @{
224     NSViewAnimationTargetKey : self,
225     NSViewAnimationStartFrameKey : [NSValue valueWithRect:[self frame]],
226     NSViewAnimationEndFrameKey : [NSValue valueWithRect:frame]
227   };
228   [resizeAnimation_ setViewAnimations:@[ animationDictionary ]];
229   [resizeAnimation_ startAnimation];
232 - (void)stopAnimation {
233   if ([resizeAnimation_ isAnimating]) {
234     // [NSViewAnimation stopAnimation] results in advancing the animation to
235     // the end. Since this is almost certainly not the behavior we want, reset
236     // the frame to the current frame.
237     NSRect frame = [self frame];
238     [resizeAnimation_ stopAnimation];
239     [self setFrame:frame];
240   }
243 - (void)clearUndoChain {
244   [undoManager_ removeAllActions];
247 - (NSRange)textView:(NSTextView *)aTextView
248     willChangeSelectionFromCharacterRange:(NSRange)oldRange
249     toCharacterRange:(NSRange)newRange {
250   if (observer_)
251     return observer_->SelectionRangeForProposedRange(newRange);
252   return newRange;
255 - (void)addToolTip:(NSString*)tooltip forRect:(NSRect)aRect {
256   [currentToolTips_ addObject:tooltip];
257   [self addToolTipRect:aRect owner:tooltip userData:nil];
260 - (void)setGrayTextAutocompletion:(NSString*)suggestText
261                         textColor:(NSColor*)suggestColor {
262   [self setNeedsDisplay:YES];
263   suggestText_.reset([suggestText retain]);
264   suggestColor_.reset([suggestColor retain]);
267 - (NSString*)suggestText {
268   return suggestText_;
271 - (NSColor*)suggestColor {
272   return suggestColor_;
275 - (NSPoint)bubblePointForDecoration:(LocationBarDecoration*)decoration {
276   const NSRect frame =
277       [[self cell] frameForDecoration:decoration inFrame:[self bounds]];
278   const NSPoint point = decoration->GetBubblePointInFrame(frame);
279   return [self convertPoint:point toView:nil];
282 // TODO(shess): -resetFieldEditorFrameIfNeeded is the place where
283 // changes to the cell layout should be flushed.  LocationBarViewMac
284 // and ToolbarController are calling this routine directly, and I
285 // think they are probably wrong.
286 // http://crbug.com/40053
287 - (void)updateMouseTracking {
288   // This will force |resetCursorRects| to be called, as it is not to be called
289   // directly.
290   [[self window] invalidateCursorRectsForView:self];
292   // |removeAllToolTips| only removes those set on the current NSView, not any
293   // subviews. Unless more tooltips are added to this view, this should suffice
294   // in place of managing a set of NSToolTipTag objects.
295   [self removeAllToolTips];
297   // Reload the decoration tooltips.
298   [currentToolTips_ removeAllObjects];
299   [[self cell] updateToolTipsInRect:[self bounds] ofView:self];
302 // NOTE(shess): http://crbug.com/19116 describes a weird bug which
303 // happens when the user runs a Print panel on Leopard.  After that,
304 // spurious -controlTextDidBeginEditing notifications are sent when an
305 // NSTextField is firstResponder, even though -currentEditor on that
306 // field returns nil.  That notification caused significant problems
307 // in OmniboxViewMac.  -textDidBeginEditing: was NOT being
308 // sent in those cases, so this approach doesn't have the problem.
309 - (void)textDidBeginEditing:(NSNotification*)aNotification {
310   [super textDidBeginEditing:aNotification];
311   if (observer_) {
312     observer_->OnDidBeginEditing();
313   }
316 - (void)textDidEndEditing:(NSNotification *)aNotification {
317   [super textDidEndEditing:aNotification];
318   if (observer_) {
319     observer_->OnDidEndEditing();
320   }
323 // When the window resigns, make sure the autocomplete popup is no
324 // longer visible, since the user's focus is elsewhere.
325 - (void)windowDidResignKey:(NSNotification*)notification {
326   DCHECK_EQ([self window], [notification object]);
327   if (observer_)
328     observer_->ClosePopup();
331 - (void)windowDidResize:(NSNotification*)notification {
332   DCHECK_EQ([self window], [notification object]);
333   if (observer_)
334     observer_->OnFrameChanged();
337 - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
338   if ([self window]) {
339     NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
340     [nc removeObserver:self
341                   name:NSWindowDidResignKeyNotification
342                 object:[self window]];
343     [nc removeObserver:self
344                   name:NSWindowDidResizeNotification
345                 object:[self window]];
346   }
349 - (void)viewDidMoveToWindow {
350   if ([self window]) {
351     NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
352     [nc addObserver:self
353            selector:@selector(windowDidResignKey:)
354                name:NSWindowDidResignKeyNotification
355              object:[self window]];
356     [nc addObserver:self
357            selector:@selector(windowDidResize:)
358                name:NSWindowDidResizeNotification
359              object:[self window]];
360     // Only register for drops if not in a popup window. Lazily create the
361     // drop handler when the type of window is known.
362     BrowserWindowController* windowController =
363         [BrowserWindowController browserWindowControllerForView:self];
364     if ([windowController isTabbedWindow])
365       dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
366   }
369 // NSTextField becomes first responder by installing a "field editor"
370 // subview.  Clicks outside the field editor (such as a decoration)
371 // will attempt to make the field the first-responder again, which
372 // causes a select-all, even if the decoration handles the click.  If
373 // the field editor is already in place, don't accept first responder
374 // again.  This allows the selection to be unmodified if the click is
375 // handled by a decoration or context menu (|-mouseDown:| will still
376 // change it if appropriate).
377 - (BOOL)acceptsFirstResponder {
378   if ([self currentEditor]) {
379     DCHECK_EQ([self currentEditor], [[self window] firstResponder]);
380     return NO;
381   }
383   // If the event is a left-mouse click, and it lands on a decoration, then the
384   // event should not cause the text field to become first responder.
385   NSEvent* event = [NSApp currentEvent];
386   if ([event type] == NSLeftMouseDown) {
387     LocationBarDecoration* decoration =
388         [[self cell] decorationForEvent:event inRect:[self bounds] ofView:self];
389     if (decoration && decoration->AcceptsMousePress())
390       return NO;
391   }
393   return [super acceptsFirstResponder];
396 // (Overridden from NSResponder)
397 - (BOOL)becomeFirstResponder {
398   BOOL doAccept = [super becomeFirstResponder];
399   if (doAccept) {
400     [[BrowserWindowController browserWindowControllerForView:self]
401         lockBarVisibilityForOwner:self withAnimation:YES delay:NO];
403     // Tells the observer that we get the focus.
404     // But we can't call observer_->OnKillFocus() in resignFirstResponder:,
405     // because the first responder will be immediately set to the field editor
406     // when calling [super becomeFirstResponder], thus we won't receive
407     // resignFirstResponder: anymore when losing focus.
408     [[self cell] handleFocusEvent:[NSApp currentEvent] ofView:self];
409   }
410   return doAccept;
413 // (Overridden from NSResponder)
414 - (BOOL)resignFirstResponder {
415   BOOL doResign = [super resignFirstResponder];
416   if (doResign) {
417     [[BrowserWindowController browserWindowControllerForView:self]
418         releaseBarVisibilityForOwner:self withAnimation:YES delay:YES];
419   }
420   return doResign;
423 - (void)drawRect:(NSRect)rect {
424   [super drawRect:rect];
425   autocomplete_text_field::DrawGrayTextAutocompletion(
426       [self attributedStringValue],
427       suggestText_,
428       suggestColor_,
429       self,
430       [[self cell] drawingRectForBounds:[self bounds]]);
433 // (URLDropTarget protocol)
434 - (id<URLDropTargetController>)urlDropController {
435   BrowserWindowController* windowController =
436       [BrowserWindowController browserWindowControllerForView:self];
437   return [windowController toolbarController];
440 // (URLDropTarget protocol)
441 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
442   // Make ourself the first responder, which will select the text to indicate
443   // that our contents would be replaced by a drop.
444   // TODO(viettrungluu): crbug.com/30809 -- this is a hack since it steals focus
445   // and doesn't return it.
446   [[self window] makeFirstResponder:self];
447   return [dropHandler_ draggingEntered:sender];
450 // (URLDropTarget protocol)
451 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
452   return [dropHandler_ draggingUpdated:sender];
455 // (URLDropTarget protocol)
456 - (void)draggingExited:(id<NSDraggingInfo>)sender {
457   return [dropHandler_ draggingExited:sender];
460 // (URLDropTarget protocol)
461 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
462   return [dropHandler_ performDragOperation:sender];
465 - (NSMenu*)decorationMenuForEvent:(NSEvent*)event {
466   AutocompleteTextFieldCell* cell = [self cell];
467   return [cell decorationMenuForEvent:event inRect:[self bounds] ofView:self];
470 - (ViewID)viewID {
471   return VIEW_ID_OMNIBOX;
474 @end
476 namespace autocomplete_text_field {
478 void DrawGrayTextAutocompletion(NSAttributedString* mainText,
479                                 NSString* suggestText,
480                                 NSColor* suggestColor,
481                                 NSView* controlView,
482                                 NSRect frame) {
483   if (![suggestText length])
484     return;
486   base::scoped_nsobject<NSTextFieldCell> cell(
487       [[NSTextFieldCell alloc] initTextCell:@""]);
488   [cell setBordered:NO];
489   [cell setDrawsBackground:NO];
490   [cell setEditable:NO];
492   base::scoped_nsobject<NSMutableAttributedString> combinedText(
493       [[NSMutableAttributedString alloc] initWithAttributedString:mainText]);
494   NSRange range = NSMakeRange([combinedText length], 0);
495   [combinedText replaceCharactersInRange:range withString:suggestText];
496   [combinedText addAttribute:NSForegroundColorAttributeName
497                        value:suggestColor
498                        range:NSMakeRange(range.location, [suggestText length])];
499   [cell setAttributedStringValue:combinedText];
501   CGFloat mainTextWidth = [mainText size].width;
502   CGFloat suggestWidth = NSWidth(frame) - mainTextWidth;
503   NSRect suggestRect = NSMakeRect(NSMinX(frame) + mainTextWidth,
504                                   NSMinY(frame),
505                                   suggestWidth,
506                                   NSHeight(frame));
508   gfx::ScopedNSGraphicsContextSaveGState saveGState;
509   NSRectClip(suggestRect);
510   [cell drawInteriorWithFrame:frame inView:controlView];
513 }  // namespace autocomplete_text_field