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. 4 5#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h" 6 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" 16 17@implementation AutocompleteTextField 18 19@synthesize observer = observer_; 20 21+ (Class)cellClass { 22 return [AutocompleteTextFieldCell class]; 23} 24 25- (void)dealloc { 26 [[NSNotificationCenter defaultCenter] removeObserver:self]; 27 [super dealloc]; 28} 29 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]); 35} 36 37- (void)flagsChanged:(NSEvent*)theEvent { 38 if (observer_) { 39 const bool controlFlag = ([theEvent modifierFlags]&NSControlKeyMask) != 0; 40 observer_->OnControlKeyChanged(controlFlag); 41 } 42} 43 44- (AutocompleteTextFieldCell*)cell { 45 NSCell* cell = [super cell]; 46 if (!cell) 47 return nil; 48 49 DCHECK([cell isKindOfClass:[AutocompleteTextFieldCell class]]); 50 return static_cast<AutocompleteTextFieldCell*>(cell); 51} 52 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. 56// 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. 70// 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. 77// 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]); 87 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 } 97 98 const NSPoint location = 99 [self convertPoint:[theEvent locationInWindow] fromView:nil]; 100 const NSRect bounds([self bounds]); 101 102 AutocompleteTextFieldCell* cell = [self cell]; 103 const NSRect textFrame([cell textFrameForFrame:bounds]); 104 105 // A version of the textFrame which extends across the field's 106 // entire width. 107 108 const NSRect fullFrame(NSMakeRect(bounds.origin.x, textFrame.origin.y, 109 bounds.size.width, textFrame.size.height)); 110 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]; 121 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 } 139 140 return; 141 } 142 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 } 148 149 NSText* editor = [self currentEditor]; 150 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]); 156 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)]; 161 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 } 168 169 [editor mouseDown:theEvent]; 170} 171 172- (void)rightMouseDown:(NSEvent*)event { 173 if (observer_) 174 observer_->OnMouseDown([event buttonNumber]); 175 [super rightMouseDown:event]; 176} 177 178- (void)otherMouseDown:(NSEvent *)event { 179 if (observer_) 180 observer_->OnMouseDown([event buttonNumber]); 181 [super otherMouseDown:event]; 182} 183 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]; 187} 188 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]; 192} 193 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]; 201} 202 203- (void)setAttributedStringValue:(NSAttributedString*)aString { 204 AutocompleteTextFieldEditor* editor = 205 static_cast<AutocompleteTextFieldEditor*>([self currentEditor]); 206 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]]); 213 214 [editor setAttributedString:aString]; 215 } 216} 217 218- (NSUndoManager*)undoManagerForTextView:(NSTextView*)textView { 219 if (!undoManager_.get()) 220 undoManager_.reset([[NSUndoManager alloc] init]); 221 return undoManager_.get(); 222} 223 224- (void)clearUndoChain { 225 [undoManager_ removeAllActions]; 226} 227 228- (NSRange)textView:(NSTextView *)aTextView 229 willChangeSelectionFromCharacterRange:(NSRange)oldRange 230 toCharacterRange:(NSRange)newRange { 231 if (observer_) 232 return observer_->SelectionRangeForProposedRange(newRange); 233 return newRange; 234} 235 236- (void)addToolTip:(NSString*)tooltip forRect:(NSRect)aRect { 237 [currentToolTips_ addObject:tooltip]; 238 [self addToolTipRect:aRect owner:tooltip userData:nil]; 239} 240 241- (void)setGrayTextAutocompletion:(NSString*)suggestText 242 textColor:(NSColor*)suggestColor { 243 [self setNeedsDisplay:YES]; 244 suggestText_.reset([suggestText retain]); 245 suggestColor_.reset([suggestColor retain]); 246} 247 248- (NSString*)suggestText { 249 return suggestText_; 250} 251 252- (NSColor*)suggestColor { 253 return suggestColor_; 254} 255 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]; 261} 262 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]; 272 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]; 277 278 // Reload the decoration tooltips. 279 [currentToolTips_ removeAllObjects]; 280 [[self cell] updateToolTipsInRect:[self bounds] ofView:self]; 281 282 // Setup/update the tracking areas for the decorations. 283 [[self cell] setUpTrackingAreasInRect:[self bounds] ofView:self]; 284} 285 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 } 298} 299 300- (void)textDidEndEditing:(NSNotification *)aNotification { 301 [super textDidEndEditing:aNotification]; 302 if (observer_) { 303 observer_->OnDidEndEditing(); 304 } 305} 306 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(); 313} 314 315- (void)windowDidResize:(NSNotification*)notification { 316 DCHECK_EQ([self window], [notification object]); 317 if (observer_) 318 observer_->OnFrameChanged(); 319} 320 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 } 331} 332 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 } 351} 352 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]; 367} 368 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]; 375 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; 384} 385 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; 394} 395 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]]); 404} 405 406// (URLDropTarget protocol) 407- (id<URLDropTargetController>)urlDropController { 408 BrowserWindowController* windowController = 409 [BrowserWindowController browserWindowControllerForView:self]; 410 return [windowController toolbarController]; 411} 412 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]; 421} 422 423// (URLDropTarget protocol) 424- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender { 425 return [dropHandler_ draggingUpdated:sender]; 426} 427 428// (URLDropTarget protocol) 429- (void)draggingExited:(id<NSDraggingInfo>)sender { 430 return [dropHandler_ draggingExited:sender]; 431} 432 433// (URLDropTarget protocol) 434- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender { 435 return [dropHandler_ performDragOperation:sender]; 436} 437 438- (NSMenu*)decorationMenuForEvent:(NSEvent*)event { 439 AutocompleteTextFieldCell* cell = [self cell]; 440 return [cell decorationMenuForEvent:event inRect:[self bounds] ofView:self]; 441} 442 443- (ViewID)viewID { 444 return VIEW_ID_OMNIBOX; 445} 446 447@end 448 449namespace autocomplete_text_field { 450 451void DrawGrayTextAutocompletion(NSAttributedString* mainText, 452 NSString* suggestText, 453 NSColor* suggestColor, 454 NSView* controlView, 455 NSRect frame) { 456 if (![suggestText length]) 457 return; 458 459 base::scoped_nsobject<NSTextFieldCell> cell( 460 [[NSTextFieldCell alloc] initTextCell:@""]); 461 [cell setBordered:NO]; 462 [cell setDrawsBackground:NO]; 463 [cell setEditable:NO]; 464 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]; 473 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)); 480 481 gfx::ScopedNSGraphicsContextSaveGState saveGState; 482 NSRectClip(suggestRect); 483 [cell drawInteriorWithFrame:frame inView:controlView]; 484} 485 486} // namespace autocomplete_text_field 487