[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / location_bar / autocomplete_text_field.mm
blobbb920d562b65475c45eb6caf1efed3929067cfee
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 @implementation AutocompleteTextField
19 @synthesize observer = observer_;
21 + (Class)cellClass {
22   return [AutocompleteTextFieldCell class];
25 - (void)dealloc {
26   [[NSNotificationCenter defaultCenter] removeObserver:self];
27   [super dealloc];
30 - (void)awakeFromNib {
31   DCHECK([[self cell] isKindOfClass:[AutocompleteTextFieldCell class]]);
32   [[self cell] setTruncatesLastVisibleLine:YES];
33   [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
34   currentToolTips_.reset([[NSMutableArray alloc] init]);
37 - (void)flagsChanged:(NSEvent*)theEvent {
38   if (observer_) {
39     const bool controlFlag = ([theEvent modifierFlags]&NSControlKeyMask) != 0;
40     observer_->OnControlKeyChanged(controlFlag);
41   }
44 - (AutocompleteTextFieldCell*)cell {
45   NSCell* cell = [super cell];
46   if (!cell)
47     return nil;
49   DCHECK([cell isKindOfClass:[AutocompleteTextFieldCell class]]);
50   return static_cast<AutocompleteTextFieldCell*>(cell);
53 // Reroute events for the decoration area to the field editor.  This
54 // will cause the cursor to be moved as close to the edge where the
55 // event was seen as possible.
57 // The reason for this code's existence is subtle.  NSTextField
58 // implements text selection and editing in terms of a "field editor".
59 // This is an NSTextView which is installed as a subview of the
60 // control when the field becomes first responder.  When the field
61 // editor is installed, it will get -mouseDown: events and handle
62 // them, rather than the text field - EXCEPT for the event which
63 // caused the change in first responder, or events which fall in the
64 // decorations outside the field editor's area.  In that case, the
65 // default NSTextField code will setup the field editor all over
66 // again, which has the side effect of doing "select all" on the text.
67 // This effect can be observed with a normal NSTextField if you click
68 // in the narrow border area, and is only really a problem because in
69 // our case the focus ring surrounds decorations which look clickable.
71 // When the user first clicks on the field, after installing the field
72 // editor the default NSTextField code detects if the hit is in the
73 // field editor area, and if so sets the selection to {0,0} to clear
74 // the selection before forwarding the event to the field editor for
75 // processing (it will set the cursor position).  This also starts the
76 // click-drag selection machinery.
78 // This code does the same thing for cases where the click was in the
79 // decoration area.  This allows the user to click-drag starting from
80 // a decoration area and get the expected selection behaviour,
81 // likewise for multiple clicks in those areas.
82 - (void)mouseDown:(NSEvent*)theEvent {
83   // TODO(groby): Figure out if OnMouseDown needs to be postponed/skipped
84   // for button decorations.
85   if (observer_)
86     observer_->OnMouseDown([theEvent buttonNumber]);
88   // If the click was a Control-click, bring up the context menu.
89   // |NSTextField| handles these cases inconsistently if the field is
90   // not already first responder.
91   if (([theEvent modifierFlags] & NSControlKeyMask) != 0) {
92     NSText* editor = [self currentEditor];
93     NSMenu* menu = [editor menuForEvent:theEvent];
94     [NSMenu popUpContextMenu:menu withEvent:theEvent forView:editor];
95     return;
96   }
98   const NSPoint location =
99       [self convertPoint:[theEvent locationInWindow] fromView:nil];
100   const NSRect bounds([self bounds]);
102   AutocompleteTextFieldCell* cell = [self cell];
103   const NSRect textFrame([cell textFrameForFrame:bounds]);
105   // A version of the textFrame which extends across the field's
106   // entire width.
108   const NSRect fullFrame(NSMakeRect(bounds.origin.x, textFrame.origin.y,
109                                     bounds.size.width, textFrame.size.height));
111   // If the mouse is in the editing area, or above or below where the
112   // editing area would be if we didn't add decorations, forward to
113   // NSTextField -mouseDown: because it does the right thing.  The
114   // above/below test is needed because NSTextView treats mouse events
115   // above/below as select-to-end-in-that-direction, which makes
116   // things janky.
117   BOOL flipped = [self isFlipped];
118   if (NSMouseInRect(location, textFrame, flipped) ||
119       !NSMouseInRect(location, fullFrame, flipped)) {
120     [super mouseDown:theEvent];
122     // After the event has been handled, if the current event is a
123     // mouse up and no selection was created (the mouse didn't move),
124     // select the entire field.
125     // NOTE(shess): This does not interfere with single-clicking to
126     // place caret after a selection is made.  An NSTextField only has
127     // a selection when it has a field editor.  The field editor is an
128     // NSText subview, which will receive the -mouseDown: in that
129     // case, and this code will never fire.
130     NSText* editor = [self currentEditor];
131     if (editor) {
132       NSEvent* currentEvent = [NSApp currentEvent];
133       if ([currentEvent type] == NSLeftMouseUp &&
134           ![editor selectedRange].length &&
135           (!observer_ || observer_->ShouldSelectAllOnMouseDown())) {
136         [editor selectAll:nil];
137       }
138     }
140     return;
141   }
143   // Give the cell a chance to intercept clicks in page-actions and
144   // other decorative items.
145   if ([cell mouseDown:theEvent inRect:bounds ofView:self]) {
146     return;
147   }
149   NSText* editor = [self currentEditor];
151   // We should only be here if we accepted first-responder status and
152   // have a field editor.  If one of these fires, it means some
153   // assumptions are being broken.
154   DCHECK(editor != nil);
155   DCHECK([editor isDescendantOf:self]);
157   // -becomeFirstResponder does a select-all, which we don't want
158   // because it can lead to a dragged-text situation.  Clear the
159   // selection (any valid empty selection will do).
160   [editor setSelectedRange:NSMakeRange(0, 0)];
162   // If the event is to the right of the editing area, scroll the
163   // field editor to the end of the content so that the selection
164   // doesn't initiate from somewhere in the middle of the text.
165   if (location.x > NSMaxX(textFrame)) {
166     [editor scrollRangeToVisible:NSMakeRange([[self stringValue] length], 0)];
167   }
169   [editor mouseDown:theEvent];
172 - (void)rightMouseDown:(NSEvent*)event {
173   if (observer_)
174     observer_->OnMouseDown([event buttonNumber]);
175   [super rightMouseDown:event];
178 - (void)otherMouseDown:(NSEvent *)event {
179   if (observer_)
180     observer_->OnMouseDown([event buttonNumber]);
181   [super otherMouseDown:event];
184 // Received from tracking areas. Pass it down to the cell, and add the field.
185 - (void)mouseEntered:(NSEvent*)theEvent {
186   [[self cell] mouseEntered:theEvent inView:self];
189 // Received from tracking areas. Pass it down to the cell, and add the field.
190 - (void)mouseExited:(NSEvent*)theEvent {
191   [[self cell] mouseExited:theEvent inView:self];
194 // Overridden so that cursor and tooltip rects can be updated.
195 - (void)setFrame:(NSRect)frameRect {
196   [super setFrame:frameRect];
197   if (observer_) {
198     observer_->OnFrameChanged();
199   }
200   [self updateMouseTracking];
203 - (void)setAttributedStringValue:(NSAttributedString*)aString {
204   AutocompleteTextFieldEditor* editor =
205       static_cast<AutocompleteTextFieldEditor*>([self currentEditor]);
207   if (!editor) {
208     [super setAttributedStringValue:aString];
209   } else {
210     // The type of the field editor must be AutocompleteTextFieldEditor,
211     // otherwise things won't work.
212     DCHECK([editor isKindOfClass:[AutocompleteTextFieldEditor class]]);
214     [editor setAttributedString:aString];
215   }
218 - (NSUndoManager*)undoManagerForTextView:(NSTextView*)textView {
219   if (!undoManager_.get())
220     undoManager_.reset([[NSUndoManager alloc] init]);
221   return undoManager_.get();
224 - (void)clearUndoChain {
225   [undoManager_ removeAllActions];
228 - (NSRange)textView:(NSTextView *)aTextView
229     willChangeSelectionFromCharacterRange:(NSRange)oldRange
230     toCharacterRange:(NSRange)newRange {
231   if (observer_)
232     return observer_->SelectionRangeForProposedRange(newRange);
233   return newRange;
236 - (void)addToolTip:(NSString*)tooltip forRect:(NSRect)aRect {
237   [currentToolTips_ addObject:tooltip];
238   [self addToolTipRect:aRect owner:tooltip userData:nil];
241 - (void)setGrayTextAutocompletion:(NSString*)suggestText
242                         textColor:(NSColor*)suggestColor {
243   [self setNeedsDisplay:YES];
244   suggestText_.reset([suggestText retain]);
245   suggestColor_.reset([suggestColor retain]);
248 - (NSString*)suggestText {
249   return suggestText_;
252 - (NSColor*)suggestColor {
253   return suggestColor_;
256 - (NSPoint)bubblePointForDecoration:(LocationBarDecoration*)decoration {
257   const NSRect frame =
258       [[self cell] frameForDecoration:decoration inFrame:[self bounds]];
259   const NSPoint point = decoration->GetBubblePointInFrame(frame);
260   return [self convertPoint:point toView:nil];
263 // TODO(shess): -resetFieldEditorFrameIfNeeded is the place where
264 // changes to the cell layout should be flushed.  LocationBarViewMac
265 // and ToolbarController are calling this routine directly, and I
266 // think they are probably wrong.
267 // http://crbug.com/40053
268 - (void)updateMouseTracking {
269   // This will force |resetCursorRects| to be called, as it is not to be called
270   // directly.
271   [[self window] invalidateCursorRectsForView:self];
273   // |removeAllToolTips| only removes those set on the current NSView, not any
274   // subviews. Unless more tooltips are added to this view, this should suffice
275   // in place of managing a set of NSToolTipTag objects.
276   [self removeAllToolTips];
278   // Reload the decoration tooltips.
279   [currentToolTips_ removeAllObjects];
280   [[self cell] updateToolTipsInRect:[self bounds] ofView:self];
282   // Setup/update the tracking areas for the decorations.
283   [[self cell] setUpTrackingAreasInRect:[self bounds] ofView:self];
286 // NOTE(shess): http://crbug.com/19116 describes a weird bug which
287 // happens when the user runs a Print panel on Leopard.  After that,
288 // spurious -controlTextDidBeginEditing notifications are sent when an
289 // NSTextField is firstResponder, even though -currentEditor on that
290 // field returns nil.  That notification caused significant problems
291 // in OmniboxViewMac.  -textDidBeginEditing: was NOT being
292 // sent in those cases, so this approach doesn't have the problem.
293 - (void)textDidBeginEditing:(NSNotification*)aNotification {
294   [super textDidBeginEditing:aNotification];
295   if (observer_) {
296     observer_->OnDidBeginEditing();
297   }
300 - (void)textDidEndEditing:(NSNotification *)aNotification {
301   [super textDidEndEditing:aNotification];
302   if (observer_) {
303     observer_->OnDidEndEditing();
304   }
307 // When the window resigns, make sure the autocomplete popup is no
308 // longer visible, since the user's focus is elsewhere.
309 - (void)windowDidResignKey:(NSNotification*)notification {
310   DCHECK_EQ([self window], [notification object]);
311   if (observer_)
312     observer_->ClosePopup();
315 - (void)windowDidResize:(NSNotification*)notification {
316   DCHECK_EQ([self window], [notification object]);
317   if (observer_)
318     observer_->OnFrameChanged();
321 - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
322   if ([self window]) {
323     NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
324     [nc removeObserver:self
325                   name:NSWindowDidResignKeyNotification
326                 object:[self window]];
327     [nc removeObserver:self
328                   name:NSWindowDidResizeNotification
329                 object:[self window]];
330   }
333 - (void)viewDidMoveToWindow {
334   if ([self window]) {
335     NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
336     [nc addObserver:self
337            selector:@selector(windowDidResignKey:)
338                name:NSWindowDidResignKeyNotification
339              object:[self window]];
340     [nc addObserver:self
341            selector:@selector(windowDidResize:)
342                name:NSWindowDidResizeNotification
343              object:[self window]];
344     // Only register for drops if not in a popup window. Lazily create the
345     // drop handler when the type of window is known.
346     BrowserWindowController* windowController =
347         [BrowserWindowController browserWindowControllerForView:self];
348     if ([windowController isTabbedWindow])
349       dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
350   }
353 // NSTextField becomes first responder by installing a "field editor"
354 // subview.  Clicks outside the field editor (such as a decoration)
355 // will attempt to make the field the first-responder again, which
356 // causes a select-all, even if the decoration handles the click.  If
357 // the field editor is already in place, don't accept first responder
358 // again.  This allows the selection to be unmodified if the click is
359 // handled by a decoration or context menu (|-mouseDown:| will still
360 // change it if appropriate).
361 - (BOOL)acceptsFirstResponder {
362   if ([self currentEditor]) {
363     DCHECK_EQ([self currentEditor], [[self window] firstResponder]);
364     return NO;
365   }
366   return [super acceptsFirstResponder];
369 // (Overridden from NSResponder)
370 - (BOOL)becomeFirstResponder {
371   BOOL doAccept = [super becomeFirstResponder];
372   if (doAccept) {
373     [[BrowserWindowController browserWindowControllerForView:self]
374         lockBarVisibilityForOwner:self withAnimation:YES delay:NO];
376     // Tells the observer that we get the focus.
377     // But we can't call observer_->OnKillFocus() in resignFirstResponder:,
378     // because the first responder will be immediately set to the field editor
379     // when calling [super becomeFirstResponder], thus we won't receive
380     // resignFirstResponder: anymore when losing focus.
381     [[self cell] handleFocusEvent:[NSApp currentEvent] ofView:self];
382   }
383   return doAccept;
386 // (Overridden from NSResponder)
387 - (BOOL)resignFirstResponder {
388   BOOL doResign = [super resignFirstResponder];
389   if (doResign) {
390     [[BrowserWindowController browserWindowControllerForView:self]
391         releaseBarVisibilityForOwner:self withAnimation:YES delay:YES];
392   }
393   return doResign;
396 - (void)drawRect:(NSRect)rect {
397   [super drawRect:rect];
398   autocomplete_text_field::DrawGrayTextAutocompletion(
399       [self attributedStringValue],
400       suggestText_,
401       suggestColor_,
402       self,
403       [[self cell] drawingRectForBounds:[self bounds]]);
406 // (URLDropTarget protocol)
407 - (id<URLDropTargetController>)urlDropController {
408   BrowserWindowController* windowController =
409       [BrowserWindowController browserWindowControllerForView:self];
410   return [windowController toolbarController];
413 // (URLDropTarget protocol)
414 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
415   // Make ourself the first responder, which will select the text to indicate
416   // that our contents would be replaced by a drop.
417   // TODO(viettrungluu): crbug.com/30809 -- this is a hack since it steals focus
418   // and doesn't return it.
419   [[self window] makeFirstResponder:self];
420   return [dropHandler_ draggingEntered:sender];
423 // (URLDropTarget protocol)
424 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
425   return [dropHandler_ draggingUpdated:sender];
428 // (URLDropTarget protocol)
429 - (void)draggingExited:(id<NSDraggingInfo>)sender {
430   return [dropHandler_ draggingExited:sender];
433 // (URLDropTarget protocol)
434 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
435   return [dropHandler_ performDragOperation:sender];
438 - (NSMenu*)decorationMenuForEvent:(NSEvent*)event {
439   AutocompleteTextFieldCell* cell = [self cell];
440   return [cell decorationMenuForEvent:event inRect:[self bounds] ofView:self];
443 - (ViewID)viewID {
444   return VIEW_ID_OMNIBOX;
447 @end
449 namespace autocomplete_text_field {
451 void DrawGrayTextAutocompletion(NSAttributedString* mainText,
452                                 NSString* suggestText,
453                                 NSColor* suggestColor,
454                                 NSView* controlView,
455                                 NSRect frame) {
456   if (![suggestText length])
457     return;
459   base::scoped_nsobject<NSTextFieldCell> cell(
460       [[NSTextFieldCell alloc] initTextCell:@""]);
461   [cell setBordered:NO];
462   [cell setDrawsBackground:NO];
463   [cell setEditable:NO];
465   base::scoped_nsobject<NSMutableAttributedString> combinedText(
466       [[NSMutableAttributedString alloc] initWithAttributedString:mainText]);
467   NSRange range = NSMakeRange([combinedText length], 0);
468   [combinedText replaceCharactersInRange:range withString:suggestText];
469   [combinedText addAttribute:NSForegroundColorAttributeName
470                        value:suggestColor
471                        range:NSMakeRange(range.location, [suggestText length])];
472   [cell setAttributedStringValue:combinedText];
474   CGFloat mainTextWidth = [mainText size].width;
475   CGFloat suggestWidth = NSWidth(frame) - mainTextWidth;
476   NSRect suggestRect = NSMakeRect(NSMinX(frame) + mainTextWidth,
477                                   NSMinY(frame),
478                                   suggestWidth,
479                                   NSHeight(frame));
481   gfx::ScopedNSGraphicsContextSaveGState saveGState;
482   NSRectClip(suggestRect);
483   [cell drawInteriorWithFrame:frame inView:controlView];
486 }  // namespace autocomplete_text_field