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"
18 const CGFloat kAnimationDuration = 0.2;
21 @implementation AutocompleteTextField
23 @synthesize observer = observer_;
26 return [AutocompleteTextFieldCell class];
30 [[NSNotificationCenter defaultCenter] removeObserver:self];
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 {
46 const bool controlFlag = ([theEvent modifierFlags]&NSControlKeyMask) != 0;
47 observer_->OnControlKeyChanged(controlFlag);
51 - (AutocompleteTextFieldCell*)cell {
52 NSCell* cell = [super cell];
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.
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];
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
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
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];
139 NSEvent* currentEvent = [NSApp currentEvent];
140 if ([currentEvent type] == NSLeftMouseUp &&
141 ![editor selectedRange].length &&
142 (!observer_ || observer_->ShouldSelectAllOnMouseDown())) {
143 [editor selectAll:nil];
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]) {
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)];
176 [editor mouseDown:theEvent];
179 - (void)rightMouseDown:(NSEvent*)event {
181 observer_->OnMouseDown([event buttonNumber]);
182 [super rightMouseDown:event];
185 - (void)otherMouseDown:(NSEvent *)event {
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];
195 observer_->OnFrameChanged();
197 [self updateMouseTracking];
200 - (void)setAttributedStringValue:(NSAttributedString*)aString {
201 AutocompleteTextFieldEditor* editor =
202 static_cast<AutocompleteTextFieldEditor*>([self currentEditor]);
205 [super setAttributedStringValue:aString];
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];
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]
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];
243 - (void)clearUndoChain {
244 [undoManager_ removeAllActions];
247 - (NSRange)textView:(NSTextView *)aTextView
248 willChangeSelectionFromCharacterRange:(NSRange)oldRange
249 toCharacterRange:(NSRange)newRange {
251 return observer_->SelectionRangeForProposedRange(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 {
271 - (NSColor*)suggestColor {
272 return suggestColor_;
275 - (NSPoint)bubblePointForDecoration:(LocationBarDecoration*)decoration {
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
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];
312 observer_->OnDidBeginEditing();
316 - (void)textDidEndEditing:(NSNotification *)aNotification {
317 [super textDidEndEditing:aNotification];
319 observer_->OnDidEndEditing();
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]);
328 observer_->ClosePopup();
331 - (void)windowDidResize:(NSNotification*)notification {
332 DCHECK_EQ([self window], [notification object]);
334 observer_->OnFrameChanged();
337 - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
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]];
349 - (void)viewDidMoveToWindow {
351 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
353 selector:@selector(windowDidResignKey:)
354 name:NSWindowDidResignKeyNotification
355 object:[self window]];
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]);
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]);
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())
393 return [super acceptsFirstResponder];
396 // (Overridden from NSResponder)
397 - (BOOL)becomeFirstResponder {
398 BOOL doAccept = [super becomeFirstResponder];
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];
413 // (Overridden from NSResponder)
414 - (BOOL)resignFirstResponder {
415 BOOL doResign = [super resignFirstResponder];
417 [[BrowserWindowController browserWindowControllerForView:self]
418 releaseBarVisibilityForOwner:self withAnimation:YES delay:YES];
423 - (void)drawRect:(NSRect)rect {
424 [super drawRect:rect];
425 autocomplete_text_field::DrawGrayTextAutocompletion(
426 [self attributedStringValue],
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];
471 return VIEW_ID_OMNIBOX;
476 namespace autocomplete_text_field {
478 void DrawGrayTextAutocompletion(NSAttributedString* mainText,
479 NSString* suggestText,
480 NSColor* suggestColor,
483 if (![suggestText length])
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
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,
508 gfx::ScopedNSGraphicsContextSaveGState saveGState;
509 NSRectClip(suggestRect);
510 [cell drawInteriorWithFrame:frame inView:controlView];
513 } // namespace autocomplete_text_field