Revert of Add button to add new FSP services to Files app. (patchset #8 id:140001...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / location_bar / autocomplete_text_field.mm
blob6c25cc82855b4c1f9dfe9e570a413e9aaf04c648
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 // Received from tracking areas. Pass it down to the cell, and add the field.
192 - (void)mouseEntered:(NSEvent*)theEvent {
193   [[self cell] mouseEntered:theEvent inView:self];
196 // Received from tracking areas. Pass it down to the cell, and add the field.
197 - (void)mouseExited:(NSEvent*)theEvent {
198   [[self cell] mouseExited:theEvent inView:self];
201 // Overridden so that cursor and tooltip rects can be updated.
202 - (void)setFrame:(NSRect)frameRect {
203   [super setFrame:frameRect];
204   if (observer_) {
205     observer_->OnFrameChanged();
206   }
207   [self updateMouseTracking];
210 - (void)setAttributedStringValue:(NSAttributedString*)aString {
211   AutocompleteTextFieldEditor* editor =
212       static_cast<AutocompleteTextFieldEditor*>([self currentEditor]);
214   if (!editor) {
215     [super setAttributedStringValue:aString];
216   } else {
217     // The type of the field editor must be AutocompleteTextFieldEditor,
218     // otherwise things won't work.
219     DCHECK([editor isKindOfClass:[AutocompleteTextFieldEditor class]]);
221     [editor setAttributedString:aString];
222   }
225 - (NSUndoManager*)undoManagerForTextView:(NSTextView*)textView {
226   if (!undoManager_.get())
227     undoManager_.reset([[NSUndoManager alloc] init]);
228   return undoManager_.get();
231 - (void)animateToFrame:(NSRect)frame {
232   [self stopAnimation];
233   NSDictionary* animationDictionary = @{
234     NSViewAnimationTargetKey : self,
235     NSViewAnimationStartFrameKey : [NSValue valueWithRect:[self frame]],
236     NSViewAnimationEndFrameKey : [NSValue valueWithRect:frame]
237   };
238   [resizeAnimation_ setViewAnimations:@[ animationDictionary ]];
239   [resizeAnimation_ startAnimation];
242 - (void)stopAnimation {
243   if ([resizeAnimation_ isAnimating]) {
244     // [NSViewAnimation stopAnimation] results in advancing the animation to
245     // the end. Since this is almost certainly not the behavior we want, reset
246     // the frame to the current frame.
247     NSRect frame = [self frame];
248     [resizeAnimation_ stopAnimation];
249     [self setFrame:frame];
250   }
253 - (void)clearUndoChain {
254   [undoManager_ removeAllActions];
257 - (NSRange)textView:(NSTextView *)aTextView
258     willChangeSelectionFromCharacterRange:(NSRange)oldRange
259     toCharacterRange:(NSRange)newRange {
260   if (observer_)
261     return observer_->SelectionRangeForProposedRange(newRange);
262   return newRange;
265 - (void)addToolTip:(NSString*)tooltip forRect:(NSRect)aRect {
266   [currentToolTips_ addObject:tooltip];
267   [self addToolTipRect:aRect owner:tooltip userData:nil];
270 - (void)setGrayTextAutocompletion:(NSString*)suggestText
271                         textColor:(NSColor*)suggestColor {
272   [self setNeedsDisplay:YES];
273   suggestText_.reset([suggestText retain]);
274   suggestColor_.reset([suggestColor retain]);
277 - (NSString*)suggestText {
278   return suggestText_;
281 - (NSColor*)suggestColor {
282   return suggestColor_;
285 - (NSPoint)bubblePointForDecoration:(LocationBarDecoration*)decoration {
286   const NSRect frame =
287       [[self cell] frameForDecoration:decoration inFrame:[self bounds]];
288   const NSPoint point = decoration->GetBubblePointInFrame(frame);
289   return [self convertPoint:point toView:nil];
292 // TODO(shess): -resetFieldEditorFrameIfNeeded is the place where
293 // changes to the cell layout should be flushed.  LocationBarViewMac
294 // and ToolbarController are calling this routine directly, and I
295 // think they are probably wrong.
296 // http://crbug.com/40053
297 - (void)updateMouseTracking {
298   // This will force |resetCursorRects| to be called, as it is not to be called
299   // directly.
300   [[self window] invalidateCursorRectsForView:self];
302   // |removeAllToolTips| only removes those set on the current NSView, not any
303   // subviews. Unless more tooltips are added to this view, this should suffice
304   // in place of managing a set of NSToolTipTag objects.
305   [self removeAllToolTips];
307   // Reload the decoration tooltips.
308   [currentToolTips_ removeAllObjects];
309   [[self cell] updateToolTipsInRect:[self bounds] ofView:self];
311   // Setup/update the tracking areas for the decorations.
312   [[self cell] setUpTrackingAreasInRect:[self bounds] ofView:self];
315 // NOTE(shess): http://crbug.com/19116 describes a weird bug which
316 // happens when the user runs a Print panel on Leopard.  After that,
317 // spurious -controlTextDidBeginEditing notifications are sent when an
318 // NSTextField is firstResponder, even though -currentEditor on that
319 // field returns nil.  That notification caused significant problems
320 // in OmniboxViewMac.  -textDidBeginEditing: was NOT being
321 // sent in those cases, so this approach doesn't have the problem.
322 - (void)textDidBeginEditing:(NSNotification*)aNotification {
323   [super textDidBeginEditing:aNotification];
324   if (observer_) {
325     observer_->OnDidBeginEditing();
326   }
329 - (void)textDidEndEditing:(NSNotification *)aNotification {
330   [super textDidEndEditing:aNotification];
331   if (observer_) {
332     observer_->OnDidEndEditing();
333   }
336 // When the window resigns, make sure the autocomplete popup is no
337 // longer visible, since the user's focus is elsewhere.
338 - (void)windowDidResignKey:(NSNotification*)notification {
339   DCHECK_EQ([self window], [notification object]);
340   if (observer_)
341     observer_->ClosePopup();
344 - (void)windowDidResize:(NSNotification*)notification {
345   DCHECK_EQ([self window], [notification object]);
346   if (observer_)
347     observer_->OnFrameChanged();
350 - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
351   if ([self window]) {
352     NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
353     [nc removeObserver:self
354                   name:NSWindowDidResignKeyNotification
355                 object:[self window]];
356     [nc removeObserver:self
357                   name:NSWindowDidResizeNotification
358                 object:[self window]];
359   }
362 - (void)viewDidMoveToWindow {
363   if ([self window]) {
364     NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
365     [nc addObserver:self
366            selector:@selector(windowDidResignKey:)
367                name:NSWindowDidResignKeyNotification
368              object:[self window]];
369     [nc addObserver:self
370            selector:@selector(windowDidResize:)
371                name:NSWindowDidResizeNotification
372              object:[self window]];
373     // Only register for drops if not in a popup window. Lazily create the
374     // drop handler when the type of window is known.
375     BrowserWindowController* windowController =
376         [BrowserWindowController browserWindowControllerForView:self];
377     if ([windowController isTabbedWindow])
378       dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
379   }
382 // NSTextField becomes first responder by installing a "field editor"
383 // subview.  Clicks outside the field editor (such as a decoration)
384 // will attempt to make the field the first-responder again, which
385 // causes a select-all, even if the decoration handles the click.  If
386 // the field editor is already in place, don't accept first responder
387 // again.  This allows the selection to be unmodified if the click is
388 // handled by a decoration or context menu (|-mouseDown:| will still
389 // change it if appropriate).
390 - (BOOL)acceptsFirstResponder {
391   if ([self currentEditor]) {
392     DCHECK_EQ([self currentEditor], [[self window] firstResponder]);
393     return NO;
394   }
395   return [super acceptsFirstResponder];
398 // (Overridden from NSResponder)
399 - (BOOL)becomeFirstResponder {
400   BOOL doAccept = [super becomeFirstResponder];
401   if (doAccept) {
402     [[BrowserWindowController browserWindowControllerForView:self]
403         lockBarVisibilityForOwner:self withAnimation:YES delay:NO];
405     // Tells the observer that we get the focus.
406     // But we can't call observer_->OnKillFocus() in resignFirstResponder:,
407     // because the first responder will be immediately set to the field editor
408     // when calling [super becomeFirstResponder], thus we won't receive
409     // resignFirstResponder: anymore when losing focus.
410     [[self cell] handleFocusEvent:[NSApp currentEvent] ofView:self];
411   }
412   return doAccept;
415 // (Overridden from NSResponder)
416 - (BOOL)resignFirstResponder {
417   BOOL doResign = [super resignFirstResponder];
418   if (doResign) {
419     [[BrowserWindowController browserWindowControllerForView:self]
420         releaseBarVisibilityForOwner:self withAnimation:YES delay:YES];
421   }
422   return doResign;
425 - (void)drawRect:(NSRect)rect {
426   [super drawRect:rect];
427   autocomplete_text_field::DrawGrayTextAutocompletion(
428       [self attributedStringValue],
429       suggestText_,
430       suggestColor_,
431       self,
432       [[self cell] drawingRectForBounds:[self bounds]]);
435 // (URLDropTarget protocol)
436 - (id<URLDropTargetController>)urlDropController {
437   BrowserWindowController* windowController =
438       [BrowserWindowController browserWindowControllerForView:self];
439   return [windowController toolbarController];
442 // (URLDropTarget protocol)
443 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
444   // Make ourself the first responder, which will select the text to indicate
445   // that our contents would be replaced by a drop.
446   // TODO(viettrungluu): crbug.com/30809 -- this is a hack since it steals focus
447   // and doesn't return it.
448   [[self window] makeFirstResponder:self];
449   return [dropHandler_ draggingEntered:sender];
452 // (URLDropTarget protocol)
453 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
454   return [dropHandler_ draggingUpdated:sender];
457 // (URLDropTarget protocol)
458 - (void)draggingExited:(id<NSDraggingInfo>)sender {
459   return [dropHandler_ draggingExited:sender];
462 // (URLDropTarget protocol)
463 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
464   return [dropHandler_ performDragOperation:sender];
467 - (NSMenu*)decorationMenuForEvent:(NSEvent*)event {
468   AutocompleteTextFieldCell* cell = [self cell];
469   return [cell decorationMenuForEvent:event inRect:[self bounds] ofView:self];
472 - (ViewID)viewID {
473   return VIEW_ID_OMNIBOX;
476 @end
478 namespace autocomplete_text_field {
480 void DrawGrayTextAutocompletion(NSAttributedString* mainText,
481                                 NSString* suggestText,
482                                 NSColor* suggestColor,
483                                 NSView* controlView,
484                                 NSRect frame) {
485   if (![suggestText length])
486     return;
488   base::scoped_nsobject<NSTextFieldCell> cell(
489       [[NSTextFieldCell alloc] initTextCell:@""]);
490   [cell setBordered:NO];
491   [cell setDrawsBackground:NO];
492   [cell setEditable:NO];
494   base::scoped_nsobject<NSMutableAttributedString> combinedText(
495       [[NSMutableAttributedString alloc] initWithAttributedString:mainText]);
496   NSRange range = NSMakeRange([combinedText length], 0);
497   [combinedText replaceCharactersInRange:range withString:suggestText];
498   [combinedText addAttribute:NSForegroundColorAttributeName
499                        value:suggestColor
500                        range:NSMakeRange(range.location, [suggestText length])];
501   [cell setAttributedStringValue:combinedText];
503   CGFloat mainTextWidth = [mainText size].width;
504   CGFloat suggestWidth = NSWidth(frame) - mainTextWidth;
505   NSRect suggestRect = NSMakeRect(NSMinX(frame) + mainTextWidth,
506                                   NSMinY(frame),
507                                   suggestWidth,
508                                   NSHeight(frame));
510   gfx::ScopedNSGraphicsContextSaveGState saveGState;
511   NSRectClip(suggestRect);
512   [cell drawInteriorWithFrame:frame inView:controlView];
515 }  // namespace autocomplete_text_field