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/toolbar/toolbar_controller.h" 12#import "chrome/browser/ui/cocoa/url_drop_target.h" 13#import "chrome/browser/ui/cocoa/view_id_util.h" 14 15@implementation AutocompleteTextField 16 17@synthesize observer = observer_; 18 19+ (Class)cellClass { 20 return [AutocompleteTextFieldCell class]; 21} 22 23- (void)dealloc { 24 [[NSNotificationCenter defaultCenter] removeObserver:self]; 25 [super dealloc]; 26} 27 28- (void)awakeFromNib { 29 DCHECK([[self cell] isKindOfClass:[AutocompleteTextFieldCell class]]); 30 [[self cell] setTruncatesLastVisibleLine:YES]; 31 [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail]; 32 currentToolTips_.reset([[NSMutableArray alloc] init]); 33} 34 35- (void)flagsChanged:(NSEvent*)theEvent { 36 if (observer_) { 37 const bool controlFlag = ([theEvent modifierFlags]&NSControlKeyMask) != 0; 38 observer_->OnControlKeyChanged(controlFlag); 39 } 40} 41 42- (AutocompleteTextFieldCell*)cell { 43 NSCell* cell = [super cell]; 44 if (!cell) 45 return nil; 46 47 DCHECK([cell isKindOfClass:[AutocompleteTextFieldCell class]]); 48 return static_cast<AutocompleteTextFieldCell*>(cell); 49} 50 51// Reroute events for the decoration area to the field editor. This 52// will cause the cursor to be moved as close to the edge where the 53// event was seen as possible. 54// 55// The reason for this code's existence is subtle. NSTextField 56// implements text selection and editing in terms of a "field editor". 57// This is an NSTextView which is installed as a subview of the 58// control when the field becomes first responder. When the field 59// editor is installed, it will get -mouseDown: events and handle 60// them, rather than the text field - EXCEPT for the event which 61// caused the change in first responder, or events which fall in the 62// decorations outside the field editor's area. In that case, the 63// default NSTextField code will setup the field editor all over 64// again, which has the side effect of doing "select all" on the text. 65// This effect can be observed with a normal NSTextField if you click 66// in the narrow border area, and is only really a problem because in 67// our case the focus ring surrounds decorations which look clickable. 68// 69// When the user first clicks on the field, after installing the field 70// editor the default NSTextField code detects if the hit is in the 71// field editor area, and if so sets the selection to {0,0} to clear 72// the selection before forwarding the event to the field editor for 73// processing (it will set the cursor position). This also starts the 74// click-drag selection machinery. 75// 76// This code does the same thing for cases where the click was in the 77// decoration area. This allows the user to click-drag starting from 78// a decoration area and get the expected selection behaviour, 79// likewise for multiple clicks in those areas. 80- (void)mouseDown:(NSEvent*)theEvent { 81 // Close the popup before processing the event. This prevents the 82 // popup from being visible while a right-click context menu or 83 // page-action menu is visible. Also, it matches other platforms. 84 if (observer_) 85 observer_->ClosePopup(); 86 87 // If the click was a Control-click, bring up the context menu. 88 // |NSTextField| handles these cases inconsistently if the field is 89 // not already first responder. 90 if (([theEvent modifierFlags] & NSControlKeyMask) != 0) { 91 NSText* editor = [self currentEditor]; 92 NSMenu* menu = [editor menuForEvent:theEvent]; 93 [NSMenu popUpContextMenu:menu withEvent:theEvent forView:editor]; 94 return; 95 } 96 97 const NSPoint location = 98 [self convertPoint:[theEvent locationInWindow] fromView:nil]; 99 const NSRect bounds([self bounds]); 100 101 AutocompleteTextFieldCell* cell = [self cell]; 102 const NSRect textFrame([cell textFrameForFrame:bounds]); 103 104 // A version of the textFrame which extends across the field's 105 // entire width. 106 107 const NSRect fullFrame(NSMakeRect(bounds.origin.x, textFrame.origin.y, 108 bounds.size.width, textFrame.size.height)); 109 110 // If the mouse is in the editing area, or above or below where the 111 // editing area would be if we didn't add decorations, forward to 112 // NSTextField -mouseDown: because it does the right thing. The 113 // above/below test is needed because NSTextView treats mouse events 114 // above/below as select-to-end-in-that-direction, which makes 115 // things janky. 116 BOOL flipped = [self isFlipped]; 117 if (NSMouseInRect(location, textFrame, flipped) || 118 !NSMouseInRect(location, fullFrame, flipped)) { 119 [super mouseDown:theEvent]; 120 121 // After the event has been handled, if the current event is a 122 // mouse up and no selection was created (the mouse didn't move), 123 // select the entire field. 124 // NOTE(shess): This does not interfere with single-clicking to 125 // place caret after a selection is made. An NSTextField only has 126 // a selection when it has a field editor. The field editor is an 127 // NSText subview, which will receive the -mouseDown: in that 128 // case, and this code will never fire. 129 NSText* editor = [self currentEditor]; 130 if (editor) { 131 NSEvent* currentEvent = [NSApp currentEvent]; 132 if ([currentEvent type] == NSLeftMouseUp && 133 ![editor selectedRange].length) { 134 [editor selectAll:nil]; 135 } 136 } 137 138 return; 139 } 140 141 // Give the cell a chance to intercept clicks in page-actions and 142 // other decorative items. 143 if ([cell mouseDown:theEvent inRect:bounds ofView:self]) { 144 return; 145 } 146 147 NSText* editor = [self currentEditor]; 148 149 // We should only be here if we accepted first-responder status and 150 // have a field editor. If one of these fires, it means some 151 // assumptions are being broken. 152 DCHECK(editor != nil); 153 DCHECK([editor isDescendantOf:self]); 154 155 // -becomeFirstResponder does a select-all, which we don't want 156 // because it can lead to a dragged-text situation. Clear the 157 // selection (any valid empty selection will do). 158 [editor setSelectedRange:NSMakeRange(0, 0)]; 159 160 // If the event is to the right of the editing area, scroll the 161 // field editor to the end of the content so that the selection 162 // doesn't initiate from somewhere in the middle of the text. 163 if (location.x > NSMaxX(textFrame)) { 164 [editor scrollRangeToVisible:NSMakeRange([[self stringValue] length], 0)]; 165 } 166 167 [editor mouseDown:theEvent]; 168} 169 170// Overridden to pass OnFrameChanged() notifications to |observer_|. 171// Additionally, cursor and tooltip rects need to be updated. 172- (void)setFrame:(NSRect)frameRect { 173 [super setFrame:frameRect]; 174 if (observer_) { 175 observer_->OnFrameChanged(); 176 } 177 [self updateCursorAndToolTipRects]; 178} 179 180- (void)setAttributedStringValue:(NSAttributedString*)aString { 181 AutocompleteTextFieldEditor* editor = 182 static_cast<AutocompleteTextFieldEditor*>([self currentEditor]); 183 184 if (!editor) { 185 [super setAttributedStringValue:aString]; 186 } else { 187 // The type of the field editor must be AutocompleteTextFieldEditor, 188 // otherwise things won't work. 189 DCHECK([editor isKindOfClass:[AutocompleteTextFieldEditor class]]); 190 191 [editor setAttributedString:aString]; 192 } 193} 194 195- (NSUndoManager*)undoManagerForTextView:(NSTextView*)textView { 196 if (!undoManager_.get()) 197 undoManager_.reset([[NSUndoManager alloc] init]); 198 return undoManager_.get(); 199} 200 201- (void)clearUndoChain { 202 [undoManager_ removeAllActions]; 203} 204 205- (NSRange)textView:(NSTextView *)aTextView 206 willChangeSelectionFromCharacterRange:(NSRange)oldRange 207 toCharacterRange:(NSRange)newRange { 208 if (observer_) 209 return observer_->SelectionRangeForProposedRange(newRange); 210 return newRange; 211} 212 213- (void)addToolTip:(NSString*)tooltip forRect:(NSRect)aRect { 214 [currentToolTips_ addObject:tooltip]; 215 [self addToolTipRect:aRect owner:tooltip userData:nil]; 216} 217 218// TODO(shess): -resetFieldEditorFrameIfNeeded is the place where 219// changes to the cell layout should be flushed. LocationBarViewMac 220// and ToolbarController are calling this routine directly, and I 221// think they are probably wrong. 222// http://crbug.com/40053 223- (void)updateCursorAndToolTipRects { 224 // This will force |resetCursorRects| to be called, as it is not to be called 225 // directly. 226 [[self window] invalidateCursorRectsForView:self]; 227 228 // |removeAllToolTips| only removes those set on the current NSView, not any 229 // subviews. Unless more tooltips are added to this view, this should suffice 230 // in place of managing a set of NSToolTipTag objects. 231 [self removeAllToolTips]; 232 233 // Reload the decoration tooltips. 234 [currentToolTips_ removeAllObjects]; 235 [[self cell] updateToolTipsInRect:[self bounds] ofView:self]; 236} 237 238// NOTE(shess): http://crbug.com/19116 describes a weird bug which 239// happens when the user runs a Print panel on Leopard. After that, 240// spurious -controlTextDidBeginEditing notifications are sent when an 241// NSTextField is firstResponder, even though -currentEditor on that 242// field returns nil. That notification caused significant problems 243// in AutocompleteEditViewMac. -textDidBeginEditing: was NOT being 244// sent in those cases, so this approach doesn't have the problem. 245- (void)textDidBeginEditing:(NSNotification*)aNotification { 246 [super textDidBeginEditing:aNotification]; 247 if (observer_) { 248 observer_->OnDidBeginEditing(); 249 } 250} 251 252- (void)textDidEndEditing:(NSNotification *)aNotification { 253 [super textDidEndEditing:aNotification]; 254 if (observer_) { 255 observer_->OnDidEndEditing(); 256 } 257} 258 259// When the window resigns, make sure the autocomplete popup is no 260// longer visible, since the user's focus is elsewhere. 261- (void)windowDidResignKey:(NSNotification*)notification { 262 DCHECK_EQ([self window], [notification object]); 263 if (observer_) 264 observer_->ClosePopup(); 265} 266 267- (void)viewWillMoveToWindow:(NSWindow*)newWindow { 268 if ([self window]) { 269 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; 270 [nc removeObserver:self 271 name:NSWindowDidResignKeyNotification 272 object:[self window]]; 273 } 274} 275 276- (void)viewDidMoveToWindow { 277 if ([self window]) { 278 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; 279 [nc addObserver:self 280 selector:@selector(windowDidResignKey:) 281 name:NSWindowDidResignKeyNotification 282 object:[self window]]; 283 // Only register for drops if not in a popup window. Lazily create the 284 // drop handler when the type of window is known. 285 BrowserWindowController* windowController = 286 [BrowserWindowController browserWindowControllerForView:self]; 287 if ([windowController isNormalWindow]) 288 dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]); 289 } 290} 291 292// NSTextField becomes first responder by installing a "field editor" 293// subview. Clicks outside the field editor (such as a decoration) 294// will attempt to make the field the first-responder again, which 295// causes a select-all, even if the decoration handles the click. If 296// the field editor is already in place, don't accept first responder 297// again. This allows the selection to be unmodified if the click is 298// handled by a decoration or context menu (|-mouseDown:| will still 299// change it if appropriate). 300- (BOOL)acceptsFirstResponder { 301 if ([self currentEditor]) { 302 DCHECK_EQ([self currentEditor], [[self window] firstResponder]); 303 return NO; 304 } 305 return [super acceptsFirstResponder]; 306} 307 308// (Overridden from NSResponder) 309- (BOOL)becomeFirstResponder { 310 BOOL doAccept = [super becomeFirstResponder]; 311 if (doAccept) { 312 [[BrowserWindowController browserWindowControllerForView:self] 313 lockBarVisibilityForOwner:self withAnimation:YES delay:NO]; 314 315 // Tells the observer that we get the focus. 316 // But we can't call observer_->OnKillFocus() in resignFirstResponder:, 317 // because the first responder will be immediately set to the field editor 318 // when calling [super becomeFirstResponder], thus we won't receive 319 // resignFirstResponder: anymore when losing focus. 320 if (observer_) { 321 NSEvent* theEvent = [NSApp currentEvent]; 322 const bool controlDown = ([theEvent modifierFlags]&NSControlKeyMask) != 0; 323 observer_->OnSetFocus(controlDown); 324 } 325 } 326 return doAccept; 327} 328 329// (Overridden from NSResponder) 330- (BOOL)resignFirstResponder { 331 BOOL doResign = [super resignFirstResponder]; 332 if (doResign) { 333 [[BrowserWindowController browserWindowControllerForView:self] 334 releaseBarVisibilityForOwner:self withAnimation:YES delay:YES]; 335 } 336 return doResign; 337} 338 339// (URLDropTarget protocol) 340- (id<URLDropTargetController>)urlDropController { 341 BrowserWindowController* windowController = 342 [BrowserWindowController browserWindowControllerForView:self]; 343 return [windowController toolbarController]; 344} 345 346// (URLDropTarget protocol) 347- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender { 348 // Make ourself the first responder, which will select the text to indicate 349 // that our contents would be replaced by a drop. 350 // TODO(viettrungluu): crbug.com/30809 -- this is a hack since it steals focus 351 // and doesn't return it. 352 [[self window] makeFirstResponder:self]; 353 return [dropHandler_ draggingEntered:sender]; 354} 355 356// (URLDropTarget protocol) 357- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender { 358 return [dropHandler_ draggingUpdated:sender]; 359} 360 361// (URLDropTarget protocol) 362- (void)draggingExited:(id<NSDraggingInfo>)sender { 363 return [dropHandler_ draggingExited:sender]; 364} 365 366// (URLDropTarget protocol) 367- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender { 368 return [dropHandler_ performDragOperation:sender]; 369} 370 371- (NSMenu*)decorationMenuForEvent:(NSEvent*)event { 372 AutocompleteTextFieldCell* cell = [self cell]; 373 return [cell decorationMenuForEvent:event inRect:[self bounds] ofView:self]; 374} 375 376- (ViewID)viewID { 377 return VIEW_ID_LOCATION_BAR; 378} 379 380@end 381