autocomplete_edit_view_mac.mm revision c407dc5cd9bdc5668497f21b26b09d988ab439de
1// Copyright (c) 2010 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#include "chrome/browser/autocomplete/autocomplete_edit_view_mac.h" 6 7#include <Carbon/Carbon.h> // kVK_Return 8 9#include "app/clipboard/clipboard.h" 10#include "app/clipboard/scoped_clipboard_writer.h" 11#include "app/resource_bundle.h" 12#include "base/nsimage_cache_mac.h" 13#include "base/string_util.h" 14#include "base/sys_string_conversions.h" 15#include "base/utf_string_conversions.h" 16#include "chrome/browser/autocomplete/autocomplete_edit.h" 17#include "chrome/browser/autocomplete/autocomplete_popup_model.h" 18#include "chrome/browser/autocomplete/autocomplete_popup_view_mac.h" 19#include "chrome/browser/browser_process.h" 20#include "chrome/browser/cocoa/event_utils.h" 21#include "chrome/browser/tab_contents/tab_contents.h" 22#include "chrome/browser/toolbar_model.h" 23#include "grit/generated_resources.h" 24#include "grit/theme_resources.h" 25#include "net/base/escape.h" 26#import "third_party/mozilla/NSPasteboard+Utils.h" 27 28// Focus-handling between |field_| and |model_| is a bit subtle. 29// Other platforms detect change of focus, which is inconvenient 30// without subclassing NSTextField (even with a subclass, the use of a 31// field editor may complicate things). 32// 33// |model_| doesn't actually do anything when it gains focus, it just 34// initializes. Visible activity happens only after the user edits. 35// NSTextField delegate receives messages around starting and ending 36// edits, so that sufcices to catch focus changes. Since all calls 37// into |model_| start from AutocompleteEditViewMac, in the worst case 38// we can add code to sync up the sense of focus as needed. 39// 40// I've added DCHECK(IsFirstResponder()) in the places which I believe 41// should only be reachable when |field_| is being edited. If these 42// fire, it probably means someone unexpected is calling into 43// |model_|. 44// 45// Other platforms don't appear to have the sense of "key window" that 46// Mac does (I believe their fields lose focus when the window loses 47// focus). Rather than modifying focus outside the control's edit 48// scope, when the window resigns key the autocomplete popup is 49// closed. |model_| still believes it has focus, and the popup will 50// be regenerated on the user's next edit. That seems to match how 51// things work on other platforms. 52 53namespace { 54 55// TODO(shess): This is ugly, find a better way. Using it right now 56// so that I can crib from gtk and still be able to see that I'm using 57// the same values easily. 58NSColor* ColorWithRGBBytes(int rr, int gg, int bb) { 59 DCHECK_LE(rr, 255); 60 DCHECK_LE(bb, 255); 61 DCHECK_LE(gg, 255); 62 return [NSColor colorWithCalibratedRed:static_cast<float>(rr)/255.0 63 green:static_cast<float>(gg)/255.0 64 blue:static_cast<float>(bb)/255.0 65 alpha:1.0]; 66} 67 68NSColor* HostTextColor() { 69 return [NSColor blackColor]; 70} 71NSColor* BaseTextColor() { 72 return [NSColor darkGrayColor]; 73} 74NSColor* SecureSchemeColor() { 75 return ColorWithRGBBytes(0x07, 0x95, 0x00); 76} 77NSColor* SecurityErrorSchemeColor() { 78 return ColorWithRGBBytes(0xa2, 0x00, 0x00); 79} 80 81// Store's the model and view state across tab switches. 82struct AutocompleteEditViewMacState { 83 AutocompleteEditViewMacState(const AutocompleteEditModel::State model_state, 84 const bool has_focus, const NSRange& selection) 85 : model_state(model_state), 86 has_focus(has_focus), 87 selection(selection) { 88 } 89 90 const AutocompleteEditModel::State model_state; 91 const bool has_focus; 92 const NSRange selection; 93}; 94 95// Returns a lazily initialized property bag accessor for saving our 96// state in a TabContents. When constructed |accessor| generates a 97// globally-unique id used to index into the per-tab PropertyBag used 98// to store the state data. 99PropertyAccessor<AutocompleteEditViewMacState>* GetStateAccessor() { 100 static PropertyAccessor<AutocompleteEditViewMacState> accessor; 101 return &accessor; 102} 103 104// Accessors for storing and getting the state from the tab. 105void StoreStateToTab(TabContents* tab, 106 const AutocompleteEditViewMacState& state) { 107 GetStateAccessor()->SetProperty(tab->property_bag(), state); 108} 109const AutocompleteEditViewMacState* GetStateFromTab(const TabContents* tab) { 110 return GetStateAccessor()->GetProperty(tab->property_bag()); 111} 112 113// Helper to make converting url_parse ranges to NSRange easier to 114// read. 115NSRange ComponentToNSRange(const url_parse::Component& component) { 116 return NSMakeRange(static_cast<NSInteger>(component.begin), 117 static_cast<NSInteger>(component.len)); 118} 119 120} // namespace 121 122// static 123NSImage* AutocompleteEditViewMac::ImageForResource(int resource_id) { 124 NSString* image_name = nil; 125 126 switch(resource_id) { 127 // From the autocomplete popup, or the star icon at the RHS of the 128 // text field. 129 case IDR_OMNIBOX_STAR: image_name = @"omnibox_star.pdf"; break; 130 case IDR_OMNIBOX_STAR_LIT: image_name = @"omnibox_star_lit.pdf"; break; 131 132 // Values from |AutocompleteMatch::TypeToIcon()|. 133 case IDR_OMNIBOX_SEARCH: image_name = @"omnibox_search.pdf"; break; 134 case IDR_OMNIBOX_HTTP: image_name = @"omnibox_http.pdf"; break; 135 case IDR_OMNIBOX_HISTORY: image_name = @"omnibox_history.pdf"; break; 136 case IDR_OMNIBOX_MORE: image_name = @"omnibox_more.pdf"; break; 137 138 // Values from |ToolbarModel::GetIcon()|. 139 case IDR_OMNIBOX_HTTPS_VALID: 140 image_name = @"omnibox_https_valid.pdf"; break; 141 case IDR_OMNIBOX_HTTPS_WARNING: 142 image_name = @"omnibox_https_warning.pdf"; break; 143 case IDR_OMNIBOX_HTTPS_INVALID: 144 image_name = @"omnibox_https_invalid.pdf"; break; 145 } 146 147 if (image_name) { 148 if (NSImage* image = nsimage_cache::ImageNamed(image_name)) { 149 return image; 150 } else { 151 NOTREACHED() 152 << "Missing image for " << base::SysNSStringToUTF8(image_name); 153 } 154 } 155 156 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 157 return rb.GetNSImageNamed(resource_id); 158} 159 160AutocompleteEditViewMac::AutocompleteEditViewMac( 161 AutocompleteEditController* controller, 162 ToolbarModel* toolbar_model, 163 Profile* profile, 164 CommandUpdater* command_updater, 165 AutocompleteTextField* field) 166 : model_(new AutocompleteEditModel(this, controller, profile)), 167 popup_view_(new AutocompletePopupViewMac(this, model_.get(), profile, 168 field)), 169 controller_(controller), 170 toolbar_model_(toolbar_model), 171 command_updater_(command_updater), 172 field_(field), 173 line_height_(0) { 174 DCHECK(controller); 175 DCHECK(toolbar_model); 176 DCHECK(profile); 177 DCHECK(command_updater); 178 DCHECK(field); 179 [field_ setObserver:this]; 180 181 // Needed so that editing doesn't lose the styling. 182 [field_ setAllowsEditingTextAttributes:YES]; 183 184 // Get the appropriate line height for the font that we use. 185 scoped_nsobject<NSLayoutManager> 186 layoutManager([[NSLayoutManager alloc] init]); 187 [layoutManager setUsesScreenFonts:YES]; 188 line_height_ = [layoutManager defaultLineHeightForFont:GetFieldFont()]; 189 DCHECK(line_height_ > 0); 190} 191 192AutocompleteEditViewMac::~AutocompleteEditViewMac() { 193 // Destroy popup view before this object in case it tries to call us 194 // back in the destructor. Likewise for destroying the model before 195 // this object. 196 popup_view_.reset(); 197 model_.reset(); 198 199 // Disconnect from |field_|, it outlives this object. 200 [field_ setObserver:NULL]; 201} 202 203void AutocompleteEditViewMac::SaveStateToTab(TabContents* tab) { 204 DCHECK(tab); 205 206 const bool hasFocus = [field_ currentEditor] ? true : false; 207 208 NSRange range; 209 if (hasFocus) { 210 range = GetSelectedRange(); 211 } else { 212 // If we are not focussed, there is no selection. Manufacture 213 // something reasonable in case it starts to matter in the future. 214 range = NSMakeRange(0, [[field_ stringValue] length]); 215 } 216 217 AutocompleteEditViewMacState state(model_->GetStateForTabSwitch(), 218 hasFocus, range); 219 StoreStateToTab(tab, state); 220} 221 222void AutocompleteEditViewMac::Update( 223 const TabContents* tab_for_state_restoring) { 224 // TODO(shess): It seems like if the tab is non-NULL, then this code 225 // shouldn't need to be called at all. When coded that way, I find 226 // that the field isn't always updated correctly. Figure out why 227 // this is. Maybe this method should be refactored into more 228 // specific cases. 229 const bool user_visible = 230 model_->UpdatePermanentText(toolbar_model_->GetText()); 231 232 if (tab_for_state_restoring) { 233 RevertAll(); 234 235 const AutocompleteEditViewMacState* state = 236 GetStateFromTab(tab_for_state_restoring); 237 if (state) { 238 // Should restore the user's text via SetUserText(). 239 model_->RestoreState(state->model_state); 240 241 // Restore focus and selection if they were present when the tab 242 // was switched away. 243 if (state->has_focus) { 244 // TODO(shess): Unfortunately, there is no safe way to update 245 // this because TabStripController -selectTabWithContents:* is 246 // also messing with focus. Both parties need to agree to 247 // store existing state before anyone tries to setup the new 248 // state. Anyhow, it would look something like this. 249#if 0 250 [[field_ window] makeFirstResponder:field_]; 251 [[field_ currentEditor] setSelectedRange:state->selection]; 252#endif 253 } 254 } 255 } else if (user_visible) { 256 // Restore everything to the baseline look. 257 RevertAll(); 258 // TODO(shess): Figure out how this case is used, to make sure 259 // we're getting the selection and popup right. 260 261 } else { 262 // TODO(shess): This corresponds to _win and _gtk, except those 263 // guard it with a test for whether the security level changed. 264 // But AFAICT, that can only change if the text changed, and that 265 // code compares the toolbar_model_ security level with the local 266 // security level. Dig in and figure out why this isn't a no-op 267 // that should go away. 268 EmphasizeURLComponents(); 269 } 270} 271 272void AutocompleteEditViewMac::OpenURL(const GURL& url, 273 WindowOpenDisposition disposition, 274 PageTransition::Type transition, 275 const GURL& alternate_nav_url, 276 size_t selected_line, 277 const std::wstring& keyword) { 278 // TODO(shess): Why is the caller passing an invalid url in the 279 // first place? Make sure that case isn't being dropped on the 280 // floor. 281 if (!url.is_valid()) { 282 return; 283 } 284 285 model_->OpenURL(url, disposition, transition, alternate_nav_url, 286 selected_line, keyword); 287} 288 289std::wstring AutocompleteEditViewMac::GetText() const { 290 return base::SysNSStringToWide([field_ stringValue]); 291} 292 293bool AutocompleteEditViewMac::IsEditingOrEmpty() const { 294 return model_->user_input_in_progress() || 295 ([[field_ stringValue] length] == 0); 296} 297 298int AutocompleteEditViewMac::GetIcon() const { 299 return IsEditingOrEmpty() ? 300 AutocompleteMatch::TypeToIcon(model_->CurrentTextType()) : 301 toolbar_model_->GetIcon(); 302} 303 304void AutocompleteEditViewMac::SetUserText(const std::wstring& text, 305 const std::wstring& display_text, 306 bool update_popup) { 307 model_->SetUserText(text); 308 // TODO(shess): TODO below from gtk. 309 // TODO(deanm): something about selection / focus change here. 310 SetText(display_text); 311 if (update_popup) { 312 UpdatePopup(); 313 } 314 controller_->OnChanged(); 315} 316 317NSRange AutocompleteEditViewMac::GetSelectedRange() const { 318 DCHECK([field_ currentEditor]); 319 return [[field_ currentEditor] selectedRange]; 320} 321 322void AutocompleteEditViewMac::SetSelectedRange(const NSRange range) { 323 // This can be called when we don't have focus. For instance, when 324 // the user clicks the "Go" button. 325 if (model_->has_focus()) { 326 // TODO(shess): If |model_| thinks we have focus, this should not 327 // be necessary. Try to convert to DCHECK(IsFirstResponder()). 328 if (![field_ currentEditor]) { 329 [[field_ window] makeFirstResponder:field_]; 330 } 331 332 // TODO(shess): What if it didn't get first responder, and there is 333 // no field editor? This will do nothing. Well, at least it won't 334 // crash. Think of something more productive to do, or prove that 335 // it cannot occur and DCHECK appropriately. 336 [[field_ currentEditor] setSelectedRange:range]; 337 } 338} 339 340void AutocompleteEditViewMac::SetWindowTextAndCaretPos(const std::wstring& text, 341 size_t caret_pos) { 342 DCHECK_LE(caret_pos, text.size()); 343 SetTextAndSelectedRange(text, NSMakeRange(caret_pos, caret_pos)); 344} 345 346void AutocompleteEditViewMac::SetForcedQuery() { 347 // We need to do this first, else |SetSelectedRange()| won't work. 348 FocusLocation(true); 349 350 const std::wstring current_text(GetText()); 351 if (current_text.empty() || (current_text[0] != '?')) { 352 SetUserText(L"?"); 353 } else { 354 NSRange range = NSMakeRange(1, current_text.size() - 1); 355 [[field_ currentEditor] setSelectedRange:range]; 356 } 357} 358 359bool AutocompleteEditViewMac::IsSelectAll() { 360 if (![field_ currentEditor]) 361 return true; 362 const NSRange all_range = NSMakeRange(0, [[field_ stringValue] length]); 363 return NSEqualRanges(all_range, GetSelectedRange()); 364} 365 366void AutocompleteEditViewMac::SelectAll(bool reversed) { 367 // TODO(shess): Figure out what |reversed| implies. The gtk version 368 // has it imply inverting the selection front to back, but I don't 369 // even know if that makes sense for Mac. 370 371 // TODO(shess): Verify that we should be stealing focus at this 372 // point. 373 SetSelectedRange(NSMakeRange(0, GetText().size())); 374} 375 376void AutocompleteEditViewMac::RevertAll() { 377 ClosePopup(); 378 model_->Revert(); 379 380 // TODO(shess): This should be a no-op, the results from GetText() 381 // could only get there via UpdateAndStyleText() in the first place. 382 // Dig into where this code can be called from and see if this line 383 // can be removed. 384 EmphasizeURLComponents(); 385 controller_->OnChanged(); 386 [field_ clearUndoChain]; 387} 388 389void AutocompleteEditViewMac::UpdatePopup() { 390 model_->SetInputInProgress(true); 391 if (!model_->has_focus()) 392 return; 393 394 // Comment copied from AutocompleteEditViewWin::UpdatePopup(): 395 // Don't inline autocomplete when: 396 // * The user is deleting text 397 // * The caret/selection isn't at the end of the text 398 // * The user has just pasted in something that replaced all the text 399 // * The user is trying to compose something in an IME 400 bool prevent_inline_autocomplete = false; 401 NSTextView* editor = (NSTextView*)[field_ currentEditor]; 402 if (editor) { 403 if ([editor hasMarkedText]) 404 prevent_inline_autocomplete = true; 405 406 if (NSMaxRange([editor selectedRange]) < [[editor textStorage] length]) 407 prevent_inline_autocomplete = true; 408 } 409 410 model_->StartAutocomplete(prevent_inline_autocomplete); 411} 412 413void AutocompleteEditViewMac::ClosePopup() { 414 popup_view_->GetModel()->StopAutocomplete(); 415} 416 417void AutocompleteEditViewMac::SetFocus() { 418} 419 420void AutocompleteEditViewMac::SetText(const std::wstring& display_text) { 421 NSString* ss = base::SysWideToNSString(display_text); 422 NSMutableAttributedString* as = 423 [[[NSMutableAttributedString alloc] initWithString:ss] autorelease]; 424 425 ApplyTextAttributes(display_text, as); 426 427 [field_ setAttributedStringValue:as]; 428 429 // TODO(shess): This may be an appropriate place to call: 430 // controller_->OnChanged(); 431 // In the current implementation, this tells LocationBarViewMac to 432 // mess around with |model_| and update |field_|. Unfortunately, 433 // when I look at our peer implementations, it's not entirely clear 434 // to me if this is safe. SetText() is sort of an utility method, 435 // and different callers sometimes have different needs. Research 436 // this issue so that it can be added safely. 437 438 // TODO(shess): Also, consider whether this code couldn't just 439 // manage things directly. Windows uses a series of overlaid view 440 // objects to accomplish the hinting stuff that OnChanged() does, so 441 // it makes sense to have it in the controller that lays those 442 // things out. Mac instead pushes the support into a custom 443 // text-field implementation. 444} 445 446void AutocompleteEditViewMac::SetTextAndSelectedRange( 447 const std::wstring& display_text, const NSRange range) { 448 SetText(display_text); 449 SetSelectedRange(range); 450} 451 452void AutocompleteEditViewMac::EmphasizeURLComponents() { 453 NSTextView* editor = (NSTextView*)[field_ currentEditor]; 454 // If the autocomplete text field is in editing mode, then we can just change 455 // its attributes through its editor. Otherwise, we simply reset its content. 456 if (editor) { 457 NSTextStorage* storage = [editor textStorage]; 458 [storage beginEditing]; 459 460 // Clear the existing attributes from the text storage, then 461 // overlay the appropriate Omnibox attributes. 462 [storage setAttributes:[NSDictionary dictionary] 463 range:NSMakeRange(0, [storage length])]; 464 ApplyTextAttributes(GetText(), storage); 465 466 [storage endEditing]; 467 } else { 468 SetText(GetText()); 469 } 470} 471 472void AutocompleteEditViewMac::ApplyTextAttributes( 473 const std::wstring& display_text, NSMutableAttributedString* as) { 474 [as addAttribute:NSFontAttributeName value:GetFieldFont() 475 range:NSMakeRange(0, [as length])]; 476 477 // Make a paragraph style locking in the standard line height as the maximum, 478 // otherwise the baseline may shift "downwards". 479 scoped_nsobject<NSMutableParagraphStyle> 480 paragraph_style([[NSMutableParagraphStyle alloc] init]); 481 [paragraph_style setMaximumLineHeight:line_height_]; 482 [as addAttribute:NSParagraphStyleAttributeName value:paragraph_style 483 range:NSMakeRange(0, [as length])]; 484 485 url_parse::Component scheme, host; 486 AutocompleteInput::ParseForEmphasizeComponents( 487 display_text, model_->GetDesiredTLD(), &scheme, &host); 488 const bool emphasize = model_->CurrentTextIsURL() && (host.len > 0); 489 if (emphasize) { 490 [as addAttribute:NSForegroundColorAttributeName value:BaseTextColor() 491 range:NSMakeRange(0, [as length])]; 492 493 [as addAttribute:NSForegroundColorAttributeName value:HostTextColor() 494 range:ComponentToNSRange(host)]; 495 } 496 497 // TODO(shess): GTK has this as a member var, figure out why. 498 // [Could it be to not change if no change? If so, I'm guessing 499 // AppKit may already handle that.] 500 const ToolbarModel::SecurityLevel security_level = 501 toolbar_model_->GetSecurityLevel(); 502 503 // Emphasize the scheme for security UI display purposes (if necessary). 504 if (!model_->user_input_in_progress() && scheme.is_nonempty() && 505 (security_level != ToolbarModel::NONE)) { 506 NSColor* color; 507 if (security_level == ToolbarModel::EV_SECURE || 508 security_level == ToolbarModel::SECURE) { 509 color = SecureSchemeColor(); 510 } else if (security_level == ToolbarModel::SECURITY_ERROR) { 511 color = SecurityErrorSchemeColor(); 512 // Add a strikethrough through the scheme. 513 [as addAttribute:NSStrikethroughStyleAttributeName 514 value:[NSNumber numberWithInt:NSUnderlineStyleSingle] 515 range:ComponentToNSRange(scheme)]; 516 } else if (security_level == ToolbarModel::SECURITY_WARNING) { 517 color = BaseTextColor(); 518 } else { 519 NOTREACHED(); 520 color = BaseTextColor(); 521 } 522 [as addAttribute:NSForegroundColorAttributeName value:color 523 range:ComponentToNSRange(scheme)]; 524 } 525} 526 527void AutocompleteEditViewMac::OnTemporaryTextMaybeChanged( 528 const std::wstring& display_text, bool save_original_selection) { 529 // TODO(shess): I believe this is for when the user arrows around 530 // the popup, will be restored if they hit escape. Figure out if 531 // that is for certain it. 532 if (save_original_selection) 533 saved_temporary_selection_ = GetSelectedRange(); 534 535 SetWindowTextAndCaretPos(display_text, display_text.size()); 536 controller_->OnChanged(); 537 [field_ clearUndoChain]; 538} 539 540bool AutocompleteEditViewMac::OnInlineAutocompleteTextMaybeChanged( 541 const std::wstring& display_text, size_t user_text_length) { 542 // TODO(shess): Make sure that this actually works. The round trip 543 // to native form and back may mean that it's the same but not the 544 // same. 545 if (display_text == GetText()) { 546 return false; 547 } 548 549 DCHECK_LE(user_text_length, display_text.size()); 550 const NSRange range = 551 NSMakeRange(user_text_length, display_text.size() - user_text_length); 552 SetTextAndSelectedRange(display_text, range); 553 controller_->OnChanged(); 554 [field_ clearUndoChain]; 555 556 return true; 557} 558 559void AutocompleteEditViewMac::OnRevertTemporaryText() { 560 SetSelectedRange(saved_temporary_selection_); 561} 562 563bool AutocompleteEditViewMac::IsFirstResponder() const { 564 return [field_ currentEditor] != nil ? true : false; 565} 566 567void AutocompleteEditViewMac::OnBeforePossibleChange() { 568 // We should only arrive here when the field is focussed. 569 DCHECK(IsFirstResponder()); 570 571 selection_before_change_ = GetSelectedRange(); 572 text_before_change_ = GetText(); 573} 574 575bool AutocompleteEditViewMac::OnAfterPossibleChange() { 576 // We should only arrive here when the field is focussed. 577 DCHECK(IsFirstResponder()); 578 579 const NSRange new_selection(GetSelectedRange()); 580 const std::wstring new_text(GetText()); 581 const size_t length = new_text.length(); 582 583 const bool selection_differs = !NSEqualRanges(new_selection, 584 selection_before_change_); 585 const bool at_end_of_edit = (length == new_selection.location); 586 const bool text_differs = (new_text != text_before_change_); 587 588 // When the user has deleted text, we don't allow inline 589 // autocomplete. This is assumed if the text has gotten shorter AND 590 // the selection has shifted towards the front of the text. During 591 // normal typing the text will almost always be shorter (as the new 592 // input replaces the autocomplete suggestion), but in that case the 593 // selection point will have moved towards the end of the text. 594 // TODO(shess): In our implementation, we can catch -deleteBackward: 595 // and other methods to provide positive knowledge that a delete 596 // occured, rather than intuiting it from context. Consider whether 597 // that would be a stronger approach. 598 const bool just_deleted_text = 599 (length < text_before_change_.length() && 600 new_selection.location <= selection_before_change_.location); 601 602 const bool something_changed = model_->OnAfterPossibleChange(new_text, 603 selection_differs, text_differs, just_deleted_text, at_end_of_edit); 604 605 // Restyle in case the user changed something. 606 // TODO(shess): I believe there are multiple-redraw cases, here. 607 // Linux watches for something_changed && text_differs, but that 608 // fails for us in case you copy the URL and paste the identical URL 609 // back (we'll lose the styling). 610 EmphasizeURLComponents(); 611 controller_->OnChanged(); 612 613 return something_changed; 614} 615 616gfx::NativeView AutocompleteEditViewMac::GetNativeView() const { 617 return field_; 618} 619 620CommandUpdater* AutocompleteEditViewMac::GetCommandUpdater() { 621 return command_updater_; 622} 623 624void AutocompleteEditViewMac::OnDidBeginEditing() { 625 // We should only arrive here when the field is focussed. 626 DCHECK([field_ currentEditor]); 627 628 // Capture the current state. 629 OnBeforePossibleChange(); 630} 631 632void AutocompleteEditViewMac::OnDidChange() { 633 // Figure out what changed and notify the model_. 634 OnAfterPossibleChange(); 635 636 // Then capture the new state. 637 OnBeforePossibleChange(); 638} 639 640void AutocompleteEditViewMac::OnDidEndEditing() { 641 ClosePopup(); 642} 643 644bool AutocompleteEditViewMac::OnDoCommandBySelector(SEL cmd) { 645 // We should only arrive here when the field is focussed. 646 DCHECK(IsFirstResponder()); 647 648 // Don't intercept up/down-arrow if the popup isn't open. 649 if (popup_view_->IsOpen()) { 650 if (cmd == @selector(moveDown:)) { 651 model_->OnUpOrDownKeyPressed(1); 652 return true; 653 } 654 655 if (cmd == @selector(moveUp:)) { 656 model_->OnUpOrDownKeyPressed(-1); 657 return true; 658 } 659 } 660 661 if (cmd == @selector(scrollPageDown:)) { 662 model_->OnUpOrDownKeyPressed(model_->result().size()); 663 return true; 664 } 665 666 if (cmd == @selector(scrollPageUp:)) { 667 model_->OnUpOrDownKeyPressed(-model_->result().size()); 668 return true; 669 } 670 671 if (cmd == @selector(cancelOperation:)) { 672 return model_->OnEscapeKeyPressed(); 673 } 674 675 if (cmd == @selector(insertTab:) || 676 cmd == @selector(insertTabIgnoringFieldEditor:)) { 677 if (model_->is_keyword_hint() && !model_->keyword().empty()) { 678 model_->AcceptKeyword(); 679 return true; 680 } 681 } 682 683 // |-noop:| is sent when the user presses Cmd+Return. Override the no-op 684 // behavior with the proper WindowOpenDisposition. 685 NSEvent* event = [NSApp currentEvent]; 686 if (cmd == @selector(insertNewline:) || 687 (cmd == @selector(noop:) && [event keyCode] == kVK_Return)) { 688 WindowOpenDisposition disposition = 689 event_utils::WindowOpenDispositionFromNSEvent(event); 690 model_->AcceptInput(disposition, false); 691 // Opening a URL in a background tab should also revert the omnibox contents 692 // to their original state. We cannot do a blanket revert in OpenURL() 693 // because middle-clicks also open in a new background tab, but those should 694 // not revert the omnibox text. 695 RevertAll(); 696 return true; 697 } 698 699 // Option-Return 700 if (cmd == @selector(insertNewlineIgnoringFieldEditor:)) { 701 model_->AcceptInput(NEW_FOREGROUND_TAB, false); 702 return true; 703 } 704 705 // When the user does Control-Enter, the existing content has "www." 706 // prepended and ".com" appended. |model_| should already have 707 // received notification when the Control key was depressed, but it 708 // is safe to tell it twice. 709 if (cmd == @selector(insertLineBreak:)) { 710 OnControlKeyChanged(true); 711 WindowOpenDisposition disposition = 712 event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); 713 model_->AcceptInput(disposition, false); 714 return true; 715 } 716 717 if (cmd == @selector(deleteBackward:)) { 718 if (OnBackspacePressed()) { 719 return true; 720 } 721 } 722 723 if (cmd == @selector(deleteForward:)) { 724 const NSUInteger modifiers = [[NSApp currentEvent] modifierFlags]; 725 if ((modifiers & NSShiftKeyMask) != 0) { 726 if (popup_view_->IsOpen()) { 727 popup_view_->GetModel()->TryDeletingCurrentItem(); 728 return true; 729 } 730 } 731 } 732 733 // Capture the state before the operation changes the content. 734 // TODO(shess): Determine if this is always redundent WRT the call 735 // in -controlTextDidChange:. 736 OnBeforePossibleChange(); 737 return false; 738} 739 740void AutocompleteEditViewMac::OnSetFocus(bool control_down) { 741 model_->OnSetFocus(control_down); 742 controller_->OnSetFocus(); 743} 744 745void AutocompleteEditViewMac::OnKillFocus() { 746 // Tell the model to reset itself. 747 model_->OnKillFocus(); 748 controller_->OnKillFocus(); 749} 750 751bool AutocompleteEditViewMac::CanCopy() { 752 const NSRange selection = GetSelectedRange(); 753 return selection.length > 0; 754} 755 756void AutocompleteEditViewMac::CopyToPasteboard(NSPasteboard* pb) { 757 DCHECK(CanCopy()); 758 759 const NSRange selection = GetSelectedRange(); 760 std::wstring text = base::SysNSStringToWide( 761 [[field_ stringValue] substringWithRange:selection]); 762 763 GURL url; 764 bool write_url = false; 765 model_->AdjustTextForCopy(selection.location, IsSelectAll(), &text, &url, 766 &write_url); 767 768 NSString* nstext = base::SysWideToNSString(text); 769 [pb declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; 770 [pb setString:nstext forType:NSStringPboardType]; 771 772 if (write_url) { 773 [pb declareURLPasteboardWithAdditionalTypes:[NSArray array] owner:nil]; 774 [pb setDataForURL:base::SysUTF8ToNSString(url.spec()) title:nstext]; 775 } 776} 777 778void AutocompleteEditViewMac::OnPaste() { 779 // This code currently expects |field_| to be focussed. 780 DCHECK([field_ currentEditor]); 781 782 std::wstring text = GetClipboardText(g_browser_process->clipboard()); 783 if (text.empty()) { 784 return; 785 } 786 NSString* s = base::SysWideToNSString(text); 787 788 // -shouldChangeTextInRange:* and -didChangeText are documented in 789 // NSTextView as things you need to do if you write additional 790 // user-initiated editing functions. They cause the appropriate 791 // delegate methods to be called. 792 // TODO(shess): It would be nice to separate the Cocoa-specific code 793 // from the Chrome-specific code. 794 NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]); 795 const NSRange selectedRange = GetSelectedRange(); 796 if ([editor shouldChangeTextInRange:selectedRange replacementString:s]) { 797 // If this paste will be replacing all the text, record that, so 798 // we can do different behaviors in such a case. 799 if (IsSelectAll()) 800 model_->on_paste_replacing_all(); 801 802 // Force a Paste operation to trigger the text_changed code in 803 // OnAfterPossibleChange(), even if identical contents are pasted 804 // into the text box. 805 text_before_change_.clear(); 806 807 [editor replaceCharactersInRange:selectedRange withString:s]; 808 [editor didChangeText]; 809 } 810} 811 812bool AutocompleteEditViewMac::CanPasteAndGo() { 813 return 814 model_->CanPasteAndGo(GetClipboardText(g_browser_process->clipboard())); 815} 816 817int AutocompleteEditViewMac::GetPasteActionStringId() { 818 DCHECK(CanPasteAndGo()); 819 820 // Use PASTE_AND_SEARCH as the default fallback (although the DCHECK above 821 // should never trigger). 822 if (!model_->is_paste_and_search()) 823 return IDS_PASTE_AND_GO; 824 else 825 return IDS_PASTE_AND_SEARCH; 826} 827 828void AutocompleteEditViewMac::OnPasteAndGo() { 829 if (CanPasteAndGo()) 830 model_->PasteAndGo(); 831} 832 833void AutocompleteEditViewMac::OnFrameChanged() { 834 // TODO(shess): UpdatePopupAppearance() is called frequently, so it 835 // should be really cheap, but in this case we could probably make 836 // things even cheaper by refactoring between the popup-placement 837 // code and the matrix-population code. 838 popup_view_->UpdatePopupAppearance(); 839 840 // Give controller a chance to rearrange decorations. 841 controller_->OnChanged(); 842} 843 844bool AutocompleteEditViewMac::OnBackspacePressed() { 845 // Don't intercept if not in keyword search mode. 846 if (model_->is_keyword_hint() || model_->keyword().empty()) { 847 return false; 848 } 849 850 // Don't intercept if there is a selection, or the cursor isn't at 851 // the leftmost position. 852 const NSRange selection = GetSelectedRange(); 853 if (selection.length > 0 || selection.location > 0) { 854 return false; 855 } 856 857 // We're showing a keyword and the user pressed backspace at the 858 // beginning of the text. Delete the selected keyword. 859 model_->ClearKeyword(GetText()); 860 return true; 861} 862 863void AutocompleteEditViewMac::OnControlKeyChanged(bool pressed) { 864 model_->OnControlKeyChanged(pressed); 865} 866 867void AutocompleteEditViewMac::FocusLocation(bool select_all) { 868 if ([field_ isEditable]) { 869 // If the text field has a field editor, it's the first responder, meaning 870 // that it's already focused. makeFirstResponder: will select all, so only 871 // call it if this behavior is desired. 872 if (select_all || ![field_ currentEditor]) 873 [[field_ window] makeFirstResponder:field_]; 874 DCHECK_EQ([field_ currentEditor], [[field_ window] firstResponder]); 875 } 876} 877 878// TODO(shess): Copied from autocomplete_edit_view_win.cc. Could this 879// be pushed into the model? 880std::wstring AutocompleteEditViewMac::GetClipboardText(Clipboard* clipboard) { 881 // autocomplete_edit_view_win.cc assumes this can never happen, we 882 // will too. 883 DCHECK(clipboard); 884 885 if (clipboard->IsFormatAvailable(Clipboard::GetPlainTextWFormatType(), 886 Clipboard::BUFFER_STANDARD)) { 887 string16 text16; 888 clipboard->ReadText(Clipboard::BUFFER_STANDARD, &text16); 889 890 // Note: Unlike in the find popup and textfield view, here we completely 891 // remove whitespace strings containing newlines. We assume users are 892 // most likely pasting in URLs that may have been split into multiple 893 // lines in terminals, email programs, etc., and so linebreaks indicate 894 // completely bogus whitespace that would just cause the input to be 895 // invalid. 896 return CollapseWhitespace(UTF16ToWide(text16), true); 897 } 898 899 // Try bookmark format. 900 // 901 // It is tempting to try bookmark format first, but the URL we get out of a 902 // bookmark has been cannonicalized via GURL. This means if a user copies 903 // and pastes from the URL bar to itself, the text will get fixed up and 904 // cannonicalized, which is not what the user expects. By pasting in this 905 // order, we are sure to paste what the user copied. 906 if (clipboard->IsFormatAvailable(Clipboard::GetUrlWFormatType(), 907 Clipboard::BUFFER_STANDARD)) { 908 std::string url_str; 909 clipboard->ReadBookmark(NULL, &url_str); 910 // pass resulting url string through GURL to normalize 911 GURL url(url_str); 912 if (url.is_valid()) { 913 return UTF8ToWide(url.spec()); 914 } 915 } 916 917 return std::wstring(); 918} 919 920// static 921NSFont* AutocompleteEditViewMac::GetFieldFont() { 922 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 923 return rb.GetFont(ResourceBundle::BaseFont).nativeFont(); 924} 925