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/string_util.h" 8 #include "base/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 #include "ui/base/l10n/l10n_util_mac.h" 17 18 namespace { 19 20 // When too much data is put into a single-line text field, things get 21 // janky due to the cost of computing the blink rect. Sometimes users 22 // accidentally paste large amounts, so place a limit on what will be 23 // accepted. 24 // 25 // 10k characters was arbitrarily chosen by seeing how much a text 26 // field could handle in a single line before it started getting too 27 // janky to recover from (jankiness was detectable around 5k). 28 // www.google.com returns an error for searches around 2k characters, 29 // so this is conservative. 30 const NSUInteger kMaxPasteLength = 10000; 31 32 // Returns |YES| if too much text would be pasted. 33 BOOL ThePasteboardIsTooDamnBig() { 34 NSPasteboard* pb = [NSPasteboard generalPasteboard]; 35 NSString* type = 36 [pb availableTypeFromArray:[NSArray arrayWithObject:NSStringPboardType]]; 37 if (!type) 38 return NO; 39 40 return [[pb stringForType:type] length] > kMaxPasteLength; 41 } 42 43 } // namespace 44 45 @implementation AutocompleteTextFieldEditor 46 47 - (id)initWithFrame:(NSRect)frameRect { 48 if ((self = [super initWithFrame:frameRect])) { 49 dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]); 50 51 forbiddenCharacters_.reset([[NSCharacterSet controlCharacterSet] retain]); 52 } 53 return self; 54 } 55 56 // If the entire field is selected, drag the same data as would be 57 // dragged from the field's location icon. In some cases the textual 58 // contents will not contain relevant data (for instance, "http://" is 59 // stripped from URLs). 60 - (BOOL)dragSelectionWithEvent:(NSEvent *)event 61 offset:(NSSize)mouseOffset 62 slideBack:(BOOL)slideBack { 63 AutocompleteTextFieldObserver* observer = [self observer]; 64 DCHECK(observer); 65 if (observer && observer->CanCopy()) { 66 NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard]; 67 observer->CopyToPasteboard(pboard); 68 69 NSPoint p; 70 NSImage* image = [self dragImageForSelectionWithEvent:event origin:&p]; 71 72 [self dragImage:image 73 at:p 74 offset:mouseOffset 75 event:event 76 pasteboard:pboard 77 source:self 78 slideBack:slideBack]; 79 return YES; 80 } 81 return [super dragSelectionWithEvent:event 82 offset:mouseOffset 83 slideBack:slideBack]; 84 } 85 86 - (void)copy:(id)sender { 87 AutocompleteTextFieldObserver* observer = [self observer]; 88 DCHECK(observer); 89 if (observer && observer->CanCopy()) 90 observer->CopyToPasteboard([NSPasteboard generalPasteboard]); 91 } 92 93 - (void)cut:(id)sender { 94 [self copy:sender]; 95 [self delete:nil]; 96 } 97 98 // This class assumes that the delegate is an AutocompleteTextField. 99 // Enforce that assumption. 100 - (AutocompleteTextField*)delegate { 101 AutocompleteTextField* delegate = 102 static_cast<AutocompleteTextField*>([super delegate]); 103 DCHECK(delegate == nil || 104 [delegate isKindOfClass:[AutocompleteTextField class]]); 105 return delegate; 106 } 107 108 - (void)setDelegate:(AutocompleteTextField*)delegate { 109 DCHECK(delegate == nil || 110 [delegate isKindOfClass:[AutocompleteTextField class]]); 111 [super setDelegate:delegate]; 112 } 113 114 // Convenience method for retrieving the observer from the delegate. 115 - (AutocompleteTextFieldObserver*)observer { 116 return [[self delegate] observer]; 117 } 118 119 - (void)paste:(id)sender { 120 if (ThePasteboardIsTooDamnBig()) { 121 NSBeep(); 122 return; 123 } 124 125 AutocompleteTextFieldObserver* observer = [self observer]; 126 DCHECK(observer); 127 if (observer) { 128 observer->OnPaste(); 129 } 130 } 131 132 - (void)pasteAndGo:sender { 133 if (ThePasteboardIsTooDamnBig()) { 134 NSBeep(); 135 return; 136 } 137 138 AutocompleteTextFieldObserver* observer = [self observer]; 139 DCHECK(observer); 140 if (observer) { 141 observer->OnPasteAndGo(); 142 } 143 } 144 145 // We have rich text, but it shouldn't be modified by the user, so 146 // don't update the font panel. In theory, -setUsesFontPanel: should 147 // accomplish this, but that gets called frequently with YES when 148 // NSTextField and NSTextView synchronize their contents. That is 149 // probably unavoidable because in most cases having rich text in the 150 // field you probably would expect it to update the font panel. 151 - (void)updateFontPanel {} 152 153 // No ruler bar, so don't update any of that state, either. 154 - (void)updateRuler {} 155 156 - (NSMenu*)menuForEvent:(NSEvent*)event { 157 // Give the control a chance to provide page-action menus. 158 // NOTE: Note that page actions aren't even in the editor's 159 // boundaries! The Cocoa control implementation seems to do a 160 // blanket forward to here if nothing more specific is returned from 161 // the control and cell calls. 162 // TODO(shess): Determine if the page-action part of this can be 163 // moved to the cell. 164 NSMenu* actionMenu = [[self delegate] decorationMenuForEvent:event]; 165 if (actionMenu) 166 return actionMenu; 167 168 NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"TITLE"] autorelease]; 169 [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CUT) 170 action:@selector(cut:) 171 keyEquivalent:@""]; 172 [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_COPY) 173 action:@selector(copy:) 174 keyEquivalent:@""]; 175 [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_PASTE) 176 action:@selector(paste:) 177 keyEquivalent:@""]; 178 179 // TODO(shess): If the control is not editable, should we show a 180 // greyed-out "Paste and Go"? 181 if ([self isEditable]) { 182 // Paste and go/search. 183 AutocompleteTextFieldObserver* observer = [self observer]; 184 DCHECK(observer); 185 if (observer && observer->CanPasteAndGo()) { 186 const int string_id = observer->GetPasteActionStringId(); 187 NSString* label = l10n_util::GetNSStringWithFixup(string_id); 188 189 // TODO(rohitrao): If the clipboard is empty, should we show a 190 // greyed-out "Paste and Go" or nothing at all? 191 if ([label length]) { 192 [menu addItemWithTitle:label 193 action:@selector(pasteAndGo:) 194 keyEquivalent:@""]; 195 } 196 } 197 198 NSString* label = l10n_util::GetNSStringWithFixup(IDS_EDIT_SEARCH_ENGINES); 199 DCHECK([label length]); 200 if ([label length]) { 201 [menu addItem:[NSMenuItem separatorItem]]; 202 NSMenuItem* item = [menu addItemWithTitle:label 203 action:@selector(commandDispatch:) 204 keyEquivalent:@""]; 205 [item setTag:IDC_EDIT_SEARCH_ENGINES]; 206 } 207 } 208 209 return menu; 210 } 211 212 // (Overridden from NSResponder) 213 - (BOOL)becomeFirstResponder { 214 BOOL doAccept = [super becomeFirstResponder]; 215 AutocompleteTextField* field = [self delegate]; 216 // Only lock visibility if we've been set up with a delegate (the text field). 217 if (doAccept && field) { 218 // Give the text field ownership of the visibility lock. (The first 219 // responder dance between the field and the field editor is a little 220 // weird.) 221 [[BrowserWindowController browserWindowControllerForView:field] 222 lockBarVisibilityForOwner:field withAnimation:YES delay:NO]; 223 } 224 return doAccept; 225 } 226 227 // (Overridden from NSResponder) 228 - (BOOL)resignFirstResponder { 229 BOOL doResign = [super resignFirstResponder]; 230 AutocompleteTextField* field = [self delegate]; 231 // Only lock visibility if we've been set up with a delegate (the text field). 232 if (doResign && field) { 233 // Give the text field ownership of the visibility lock. 234 [[BrowserWindowController browserWindowControllerForView:field] 235 releaseBarVisibilityForOwner:field withAnimation:YES delay:YES]; 236 237 AutocompleteTextFieldObserver* observer = [self observer]; 238 if (observer) 239 observer->OnKillFocus(); 240 } 241 return doResign; 242 } 243 244 // (URLDropTarget protocol) 245 - (id<URLDropTargetController>)urlDropController { 246 BrowserWindowController* windowController = 247 [BrowserWindowController browserWindowControllerForView:self]; 248 return [windowController toolbarController]; 249 } 250 251 // (URLDropTarget protocol) 252 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender { 253 // Make ourself the first responder (even though we're presumably already the 254 // first responder), which will select the text to indicate that our contents 255 // would be replaced by a drop. 256 [[self window] makeFirstResponder:self]; 257 return [dropHandler_ draggingEntered:sender]; 258 } 259 260 // (URLDropTarget protocol) 261 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender { 262 return [dropHandler_ draggingUpdated:sender]; 263 } 264 265 // (URLDropTarget protocol) 266 - (void)draggingExited:(id<NSDraggingInfo>)sender { 267 return [dropHandler_ draggingExited:sender]; 268 } 269 270 // (URLDropTarget protocol) 271 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender { 272 return [dropHandler_ performDragOperation:sender]; 273 } 274 275 // Prevent control characters from being entered into the Omnibox. 276 // This is invoked for keyboard entry, not for pasting. 277 - (void)insertText:(id)aString { 278 // This method is documented as received either |NSString| or 279 // |NSAttributedString|. The autocomplete code will restyle the 280 // results in any case, so simplify by always using |NSString|. 281 if ([aString isKindOfClass:[NSAttributedString class]]) 282 aString = [aString string]; 283 284 // Repeatedly remove control characters. The loop will only ever 285 // execute at allwhen the user enters control characters (using 286 // Ctrl-Alt- or Ctrl-Q). Making this generally efficient would 287 // probably be a loss, since the input always seems to be a single 288 // character. 289 NSRange range = [aString rangeOfCharacterFromSet:forbiddenCharacters_]; 290 while (range.location != NSNotFound) { 291 aString = [aString stringByReplacingCharactersInRange:range withString:@""]; 292 range = [aString rangeOfCharacterFromSet:forbiddenCharacters_]; 293 } 294 DCHECK_EQ(range.length, 0U); 295 296 // NOTE: If |aString| is empty, this intentionally replaces the 297 // selection with empty. This seems consistent with the case where 298 // the input contained a mixture of characters and the string ended 299 // up not empty. 300 [super insertText:aString]; 301 } 302 303 - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange { 304 if (![self hasMarkedText]) { 305 // Before input methods set composition text in the omnibox, we need to 306 // examine whether the autocompletion controller accepts the keyword to 307 // avoid committing the current composition text wrongly. 308 AutocompleteTextFieldObserver* observer = [self observer]; 309 if (observer) 310 observer->OnStartingIME(); 311 } 312 313 [super setMarkedText:aString selectedRange:selRange]; 314 315 // Because the AutocompleteEditViewMac class treats marked text as content, 316 // we need to treat the change to marked text as content change as well. 317 [self didChangeText]; 318 } 319 320 - (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange 321 granularity:(NSSelectionGranularity)granularity { 322 AutocompleteTextFieldObserver* observer = [self observer]; 323 NSRange modifiedRange = [super selectionRangeForProposedRange:proposedSelRange 324 granularity:granularity]; 325 if (observer) 326 return observer->SelectionRangeForProposedRange(modifiedRange); 327 return modifiedRange; 328 } 329 330 331 332 333 - (void)setSelectedRange:(NSRange)charRange 334 affinity:(NSSelectionAffinity)affinity 335 stillSelecting:(BOOL)flag { 336 [super setSelectedRange:charRange affinity:affinity stillSelecting:flag]; 337 338 // We're only interested in selection changes directly caused by keyboard 339 // input from the user. 340 if (interpretingKeyEvents_) 341 textChangedByKeyEvents_ = YES; 342 } 343 344 - (void)interpretKeyEvents:(NSArray *)eventArray { 345 DCHECK(!interpretingKeyEvents_); 346 interpretingKeyEvents_ = YES; 347 textChangedByKeyEvents_ = NO; 348 AutocompleteTextFieldObserver* observer = [self observer]; 349 350 if (observer) 351 observer->OnBeforeChange(); 352 353 [super interpretKeyEvents:eventArray]; 354 355 if (textChangedByKeyEvents_ && observer) 356 observer->OnDidChange(); 357 358 DCHECK(interpretingKeyEvents_); 359 interpretingKeyEvents_ = NO; 360 } 361 362 - (BOOL)shouldChangeTextInRange:(NSRange)affectedCharRange 363 replacementString:(NSString *)replacementString { 364 BOOL ret = [super shouldChangeTextInRange:affectedCharRange 365 replacementString:replacementString]; 366 367 if (ret && !interpretingKeyEvents_) { 368 AutocompleteTextFieldObserver* observer = [self observer]; 369 if (observer) 370 observer->OnBeforeChange(); 371 } 372 return ret; 373 } 374 375 - (void)didChangeText { 376 [super didChangeText]; 377 378 AutocompleteTextFieldObserver* observer = [self observer]; 379 if (observer) { 380 if (!interpretingKeyEvents_) 381 observer->OnDidChange(); 382 else 383 textChangedByKeyEvents_ = YES; 384 } 385 } 386 387 - (void)doCommandBySelector:(SEL)cmd { 388 // TODO(shess): Review code for cases where we're fruitlessly attempting to 389 // work in spite of not having an observer. 390 AutocompleteTextFieldObserver* observer = [self observer]; 391 392 if (observer && observer->OnDoCommandBySelector(cmd)) { 393 // The observer should already be aware of any changes to the text, so 394 // setting |textChangedByKeyEvents_| to NO to prevent its OnDidChange() 395 // method from being called unnecessarily. 396 textChangedByKeyEvents_ = NO; 397 return; 398 } 399 400 // If the escape key was pressed and no revert happened and we're in 401 // fullscreen mode, make it resign key. 402 if (cmd == @selector(cancelOperation:)) { 403 BrowserWindowController* windowController = 404 [BrowserWindowController browserWindowControllerForView:self]; 405 if ([windowController isFullscreen]) { 406 [windowController focusTabContents]; 407 return; 408 } 409 } 410 411 [super doCommandBySelector:cmd]; 412 } 413 414 - (void)setAttributedString:(NSAttributedString*)aString { 415 NSTextStorage* textStorage = [self textStorage]; 416 DCHECK(textStorage); 417 [textStorage setAttributedString:aString]; 418 419 // The text has been changed programmatically. The observer should know 420 // this change, so setting |textChangedByKeyEvents_| to NO to 421 // prevent its OnDidChange() method from being called unnecessarily. 422 textChangedByKeyEvents_ = NO; 423 } 424 425 - (void)mouseDown:(NSEvent*)theEvent { 426 // Close the popup before processing the event. 427 AutocompleteTextFieldObserver* observer = [self observer]; 428 if (observer) 429 observer->ClosePopup(); 430 431 [super mouseDown:theEvent]; 432 } 433 434 @end 435