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