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_editor.h" 6 7 #include "base/strings/string_util.h" 8 #include "base/strings/sys_string_conversions.h" 9 #include "chrome/app/chrome_command_ids.h" // IDC_* 10 #include "chrome/browser/ui/browser_list.h" 11 #import "chrome/browser/ui/cocoa/browser_window_controller.h" 12 #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h" 13 #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h" 14 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h" 15 #include "grit/generated_resources.h" 16 #import "ui/base/cocoa/find_pasteboard.h" 17 #include "ui/base/l10n/l10n_util_mac.h" 18 19 namespace { 20 21 // When too much data is put into a single-line text field, things get 22 // janky due to the cost of computing the blink rect. Sometimes users 23 // accidentally paste large amounts, so place a limit on what will be 24 // accepted. 25 // 26 // 10k characters was arbitrarily chosen by seeing how much a text 27 // field could handle in a single line before it started getting too 28 // janky to recover from (jankiness was detectable around 5k). 29 // www.google.com returns an error for searches around 2k characters, 30 // so this is conservative. 31 const NSUInteger kMaxPasteLength = 10000; 32 33 // Returns |YES| if too much text would be pasted. 34 BOOL ThePasteboardIsTooDamnBig() { 35 NSPasteboard* pb = [NSPasteboard generalPasteboard]; 36 NSString* type = 37 [pb availableTypeFromArray:[NSArray arrayWithObject:NSStringPboardType]]; 38 if (!type) 39 return NO; 40 41 return [[pb stringForType:type] length] > kMaxPasteLength; 42 } 43 44 } // namespace 45 46 @implementation AutocompleteTextFieldEditor 47 48 - (BOOL)shouldDrawInsertionPoint { 49 return [super shouldDrawInsertionPoint] && 50 ![[[self delegate] cell] hideFocusState]; 51 } 52 53 - (id)initWithFrame:(NSRect)frameRect { 54 if ((self = [super initWithFrame:frameRect])) { 55 dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]); 56 57 forbiddenCharacters_.reset([[NSCharacterSet controlCharacterSet] retain]); 58 59 // These checks seem inappropriate to the omnibox, and also 60 // unlikely to work reliably due to our autocomplete interfering. 61 // 62 // Also see <http://crbug.com/173405>. 63 NSTextCheckingTypes checkingTypes = [self enabledTextCheckingTypes]; 64 checkingTypes &= ~NSTextCheckingTypeReplacement; 65 checkingTypes &= ~NSTextCheckingTypeCorrection; 66 [self setEnabledTextCheckingTypes:checkingTypes]; 67 } 68 return self; 69 } 70 71 // If the entire field is selected, drag the same data as would be 72 // dragged from the field's location icon. In some cases the textual 73 // contents will not contain relevant data (for instance, "http://" is 74 // stripped from URLs). 75 - (BOOL)dragSelectionWithEvent:(NSEvent *)event 76 offset:(NSSize)mouseOffset 77 slideBack:(BOOL)slideBack { 78 AutocompleteTextFieldObserver* observer = [self observer]; 79 DCHECK(observer); 80 if (observer && observer->CanCopy()) { 81 NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard]; 82 observer->CopyToPasteboard(pboard); 83 84 NSPoint p; 85 NSImage* image = [self dragImageForSelectionWithEvent:event origin:&p]; 86 87 [self dragImage:image 88 at:p 89 offset:mouseOffset 90 event:event 91 pasteboard:pboard 92 source:self 93 slideBack:slideBack]; 94 return YES; 95 } 96 return [super dragSelectionWithEvent:event 97 offset:mouseOffset 98 slideBack:slideBack]; 99 } 100 101 - (void)copy:(id)sender { 102 AutocompleteTextFieldObserver* observer = [self observer]; 103 DCHECK(observer); 104 if (observer && observer->CanCopy()) 105 observer->CopyToPasteboard([NSPasteboard generalPasteboard]); 106 } 107 108 - (void)cut:(id)sender { 109 [self copy:sender]; 110 [self delete:nil]; 111 } 112 113 - (void)showURL:(id)sender { 114 AutocompleteTextFieldObserver* observer = [self observer]; 115 DCHECK(observer); 116 observer->ShowURL(); 117 } 118 119 // This class assumes that the delegate is an AutocompleteTextField. 120 // Enforce that assumption. 121 - (AutocompleteTextField*)delegate { 122 AutocompleteTextField* delegate = 123 static_cast<AutocompleteTextField*>([super delegate]); 124 DCHECK(delegate == nil || 125 [delegate isKindOfClass:[AutocompleteTextField class]]); 126 return delegate; 127 } 128 129 - (void)setDelegate:(AutocompleteTextField*)delegate { 130 DCHECK(delegate == nil || 131 [delegate isKindOfClass:[AutocompleteTextField class]]); 132 133 // Unregister from any previously registered undo and redo notifications. 134 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; 135 [nc removeObserver:self 136 name:NSUndoManagerDidUndoChangeNotification 137 object:nil]; 138 [nc removeObserver:self 139 name:NSUndoManagerDidRedoChangeNotification 140 object:nil]; 141 142 // Set the delegate. 143 [super setDelegate:delegate]; 144 145 // Register for undo and redo notifications from the new |delegate|, if it is 146 // non-nil. 147 if ([self delegate]) { 148 NSUndoManager* undo_manager = [self undoManager]; 149 [nc addObserver:self 150 selector:@selector(didUndoOrRedo:) 151 name:NSUndoManagerDidUndoChangeNotification 152 object:undo_manager]; 153 [nc addObserver:self 154 selector:@selector(didUndoOrRedo:) 155 name:NSUndoManagerDidRedoChangeNotification 156 object:undo_manager]; 157 } 158 } 159 160 - (void)didUndoOrRedo:(NSNotification *)aNotification { 161 AutocompleteTextFieldObserver* observer = [self observer]; 162 if (observer) 163 observer->OnDidChange(); 164 } 165 166 // Convenience method for retrieving the observer from the delegate. 167 - (AutocompleteTextFieldObserver*)observer { 168 return [[self delegate] observer]; 169 } 170 171 - (void)paste:(id)sender { 172 if (ThePasteboardIsTooDamnBig()) { 173 NSBeep(); 174 return; 175 } 176 177 AutocompleteTextFieldObserver* observer = [self observer]; 178 DCHECK(observer); 179 if (observer) { 180 observer->OnPaste(); 181 } 182 } 183 184 - (void)pasteAndMatchStyle:(id)sender { 185 [self paste:sender]; 186 } 187 188 - (void)pasteAndGo:sender { 189 if (ThePasteboardIsTooDamnBig()) { 190 NSBeep(); 191 return; 192 } 193 194 AutocompleteTextFieldObserver* observer = [self observer]; 195 DCHECK(observer); 196 if (observer) { 197 observer->OnPasteAndGo(); 198 } 199 } 200 201 // We have rich text, but it shouldn't be modified by the user, so 202 // don't update the font panel. In theory, -setUsesFontPanel: should 203 // accomplish this, but that gets called frequently with YES when 204 // NSTextField and NSTextView synchronize their contents. That is 205 // probably unavoidable because in most cases having rich text in the 206 // field you probably would expect it to update the font panel. 207 - (void)updateFontPanel {} 208 209 // No ruler bar, so don't update any of that state, either. 210 - (void)updateRuler {} 211 212 - (NSMenu*)menuForEvent:(NSEvent*)event { 213 // Give the control a chance to provide page-action menus. 214 // NOTE: Note that page actions aren't even in the editor's 215 // boundaries! The Cocoa control implementation seems to do a 216 // blanket forward to here if nothing more specific is returned from 217 // the control and cell calls. 218 // TODO(shess): Determine if the page-action part of this can be 219 // moved to the cell. 220 NSMenu* actionMenu = [[self delegate] decorationMenuForEvent:event]; 221 if (actionMenu) 222 return actionMenu; 223 224 NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"TITLE"] autorelease]; 225 [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CUT) 226 action:@selector(cut:) 227 keyEquivalent:@""]; 228 [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_COPY) 229 action:@selector(copy:) 230 keyEquivalent:@""]; 231 232 [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_PASTE) 233 action:@selector(paste:) 234 keyEquivalent:@""]; 235 236 // TODO(shess): If the control is not editable, should we show a 237 // greyed-out "Paste and Go"? 238 if ([self isEditable]) { 239 // Paste and go/search. 240 AutocompleteTextFieldObserver* observer = [self observer]; 241 DCHECK(observer); 242 if (!ThePasteboardIsTooDamnBig()) { 243 NSString* pasteAndGoLabel = 244 l10n_util::GetNSStringWithFixup(observer->GetPasteActionStringId()); 245 DCHECK([pasteAndGoLabel length]); 246 [menu addItemWithTitle:pasteAndGoLabel 247 action:@selector(pasteAndGo:) 248 keyEquivalent:@""]; 249 } 250 251 [menu addItem:[NSMenuItem separatorItem]]; 252 253 // Display a "Show URL" option if search term replacement is active. 254 if (observer->ShouldEnableShowURL()) { 255 NSString* showURLLabel = 256 l10n_util::GetNSStringWithFixup(IDS_SHOW_URL_MAC); 257 DCHECK([showURLLabel length]); 258 [menu addItemWithTitle:showURLLabel 259 action:@selector(showURL:) 260 keyEquivalent:@""]; 261 } 262 263 NSString* searchEngineLabel = 264 l10n_util::GetNSStringWithFixup(IDS_EDIT_SEARCH_ENGINES); 265 DCHECK([searchEngineLabel length]); 266 NSMenuItem* item = [menu addItemWithTitle:searchEngineLabel 267 action:@selector(commandDispatch:) 268 keyEquivalent:@""]; 269 [item setTag:IDC_EDIT_SEARCH_ENGINES]; 270 } 271 272 return menu; 273 } 274 275 // (Overridden from NSResponder) 276 - (BOOL)becomeFirstResponder { 277 BOOL doAccept = [super becomeFirstResponder]; 278 AutocompleteTextField* field = [self delegate]; 279 // Only lock visibility if we've been set up with a delegate (the text field). 280 if (doAccept && field) { 281 // Give the text field ownership of the visibility lock. (The first 282 // responder dance between the field and the field editor is a little 283 // weird.) 284 [[BrowserWindowController browserWindowControllerForView:field] 285 lockBarVisibilityForOwner:field withAnimation:YES delay:NO]; 286 } 287 return doAccept; 288 } 289 290 // (Overridden from NSResponder) 291 - (BOOL)resignFirstResponder { 292 BOOL doResign = [super resignFirstResponder]; 293 AutocompleteTextField* field = [self delegate]; 294 // Only lock visibility if we've been set up with a delegate (the text field). 295 if (doResign && field) { 296 // Give the text field ownership of the visibility lock. 297 [[BrowserWindowController browserWindowControllerForView:field] 298 releaseBarVisibilityForOwner:field withAnimation:YES delay:YES]; 299 300 AutocompleteTextFieldObserver* observer = [self observer]; 301 if (observer) 302 observer->OnKillFocus(); 303 } 304 return doResign; 305 } 306 307 - (void)mouseDown:(NSEvent*)event { 308 AutocompleteTextFieldObserver* observer = [self observer]; 309 if (observer) 310 observer->OnMouseDown([event buttonNumber]); 311 [super mouseDown:event]; 312 } 313 314 - (void)rightMouseDown:(NSEvent *)event { 315 AutocompleteTextFieldObserver* observer = [self observer]; 316 if (observer) 317 observer->OnMouseDown([event buttonNumber]); 318 [super rightMouseDown:event]; 319 } 320 321 - (void)otherMouseDown:(NSEvent *)event { 322 AutocompleteTextFieldObserver* observer = [self observer]; 323 if (observer) 324 observer->OnMouseDown([event buttonNumber]); 325 [super otherMouseDown:event]; 326 } 327 328 // (URLDropTarget protocol) 329 - (id<URLDropTargetController>)urlDropController { 330 BrowserWindowController* windowController = 331 [BrowserWindowController browserWindowControllerForView:self]; 332 return [windowController toolbarController]; 333 } 334 335 // (URLDropTarget protocol) 336 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender { 337 // Make ourself the first responder (even though we're presumably already the 338 // first responder), which will select the text to indicate that our contents 339 // would be replaced by a drop. 340 [[self window] makeFirstResponder:self]; 341 return [dropHandler_ draggingEntered:sender]; 342 } 343 344 // (URLDropTarget protocol) 345 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender { 346 return [dropHandler_ draggingUpdated:sender]; 347 } 348 349 // (URLDropTarget protocol) 350 - (void)draggingExited:(id<NSDraggingInfo>)sender { 351 return [dropHandler_ draggingExited:sender]; 352 } 353 354 // (URLDropTarget protocol) 355 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender { 356 return [dropHandler_ performDragOperation:sender]; 357 } 358 359 // Prevent control characters from being entered into the Omnibox. 360 // This is invoked for keyboard entry, not for pasting. 361 - (void)insertText:(id)aString { 362 // Repeatedly remove control characters. The loop will only ever 363 // execute at all when the user enters control characters (using 364 // Ctrl-Alt- or Ctrl-Q). Making this generally efficient would 365 // probably be a loss, since the input always seems to be a single 366 // character. 367 if ([aString isKindOfClass:[NSAttributedString class]]) { 368 NSRange range = 369 [[aString string] rangeOfCharacterFromSet:forbiddenCharacters_]; 370 while (range.location != NSNotFound) { 371 aString = [[aString mutableCopy] autorelease]; 372 [aString deleteCharactersInRange:range]; 373 range = [[aString string] rangeOfCharacterFromSet:forbiddenCharacters_]; 374 } 375 DCHECK_EQ(range.length, 0U); 376 } else { 377 NSRange range = [aString rangeOfCharacterFromSet:forbiddenCharacters_]; 378 while (range.location != NSNotFound) { 379 aString = 380 [aString stringByReplacingCharactersInRange:range withString:@""]; 381 range = [aString rangeOfCharacterFromSet:forbiddenCharacters_]; 382 } 383 DCHECK_EQ(range.length, 0U); 384 } 385 386 // NOTE: If |aString| is empty, this intentionally replaces the 387 // selection with empty. This seems consistent with the case where 388 // the input contained a mixture of characters and the string ended 389 // up not empty. 390 [super insertText:aString]; 391 } 392 393 - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange { 394 [super setMarkedText:aString selectedRange:selRange]; 395 396 // Because the OmniboxViewMac class treats marked text as content, 397 // we need to treat the change to marked text as content change as well. 398 [self didChangeText]; 399 } 400 401 - (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange 402 granularity:(NSSelectionGranularity)granularity { 403 AutocompleteTextFieldObserver* observer = [self observer]; 404 NSRange modifiedRange = [super selectionRangeForProposedRange:proposedSelRange 405 granularity:granularity]; 406 if (observer) 407 return observer->SelectionRangeForProposedRange(modifiedRange); 408 return modifiedRange; 409 } 410 411 412 413 414 - (void)setSelectedRange:(NSRange)charRange 415 affinity:(NSSelectionAffinity)affinity 416 stillSelecting:(BOOL)flag { 417 [super setSelectedRange:charRange affinity:affinity stillSelecting:flag]; 418 419 // We're only interested in selection changes directly caused by keyboard 420 // input from the user. 421 if (interpretingKeyEvents_) 422 textChangedByKeyEvents_ = YES; 423 } 424 425 - (void)interpretKeyEvents:(NSArray *)eventArray { 426 DCHECK(!interpretingKeyEvents_); 427 interpretingKeyEvents_ = YES; 428 textChangedByKeyEvents_ = NO; 429 AutocompleteTextFieldObserver* observer = [self observer]; 430 431 if (observer) 432 observer->OnBeforeChange(); 433 434 [super interpretKeyEvents:eventArray]; 435 436 if (textChangedByKeyEvents_ && observer) 437 observer->OnDidChange(); 438 439 DCHECK(interpretingKeyEvents_); 440 interpretingKeyEvents_ = NO; 441 } 442 443 - (BOOL)shouldChangeTextInRange:(NSRange)affectedCharRange 444 replacementString:(NSString *)replacementString { 445 BOOL ret = [super shouldChangeTextInRange:affectedCharRange 446 replacementString:replacementString]; 447 448 if (ret && !interpretingKeyEvents_) { 449 AutocompleteTextFieldObserver* observer = [self observer]; 450 if (observer) 451 observer->OnBeforeChange(); 452 } 453 return ret; 454 } 455 456 - (void)didChangeText { 457 [super didChangeText]; 458 459 AutocompleteTextFieldObserver* observer = [self observer]; 460 if (observer) { 461 if (!interpretingKeyEvents_ && 462 ![[self undoManager] isUndoing] && ![[self undoManager] isRedoing]) { 463 observer->OnDidChange(); 464 } else if (interpretingKeyEvents_) { 465 textChangedByKeyEvents_ = YES; 466 } 467 } 468 } 469 470 - (void)doCommandBySelector:(SEL)cmd { 471 // TODO(shess): Review code for cases where we're fruitlessly attempting to 472 // work in spite of not having an observer. 473 AutocompleteTextFieldObserver* observer = [self observer]; 474 475 if (observer && observer->OnDoCommandBySelector(cmd)) { 476 // The observer should already be aware of any changes to the text, so 477 // setting |textChangedByKeyEvents_| to NO to prevent its OnDidChange() 478 // method from being called unnecessarily. 479 textChangedByKeyEvents_ = NO; 480 return; 481 } 482 483 // If the escape key was pressed and no revert happened and we're in 484 // fullscreen mode, give focus to the web contents, which may dismiss the 485 // overlay. 486 if (cmd == @selector(cancelOperation:)) { 487 BrowserWindowController* windowController = 488 [BrowserWindowController browserWindowControllerForView:self]; 489 if ([windowController isFullscreen]) { 490 [windowController focusTabContents]; 491 textChangedByKeyEvents_ = NO; 492 return; 493 } 494 } 495 496 [super doCommandBySelector:cmd]; 497 } 498 499 - (void)setAttributedString:(NSAttributedString*)aString { 500 NSTextStorage* textStorage = [self textStorage]; 501 DCHECK(textStorage); 502 [textStorage setAttributedString:aString]; 503 504 // The text has been changed programmatically. The observer should know 505 // this change, so setting |textChangedByKeyEvents_| to NO to 506 // prevent its OnDidChange() method from being called unnecessarily. 507 textChangedByKeyEvents_ = NO; 508 } 509 510 - (BOOL)validateMenuItem:(NSMenuItem*)item { 511 if ([item action] == @selector(copyToFindPboard:)) 512 return [self selectedRange].length > 0; 513 if ([item action] == @selector(pasteAndGo:)) { 514 // TODO(rohitrao): If the clipboard is empty, should we show a 515 // greyed-out "Paste and Go" or nothing at all? 516 AutocompleteTextFieldObserver* observer = [self observer]; 517 DCHECK(observer); 518 return observer->CanPasteAndGo(); 519 } 520 if ([item action] == @selector(showURL:)) { 521 AutocompleteTextFieldObserver* observer = [self observer]; 522 DCHECK(observer); 523 return observer->ShouldEnableShowURL(); 524 } 525 return [super validateMenuItem:item]; 526 } 527 528 - (void)copyToFindPboard:(id)sender { 529 NSRange selectedRange = [self selectedRange]; 530 if (selectedRange.length == 0) 531 return; 532 NSAttributedString* selection = 533 [self attributedSubstringForProposedRange:selectedRange 534 actualRange:NULL]; 535 if (!selection) 536 return; 537 538 [[FindPasteboard sharedInstance] setFindText:[selection string]]; 539 } 540 541 - (void)drawRect:(NSRect)rect { 542 [super drawRect:rect]; 543 autocomplete_text_field::DrawGrayTextAutocompletion( 544 [self textStorage], 545 [[self delegate] suggestText], 546 [[self delegate] suggestColor], 547 self, 548 [self bounds]); 549 } 550 551 @end 552