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 // 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];
205 observer_->OnFrameChanged();
207 [self updateMouseTracking];
210 - (void)setAttributedStringValue:(NSAttributedString*)aString {
211 AutocompleteTextFieldEditor* editor =
212 static_cast<AutocompleteTextFieldEditor*>([self currentEditor]);
215 [super setAttributedStringValue:aString];
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];
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]
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];
253 - (void)clearUndoChain {
254 [undoManager_ removeAllActions];
257 - (NSRange)textView:(NSTextView *)aTextView
258 willChangeSelectionFromCharacterRange:(NSRange)oldRange
259 toCharacterRange:(NSRange)newRange {
261 return observer_->SelectionRangeForProposedRange(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 {
281 - (NSColor*)suggestColor {
282 return suggestColor_;
285 - (NSPoint)bubblePointForDecoration:(LocationBarDecoration*)decoration {
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
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];
325 observer_->OnDidBeginEditing();
329 - (void)textDidEndEditing:(NSNotification *)aNotification {
330 [super textDidEndEditing:aNotification];
332 observer_->OnDidEndEditing();
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]);
341 observer_->ClosePopup();
344 - (void)windowDidResize:(NSNotification*)notification {
345 DCHECK_EQ([self window], [notification object]);
347 observer_->OnFrameChanged();
350 - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
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]];
362 - (void)viewDidMoveToWindow {
364 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
366 selector:@selector(windowDidResignKey:)
367 name:NSWindowDidResignKeyNotification
368 object:[self window]];
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]);
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]);
395 return [super acceptsFirstResponder];
398 // (Overridden from NSResponder)
399 - (BOOL)becomeFirstResponder {
400 BOOL doAccept = [super becomeFirstResponder];
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];
415 // (Overridden from NSResponder)
416 - (BOOL)resignFirstResponder {
417 BOOL doResign = [super resignFirstResponder];
419 [[BrowserWindowController browserWindowControllerForView:self]
420 releaseBarVisibilityForOwner:self withAnimation:YES delay:YES];
425 - (void)drawRect:(NSRect)rect {
426 [super drawRect:rect];
427 autocomplete_text_field::DrawGrayTextAutocompletion(
428 [self attributedStringValue],
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];
473 return VIEW_ID_OMNIBOX;
478 namespace autocomplete_text_field {
480 void DrawGrayTextAutocompletion(NSAttributedString* mainText,
481 NSString* suggestText,
482 NSColor* suggestColor,
485 if (![suggestText length])
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
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,
510 gfx::ScopedNSGraphicsContextSaveGState saveGState;
511 NSRectClip(suggestRect);
512 [cell drawInteriorWithFrame:frame inView:controlView];
515 } // namespace autocomplete_text_field