1 // Copyright (c) 2012 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/ui/cocoa/omnibox/omnibox_view_mac.h" 6 7 #include <Carbon/Carbon.h> // kVK_Return 8 9 #include "base/mac/foundation_util.h" 10 #include "base/metrics/histogram.h" 11 #include "base/strings/string_util.h" 12 #include "base/strings/sys_string_conversions.h" 13 #include "base/strings/utf_string_conversions.h" 14 #include "chrome/browser/autocomplete/autocomplete_input.h" 15 #include "chrome/browser/autocomplete/autocomplete_match.h" 16 #include "chrome/browser/browser_process.h" 17 #include "chrome/browser/search/search.h" 18 #include "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h" 19 #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h" 20 #include "chrome/browser/ui/cocoa/omnibox/omnibox_popup_view_mac.h" 21 #include "chrome/browser/ui/omnibox/omnibox_edit_controller.h" 22 #include "chrome/browser/ui/omnibox/omnibox_popup_model.h" 23 #include "chrome/browser/ui/toolbar/toolbar_model.h" 24 #include "content/public/browser/web_contents.h" 25 #include "extensions/common/constants.h" 26 #include "grit/generated_resources.h" 27 #include "grit/theme_resources.h" 28 #import "third_party/mozilla/NSPasteboard+Utils.h" 29 #import "ui/base/cocoa/cocoa_base_utils.h" 30 #include "ui/base/clipboard/clipboard.h" 31 #include "ui/base/resource/resource_bundle.h" 32 #include "ui/gfx/font.h" 33 #include "ui/gfx/font_list.h" 34 #include "ui/gfx/geometry/rect.h" 35 36 using content::WebContents; 37 38 // Focus-handling between |field_| and model() is a bit subtle. 39 // Other platforms detect change of focus, which is inconvenient 40 // without subclassing NSTextField (even with a subclass, the use of a 41 // field editor may complicate things). 42 // 43 // model() doesn't actually do anything when it gains focus, it just 44 // initializes. Visible activity happens only after the user edits. 45 // NSTextField delegate receives messages around starting and ending 46 // edits, so that suffices to catch focus changes. Since all calls 47 // into model() start from OmniboxViewMac, in the worst case 48 // we can add code to sync up the sense of focus as needed. 49 // 50 // I've added DCHECK(IsFirstResponder()) in the places which I believe 51 // should only be reachable when |field_| is being edited. If these 52 // fire, it probably means someone unexpected is calling into 53 // model(). 54 // 55 // Other platforms don't appear to have the sense of "key window" that 56 // Mac does (I believe their fields lose focus when the window loses 57 // focus). Rather than modifying focus outside the control's edit 58 // scope, when the window resigns key the autocomplete popup is 59 // closed. model() still believes it has focus, and the popup will 60 // be regenerated on the user's next edit. That seems to match how 61 // things work on other platforms. 62 63 namespace { 64 65 // TODO(shess): This is ugly, find a better way. Using it right now 66 // so that I can crib from gtk and still be able to see that I'm using 67 // the same values easily. 68 NSColor* ColorWithRGBBytes(int rr, int gg, int bb) { 69 DCHECK_LE(rr, 255); 70 DCHECK_LE(bb, 255); 71 DCHECK_LE(gg, 255); 72 return [NSColor colorWithCalibratedRed:static_cast<float>(rr)/255.0 73 green:static_cast<float>(gg)/255.0 74 blue:static_cast<float>(bb)/255.0 75 alpha:1.0]; 76 } 77 78 NSColor* HostTextColor() { 79 return [NSColor blackColor]; 80 } 81 NSColor* BaseTextColor() { 82 return [NSColor darkGrayColor]; 83 } 84 NSColor* SecureSchemeColor() { 85 return ColorWithRGBBytes(0x07, 0x95, 0x00); 86 } 87 NSColor* SecurityErrorSchemeColor() { 88 return ColorWithRGBBytes(0xa2, 0x00, 0x00); 89 } 90 91 const char kOmniboxViewMacStateKey[] = "OmniboxViewMacState"; 92 93 // Store's the model and view state across tab switches. 94 struct OmniboxViewMacState : public base::SupportsUserData::Data { 95 OmniboxViewMacState(const OmniboxEditModel::State model_state, 96 const bool has_focus, 97 const NSRange& selection) 98 : model_state(model_state), 99 has_focus(has_focus), 100 selection(selection) { 101 } 102 virtual ~OmniboxViewMacState() {} 103 104 const OmniboxEditModel::State model_state; 105 const bool has_focus; 106 const NSRange selection; 107 }; 108 109 // Accessors for storing and getting the state from the tab. 110 void StoreStateToTab(WebContents* tab, 111 OmniboxViewMacState* state) { 112 tab->SetUserData(kOmniboxViewMacStateKey, state); 113 } 114 const OmniboxViewMacState* GetStateFromTab(const WebContents* tab) { 115 return static_cast<OmniboxViewMacState*>( 116 tab->GetUserData(&kOmniboxViewMacStateKey)); 117 } 118 119 // Helper to make converting url ranges to NSRange easier to 120 // read. 121 NSRange ComponentToNSRange(const url::Component& component) { 122 return NSMakeRange(static_cast<NSInteger>(component.begin), 123 static_cast<NSInteger>(component.len)); 124 } 125 126 } // namespace 127 128 // static 129 NSImage* OmniboxViewMac::ImageForResource(int resource_id) { 130 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 131 return rb.GetNativeImageNamed(resource_id).ToNSImage(); 132 } 133 134 // static 135 NSColor* OmniboxViewMac::SuggestTextColor() { 136 return [NSColor colorWithCalibratedWhite:0.0 alpha:0.5]; 137 } 138 139 OmniboxViewMac::OmniboxViewMac(OmniboxEditController* controller, 140 Profile* profile, 141 CommandUpdater* command_updater, 142 AutocompleteTextField* field) 143 : OmniboxView(profile, controller, command_updater), 144 popup_view_(new OmniboxPopupViewMac(this, model(), field)), 145 field_(field), 146 saved_temporary_selection_(NSMakeRange(0, 0)), 147 selection_before_change_(NSMakeRange(0, 0)), 148 marked_range_before_change_(NSMakeRange(0, 0)), 149 delete_was_pressed_(false), 150 delete_at_end_pressed_(false), 151 in_coalesced_update_block_(false), 152 do_coalesced_text_update_(false), 153 do_coalesced_range_update_(false) { 154 [field_ setObserver:this]; 155 156 // Needed so that editing doesn't lose the styling. 157 [field_ setAllowsEditingTextAttributes:YES]; 158 159 // Get the appropriate line height for the font that we use. 160 base::scoped_nsobject<NSLayoutManager> layoutManager( 161 [[NSLayoutManager alloc] init]); 162 [layoutManager setUsesScreenFonts:YES]; 163 } 164 165 OmniboxViewMac::~OmniboxViewMac() { 166 // Destroy popup view before this object in case it tries to call us 167 // back in the destructor. 168 popup_view_.reset(); 169 170 // Disconnect from |field_|, it outlives this object. 171 [field_ setObserver:NULL]; 172 } 173 174 void OmniboxViewMac::SaveStateToTab(WebContents* tab) { 175 DCHECK(tab); 176 177 const bool hasFocus = [field_ currentEditor] ? true : false; 178 179 NSRange range; 180 if (hasFocus) { 181 range = GetSelectedRange(); 182 } else { 183 // If we are not focussed, there is no selection. Manufacture 184 // something reasonable in case it starts to matter in the future. 185 range = NSMakeRange(0, GetTextLength()); 186 } 187 188 OmniboxViewMacState* state = 189 new OmniboxViewMacState(model()->GetStateForTabSwitch(), hasFocus, range); 190 StoreStateToTab(tab, state); 191 } 192 193 void OmniboxViewMac::OnTabChanged(const WebContents* web_contents) { 194 const OmniboxViewMacState* state = GetStateFromTab(web_contents); 195 model()->RestoreState(state ? &state->model_state : NULL); 196 // Restore focus and selection if they were present when the tab 197 // was switched away. 198 if (state && state->has_focus) { 199 // TODO(shess): Unfortunately, there is no safe way to update 200 // this because TabStripController -selectTabWithContents:* is 201 // also messing with focus. Both parties need to agree to 202 // store existing state before anyone tries to setup the new 203 // state. Anyhow, it would look something like this. 204 #if 0 205 [[field_ window] makeFirstResponder:field_]; 206 [[field_ currentEditor] setSelectedRange:state->selection]; 207 #endif 208 } 209 } 210 211 void OmniboxViewMac::Update() { 212 if (chrome::ShouldDisplayOriginChip()) { 213 NSDictionary* placeholder_attributes = @{ 214 NSFontAttributeName : GetFieldFont(gfx::Font::NORMAL), 215 NSForegroundColorAttributeName : [NSColor disabledControlTextColor] 216 }; 217 base::scoped_nsobject<NSMutableAttributedString> placeholder_text( 218 [[NSMutableAttributedString alloc] 219 initWithString:base::SysUTF16ToNSString(GetHintText()) 220 attributes:placeholder_attributes]); 221 [[field_ cell] setPlaceholderAttributedString:placeholder_text]; 222 } 223 if (model()->UpdatePermanentText()) { 224 // Something visibly changed. Re-enable URL replacement. 225 controller()->GetToolbarModel()->set_url_replacement_enabled(true); 226 model()->UpdatePermanentText(); 227 228 // Restore everything to the baseline look. 229 RevertAll(); 230 231 // TODO(shess): Figure out how this case is used, to make sure 232 // we're getting the selection and popup right. 233 } else { 234 // TODO(shess): This corresponds to _win and _gtk, except those 235 // guard it with a test for whether the security level changed. 236 // But AFAICT, that can only change if the text changed, and that 237 // code compares the toolbar model security level with the local 238 // security level. Dig in and figure out why this isn't a no-op 239 // that should go away. 240 EmphasizeURLComponents(); 241 } 242 } 243 244 void OmniboxViewMac::OpenMatch(const AutocompleteMatch& match, 245 WindowOpenDisposition disposition, 246 const GURL& alternate_nav_url, 247 const base::string16& pasted_text, 248 size_t selected_line) { 249 // Coalesce text and selection updates from the following function. If we 250 // don't do this, the user may see intermediate states as brief flickers. 251 in_coalesced_update_block_ = true; 252 OmniboxView::OpenMatch( 253 match, disposition, alternate_nav_url, pasted_text, selected_line); 254 in_coalesced_update_block_ = false; 255 if (do_coalesced_text_update_) 256 SetText(coalesced_text_update_); 257 do_coalesced_text_update_ = false; 258 if (do_coalesced_range_update_) 259 SetSelectedRange(coalesced_range_update_); 260 do_coalesced_range_update_ = false; 261 } 262 263 base::string16 OmniboxViewMac::GetText() const { 264 return base::SysNSStringToUTF16([field_ stringValue]); 265 } 266 267 NSRange OmniboxViewMac::GetSelectedRange() const { 268 return [[field_ currentEditor] selectedRange]; 269 } 270 271 NSRange OmniboxViewMac::GetMarkedRange() const { 272 DCHECK([field_ currentEditor]); 273 return [(NSTextView*)[field_ currentEditor] markedRange]; 274 } 275 276 void OmniboxViewMac::SetSelectedRange(const NSRange range) { 277 if (in_coalesced_update_block_) { 278 do_coalesced_range_update_ = true; 279 coalesced_range_update_ = range; 280 return; 281 } 282 283 // This can be called when we don't have focus. For instance, when 284 // the user clicks the "Go" button. 285 if (model()->has_focus()) { 286 // TODO(shess): If model() thinks we have focus, this should not 287 // be necessary. Try to convert to DCHECK(IsFirstResponder()). 288 if (![field_ currentEditor]) { 289 [[field_ window] makeFirstResponder:field_]; 290 } 291 292 // TODO(shess): What if it didn't get first responder, and there is 293 // no field editor? This will do nothing. Well, at least it won't 294 // crash. Think of something more productive to do, or prove that 295 // it cannot occur and DCHECK appropriately. 296 [[field_ currentEditor] setSelectedRange:range]; 297 } 298 } 299 300 void OmniboxViewMac::SetWindowTextAndCaretPos(const base::string16& text, 301 size_t caret_pos, 302 bool update_popup, 303 bool notify_text_changed) { 304 DCHECK_LE(caret_pos, text.size()); 305 SetTextAndSelectedRange(text, NSMakeRange(caret_pos, 0)); 306 307 if (update_popup) 308 UpdatePopup(); 309 310 if (notify_text_changed) 311 TextChanged(); 312 } 313 314 void OmniboxViewMac::SetForcedQuery() { 315 // We need to do this first, else |SetSelectedRange()| won't work. 316 FocusLocation(true); 317 318 const base::string16 current_text(GetText()); 319 const size_t start = current_text.find_first_not_of(base::kWhitespaceUTF16); 320 if (start == base::string16::npos || (current_text[start] != '?')) { 321 SetUserText(base::ASCIIToUTF16("?")); 322 } else { 323 NSRange range = NSMakeRange(start + 1, current_text.size() - start - 1); 324 [[field_ currentEditor] setSelectedRange:range]; 325 } 326 } 327 328 bool OmniboxViewMac::IsSelectAll() const { 329 if (![field_ currentEditor]) 330 return true; 331 const NSRange all_range = NSMakeRange(0, GetTextLength()); 332 return NSEqualRanges(all_range, GetSelectedRange()); 333 } 334 335 bool OmniboxViewMac::DeleteAtEndPressed() { 336 return delete_at_end_pressed_; 337 } 338 339 void OmniboxViewMac::GetSelectionBounds(base::string16::size_type* start, 340 base::string16::size_type* end) const { 341 if (![field_ currentEditor]) { 342 *start = *end = 0; 343 return; 344 } 345 346 const NSRange selected_range = GetSelectedRange(); 347 *start = static_cast<size_t>(selected_range.location); 348 *end = static_cast<size_t>(NSMaxRange(selected_range)); 349 } 350 351 void OmniboxViewMac::SelectAll(bool reversed) { 352 // TODO(shess): Figure out what |reversed| implies. The gtk version 353 // has it imply inverting the selection front to back, but I don't 354 // even know if that makes sense for Mac. 355 356 // TODO(shess): Verify that we should be stealing focus at this 357 // point. 358 SetSelectedRange(NSMakeRange(0, GetTextLength())); 359 } 360 361 void OmniboxViewMac::RevertAll() { 362 OmniboxView::RevertAll(); 363 [field_ clearUndoChain]; 364 } 365 366 void OmniboxViewMac::UpdatePopup() { 367 model()->SetInputInProgress(true); 368 if (!model()->has_focus()) 369 return; 370 371 // Comment copied from OmniboxViewWin::UpdatePopup(): 372 // Don't inline autocomplete when: 373 // * The user is deleting text 374 // * The caret/selection isn't at the end of the text 375 // * The user has just pasted in something that replaced all the text 376 // * The user is trying to compose something in an IME 377 bool prevent_inline_autocomplete = IsImeComposing(); 378 NSTextView* editor = (NSTextView*)[field_ currentEditor]; 379 if (editor) { 380 if (NSMaxRange([editor selectedRange]) < [[editor textStorage] length]) 381 prevent_inline_autocomplete = true; 382 } 383 384 model()->StartAutocomplete([editor selectedRange].length != 0, 385 prevent_inline_autocomplete); 386 } 387 388 void OmniboxViewMac::CloseOmniboxPopup() { 389 // Call both base class methods. 390 ClosePopup(); 391 OmniboxView::CloseOmniboxPopup(); 392 } 393 394 void OmniboxViewMac::SetFocus() { 395 FocusLocation(false); 396 model()->SetCaretVisibility(true); 397 } 398 399 void OmniboxViewMac::ApplyCaretVisibility() { 400 [[field_ cell] setHideFocusState:!model()->is_caret_visible() 401 ofView:field_]; 402 } 403 404 void OmniboxViewMac::SetText(const base::string16& display_text) { 405 SetTextInternal(display_text); 406 } 407 408 void OmniboxViewMac::SetTextInternal(const base::string16& display_text) { 409 if (in_coalesced_update_block_) { 410 do_coalesced_text_update_ = true; 411 coalesced_text_update_ = display_text; 412 // Don't do any selection changes, since they apply to the previous text. 413 do_coalesced_range_update_ = false; 414 return; 415 } 416 417 NSString* ss = base::SysUTF16ToNSString(display_text); 418 NSMutableAttributedString* as = 419 [[[NSMutableAttributedString alloc] initWithString:ss] autorelease]; 420 421 ApplyTextAttributes(display_text, as); 422 [field_ setAttributedStringValue:as]; 423 424 // TODO(shess): This may be an appropriate place to call: 425 // model()->OnChanged(); 426 // In the current implementation, this tells LocationBarViewMac to 427 // mess around with model() and update |field_|. Unfortunately, 428 // when I look at our peer implementations, it's not entirely clear 429 // to me if this is safe. SetTextInternal() is sort of an utility method, 430 // and different callers sometimes have different needs. Research 431 // this issue so that it can be added safely. 432 433 // TODO(shess): Also, consider whether this code couldn't just 434 // manage things directly. Windows uses a series of overlaid view 435 // objects to accomplish the hinting stuff that OnChanged() does, so 436 // it makes sense to have it in the controller that lays those 437 // things out. Mac instead pushes the support into a custom 438 // text-field implementation. 439 } 440 441 void OmniboxViewMac::SetTextAndSelectedRange(const base::string16& display_text, 442 const NSRange range) { 443 SetText(display_text); 444 SetSelectedRange(range); 445 } 446 447 void OmniboxViewMac::EmphasizeURLComponents() { 448 NSTextView* editor = (NSTextView*)[field_ currentEditor]; 449 // If the autocomplete text field is in editing mode, then we can just change 450 // its attributes through its editor. Otherwise, we simply reset its content. 451 if (editor) { 452 NSTextStorage* storage = [editor textStorage]; 453 [storage beginEditing]; 454 455 // Clear the existing attributes from the text storage, then 456 // overlay the appropriate Omnibox attributes. 457 [storage setAttributes:[NSDictionary dictionary] 458 range:NSMakeRange(0, [storage length])]; 459 ApplyTextAttributes(GetText(), storage); 460 461 [storage endEditing]; 462 463 // This function can be called during the editor's -resignFirstResponder. If 464 // that happens, |storage| and |field_| will not be synced automatically any 465 // more. Calling -stringValue ensures that |field_| reflects the changes to 466 // |storage|. 467 [field_ stringValue]; 468 } else { 469 SetText(GetText()); 470 } 471 } 472 473 void OmniboxViewMac::ApplyTextAttributes(const base::string16& display_text, 474 NSMutableAttributedString* as) { 475 NSUInteger as_length = [as length]; 476 NSRange as_entire_string = NSMakeRange(0, as_length); 477 478 [as addAttribute:NSFontAttributeName value:GetFieldFont(gfx::Font::NORMAL) 479 range:as_entire_string]; 480 481 // A kinda hacky way to add breaking at periods. This is what Safari does. 482 // This works for IDNs too, despite the "en_US". 483 [as addAttribute:@"NSLanguage" value:@"en_US_POSIX" 484 range:as_entire_string]; 485 486 // Make a paragraph style locking in the standard line height as the maximum, 487 // otherwise the baseline may shift "downwards". 488 base::scoped_nsobject<NSMutableParagraphStyle> paragraph_style( 489 [[NSMutableParagraphStyle alloc] init]); 490 CGFloat line_height = [[field_ cell] lineHeight]; 491 [paragraph_style setMaximumLineHeight:line_height]; 492 [paragraph_style setMinimumLineHeight:line_height]; 493 [paragraph_style setLineBreakMode:NSLineBreakByTruncatingTail]; 494 [as addAttribute:NSParagraphStyleAttributeName value:paragraph_style 495 range:as_entire_string]; 496 497 url::Component scheme, host; 498 AutocompleteInput::ParseForEmphasizeComponents( 499 display_text, &scheme, &host); 500 bool grey_out_url = display_text.substr(scheme.begin, scheme.len) == 501 base::UTF8ToUTF16(extensions::kExtensionScheme); 502 if (model()->CurrentTextIsURL() && 503 (host.is_nonempty() || grey_out_url)) { 504 [as addAttribute:NSForegroundColorAttributeName value:BaseTextColor() 505 range:as_entire_string]; 506 507 if (!grey_out_url) { 508 [as addAttribute:NSForegroundColorAttributeName value:HostTextColor() 509 range:ComponentToNSRange(host)]; 510 } 511 } 512 513 // TODO(shess): GTK has this as a member var, figure out why. 514 // [Could it be to not change if no change? If so, I'm guessing 515 // AppKit may already handle that.] 516 const ToolbarModel::SecurityLevel security_level = 517 controller()->GetToolbarModel()->GetSecurityLevel(false); 518 519 // Emphasize the scheme for security UI display purposes (if necessary). 520 if (!model()->user_input_in_progress() && model()->CurrentTextIsURL() && 521 scheme.is_nonempty() && (security_level != ToolbarModel::NONE)) { 522 NSColor* color; 523 if (security_level == ToolbarModel::EV_SECURE || 524 security_level == ToolbarModel::SECURE) { 525 color = SecureSchemeColor(); 526 } else if (security_level == ToolbarModel::SECURITY_ERROR) { 527 color = SecurityErrorSchemeColor(); 528 // Add a strikethrough through the scheme. 529 [as addAttribute:NSStrikethroughStyleAttributeName 530 value:[NSNumber numberWithInt:NSUnderlineStyleSingle] 531 range:ComponentToNSRange(scheme)]; 532 } else if (security_level == ToolbarModel::SECURITY_WARNING) { 533 color = BaseTextColor(); 534 } else { 535 NOTREACHED(); 536 color = BaseTextColor(); 537 } 538 [as addAttribute:NSForegroundColorAttributeName value:color 539 range:ComponentToNSRange(scheme)]; 540 } 541 } 542 543 void OmniboxViewMac::OnTemporaryTextMaybeChanged( 544 const base::string16& display_text, 545 bool save_original_selection, 546 bool notify_text_changed) { 547 if (save_original_selection) 548 saved_temporary_selection_ = GetSelectedRange(); 549 550 SetWindowTextAndCaretPos(display_text, display_text.size(), false, false); 551 if (notify_text_changed) 552 model()->OnChanged(); 553 [field_ clearUndoChain]; 554 } 555 556 bool OmniboxViewMac::OnInlineAutocompleteTextMaybeChanged( 557 const base::string16& display_text, 558 size_t user_text_length) { 559 // TODO(shess): Make sure that this actually works. The round trip 560 // to native form and back may mean that it's the same but not the 561 // same. 562 if (display_text == GetText()) 563 return false; 564 565 DCHECK_LE(user_text_length, display_text.size()); 566 const NSRange range = 567 NSMakeRange(user_text_length, display_text.size() - user_text_length); 568 SetTextAndSelectedRange(display_text, range); 569 model()->OnChanged(); 570 [field_ clearUndoChain]; 571 572 return true; 573 } 574 575 void OmniboxViewMac::OnInlineAutocompleteTextCleared() { 576 } 577 578 void OmniboxViewMac::OnRevertTemporaryText() { 579 SetSelectedRange(saved_temporary_selection_); 580 // We got here because the user hit the Escape key. We explicitly don't call 581 // TextChanged(), since OmniboxPopupModel::ResetToDefaultMatch() has already 582 // been called by now, and it would've called TextChanged() if it was 583 // warranted. 584 } 585 586 bool OmniboxViewMac::IsFirstResponder() const { 587 return [field_ currentEditor] != nil ? true : false; 588 } 589 590 void OmniboxViewMac::OnBeforePossibleChange() { 591 // We should only arrive here when the field is focussed. 592 DCHECK(IsFirstResponder()); 593 594 selection_before_change_ = GetSelectedRange(); 595 text_before_change_ = GetText(); 596 marked_range_before_change_ = GetMarkedRange(); 597 } 598 599 bool OmniboxViewMac::OnAfterPossibleChange() { 600 // We should only arrive here when the field is focussed. 601 DCHECK(IsFirstResponder()); 602 603 const NSRange new_selection(GetSelectedRange()); 604 const base::string16 new_text(GetText()); 605 const size_t length = new_text.length(); 606 607 const bool selection_differs = 608 (new_selection.length || selection_before_change_.length) && 609 !NSEqualRanges(new_selection, selection_before_change_); 610 const bool at_end_of_edit = (length == new_selection.location); 611 const bool text_differs = (new_text != text_before_change_) || 612 !NSEqualRanges(marked_range_before_change_, GetMarkedRange()); 613 614 // When the user has deleted text, we don't allow inline 615 // autocomplete. This is assumed if the text has gotten shorter AND 616 // the selection has shifted towards the front of the text. During 617 // normal typing the text will almost always be shorter (as the new 618 // input replaces the autocomplete suggestion), but in that case the 619 // selection point will have moved towards the end of the text. 620 // TODO(shess): In our implementation, we can catch -deleteBackward: 621 // and other methods to provide positive knowledge that a delete 622 // occured, rather than intuiting it from context. Consider whether 623 // that would be a stronger approach. 624 const bool just_deleted_text = 625 (length < text_before_change_.length() && 626 new_selection.location <= selection_before_change_.location); 627 628 delete_at_end_pressed_ = false; 629 630 const bool something_changed = model()->OnAfterPossibleChange( 631 text_before_change_, new_text, new_selection.location, 632 NSMaxRange(new_selection), selection_differs, text_differs, 633 just_deleted_text, !IsImeComposing()); 634 635 if (delete_was_pressed_ && at_end_of_edit) 636 delete_at_end_pressed_ = true; 637 638 // Restyle in case the user changed something. 639 // TODO(shess): I believe there are multiple-redraw cases, here. 640 // Linux watches for something_changed && text_differs, but that 641 // fails for us in case you copy the URL and paste the identical URL 642 // back (we'll lose the styling). 643 TextChanged(); 644 645 delete_was_pressed_ = false; 646 647 return something_changed; 648 } 649 650 gfx::NativeView OmniboxViewMac::GetNativeView() const { 651 return field_; 652 } 653 654 gfx::NativeView OmniboxViewMac::GetRelativeWindowForPopup() const { 655 // Not used on mac. 656 NOTREACHED(); 657 return NULL; 658 } 659 660 void OmniboxViewMac::SetGrayTextAutocompletion( 661 const base::string16& suggest_text) { 662 if (suggest_text == suggest_text_) 663 return; 664 suggest_text_ = suggest_text; 665 [field_ setGrayTextAutocompletion:base::SysUTF16ToNSString(suggest_text) 666 textColor:SuggestTextColor()]; 667 } 668 669 base::string16 OmniboxViewMac::GetGrayTextAutocompletion() const { 670 return suggest_text_; 671 } 672 673 int OmniboxViewMac::GetTextWidth() const { 674 // Not used on mac. 675 NOTREACHED(); 676 return 0; 677 } 678 679 int OmniboxViewMac::GetWidth() const { 680 return ceil([field_ bounds].size.width); 681 } 682 683 bool OmniboxViewMac::IsImeComposing() const { 684 return [(NSTextView*)[field_ currentEditor] hasMarkedText]; 685 } 686 687 void OmniboxViewMac::OnDidBeginEditing() { 688 // We should only arrive here when the field is focussed. 689 DCHECK([field_ currentEditor]); 690 } 691 692 void OmniboxViewMac::OnBeforeChange() { 693 // Capture the current state. 694 OnBeforePossibleChange(); 695 } 696 697 void OmniboxViewMac::OnDidChange() { 698 // Figure out what changed and notify the model. 699 OnAfterPossibleChange(); 700 } 701 702 void OmniboxViewMac::OnDidEndEditing() { 703 ClosePopup(); 704 } 705 706 bool OmniboxViewMac::OnDoCommandBySelector(SEL cmd) { 707 if (cmd == @selector(deleteForward:)) 708 delete_was_pressed_ = true; 709 710 if (cmd == @selector(moveDown:)) { 711 model()->OnUpOrDownKeyPressed(1); 712 return true; 713 } 714 715 if (cmd == @selector(moveUp:)) { 716 model()->OnUpOrDownKeyPressed(-1); 717 return true; 718 } 719 720 if (model()->popup_model()->IsOpen()) { 721 if (cmd == @selector(insertBacktab:)) { 722 if (model()->popup_model()->selected_line_state() == 723 OmniboxPopupModel::KEYWORD) { 724 model()->ClearKeyword(GetText()); 725 return true; 726 } else { 727 model()->OnUpOrDownKeyPressed(-1); 728 return true; 729 } 730 } 731 732 if ((cmd == @selector(insertTab:) || 733 cmd == @selector(insertTabIgnoringFieldEditor:)) && 734 !model()->is_keyword_hint()) { 735 model()->OnUpOrDownKeyPressed(1); 736 return true; 737 } 738 } 739 740 if (cmd == @selector(moveRight:)) { 741 // Only commit suggested text if the cursor is all the way to the right and 742 // there is no selection. 743 if (suggest_text_.length() > 0 && IsCaretAtEnd()) { 744 model()->CommitSuggestedText(); 745 return true; 746 } 747 } 748 749 if (cmd == @selector(scrollPageDown:)) { 750 model()->OnUpOrDownKeyPressed(model()->result().size()); 751 return true; 752 } 753 754 if (cmd == @selector(scrollPageUp:)) { 755 model()->OnUpOrDownKeyPressed(-model()->result().size()); 756 return true; 757 } 758 759 if (cmd == @selector(cancelOperation:)) { 760 return model()->OnEscapeKeyPressed(); 761 } 762 763 if ((cmd == @selector(insertTab:) || 764 cmd == @selector(insertTabIgnoringFieldEditor:)) && 765 model()->is_keyword_hint()) { 766 return model()->AcceptKeyword(ENTERED_KEYWORD_MODE_VIA_TAB); 767 } 768 769 // |-noop:| is sent when the user presses Cmd+Return. Override the no-op 770 // behavior with the proper WindowOpenDisposition. 771 NSEvent* event = [NSApp currentEvent]; 772 if (cmd == @selector(insertNewline:) || 773 (cmd == @selector(noop:) && 774 ([event type] == NSKeyDown || [event type] == NSKeyUp) && 775 [event keyCode] == kVK_Return)) { 776 WindowOpenDisposition disposition = 777 ui::WindowOpenDispositionFromNSEvent(event); 778 model()->AcceptInput(disposition, false); 779 // Opening a URL in a background tab should also revert the omnibox contents 780 // to their original state. We cannot do a blanket revert in OpenURL() 781 // because middle-clicks also open in a new background tab, but those should 782 // not revert the omnibox text. 783 RevertAll(); 784 return true; 785 } 786 787 // Option-Return 788 if (cmd == @selector(insertNewlineIgnoringFieldEditor:)) { 789 model()->AcceptInput(NEW_FOREGROUND_TAB, false); 790 return true; 791 } 792 793 // When the user does Control-Enter, the existing content has "www." 794 // prepended and ".com" appended. model() should already have 795 // received notification when the Control key was depressed, but it 796 // is safe to tell it twice. 797 if (cmd == @selector(insertLineBreak:)) { 798 OnControlKeyChanged(true); 799 WindowOpenDisposition disposition = 800 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); 801 model()->AcceptInput(disposition, false); 802 return true; 803 } 804 805 if (cmd == @selector(deleteBackward:)) { 806 if (OnBackspacePressed()) { 807 return true; 808 } 809 } 810 811 if (cmd == @selector(deleteForward:)) { 812 const NSUInteger modifiers = [[NSApp currentEvent] modifierFlags]; 813 if ((modifiers & NSShiftKeyMask) != 0) { 814 if (model()->popup_model()->IsOpen()) { 815 model()->popup_model()->TryDeletingCurrentItem(); 816 return true; 817 } 818 } 819 } 820 821 return false; 822 } 823 824 void OmniboxViewMac::OnSetFocus(bool control_down) { 825 model()->OnSetFocus(control_down); 826 controller()->OnSetFocus(); 827 828 HandleOriginChipMouseRelease(); 829 } 830 831 void OmniboxViewMac::OnKillFocus() { 832 // Tell the model to reset itself. 833 model()->OnWillKillFocus(NULL); 834 model()->OnKillFocus(); 835 836 OnDidKillFocus(); 837 } 838 839 void OmniboxViewMac::OnMouseDown(NSInteger button_number) { 840 // Restore caret visibility whenever the user clicks in the the omnibox. This 841 // is not always covered by OnSetFocus() because when clicking while the 842 // omnibox has invisible focus does not trigger a new OnSetFocus() call. 843 if (button_number == 0 || button_number == 1) 844 model()->SetCaretVisibility(true); 845 } 846 847 bool OmniboxViewMac::ShouldSelectAllOnMouseDown() { 848 return !controller()->GetToolbarModel()->WouldPerformSearchTermReplacement( 849 false); 850 } 851 852 bool OmniboxViewMac::CanCopy() { 853 const NSRange selection = GetSelectedRange(); 854 return selection.length > 0; 855 } 856 857 void OmniboxViewMac::CopyToPasteboard(NSPasteboard* pb) { 858 DCHECK(CanCopy()); 859 860 const NSRange selection = GetSelectedRange(); 861 base::string16 text = base::SysNSStringToUTF16( 862 [[field_ stringValue] substringWithRange:selection]); 863 864 // Copy the URL unless this is the search URL and it's being replaced by the 865 // Extended Instant API. 866 GURL url; 867 bool write_url = false; 868 if (!controller()->GetToolbarModel()->WouldPerformSearchTermReplacement( 869 false)) { 870 model()->AdjustTextForCopy(selection.location, IsSelectAll(), &text, &url, 871 &write_url); 872 } 873 874 if (IsSelectAll()) 875 UMA_HISTOGRAM_COUNTS(OmniboxEditModel::kCutOrCopyAllTextHistogram, 1); 876 877 NSString* nstext = base::SysUTF16ToNSString(text); 878 [pb declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; 879 [pb setString:nstext forType:NSStringPboardType]; 880 881 if (write_url) { 882 [pb declareURLPasteboardWithAdditionalTypes:[NSArray array] owner:nil]; 883 [pb setDataForURL:base::SysUTF8ToNSString(url.spec()) title:nstext]; 884 } 885 } 886 887 void OmniboxViewMac::ShowURL() { 888 DCHECK(ShouldEnableShowURL()); 889 OmniboxView::ShowURL(); 890 } 891 892 void OmniboxViewMac::OnPaste() { 893 // This code currently expects |field_| to be focussed. 894 DCHECK([field_ currentEditor]); 895 896 base::string16 text = GetClipboardText(); 897 if (text.empty()) { 898 return; 899 } 900 NSString* s = base::SysUTF16ToNSString(text); 901 902 // -shouldChangeTextInRange:* and -didChangeText are documented in 903 // NSTextView as things you need to do if you write additional 904 // user-initiated editing functions. They cause the appropriate 905 // delegate methods to be called. 906 // TODO(shess): It would be nice to separate the Cocoa-specific code 907 // from the Chrome-specific code. 908 NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]); 909 const NSRange selectedRange = GetSelectedRange(); 910 if ([editor shouldChangeTextInRange:selectedRange replacementString:s]) { 911 // Record this paste, so we can do different behavior. 912 model()->OnPaste(); 913 914 // Force a Paste operation to trigger the text_changed code in 915 // OnAfterPossibleChange(), even if identical contents are pasted 916 // into the text box. 917 text_before_change_.clear(); 918 919 [editor replaceCharactersInRange:selectedRange withString:s]; 920 [editor didChangeText]; 921 } 922 } 923 924 // TODO(dominich): Move to OmniboxView base class? Currently this is defined on 925 // the AutocompleteTextFieldObserver but the logic is shared between all 926 // platforms. Some refactor might be necessary to simplify this. Or at least 927 // this method could call the OmniboxView version. 928 bool OmniboxViewMac::ShouldEnableShowURL() { 929 return controller()->GetToolbarModel()->WouldReplaceURL(); 930 } 931 932 bool OmniboxViewMac::CanPasteAndGo() { 933 return model()->CanPasteAndGo(GetClipboardText()); 934 } 935 936 int OmniboxViewMac::GetPasteActionStringId() { 937 base::string16 text(GetClipboardText()); 938 DCHECK(model()->CanPasteAndGo(text)); 939 return model()->IsPasteAndSearch(text) ? 940 IDS_PASTE_AND_SEARCH : IDS_PASTE_AND_GO; 941 } 942 943 void OmniboxViewMac::OnPasteAndGo() { 944 base::string16 text(GetClipboardText()); 945 if (model()->CanPasteAndGo(text)) 946 model()->PasteAndGo(text); 947 } 948 949 void OmniboxViewMac::OnFrameChanged() { 950 // TODO(shess): UpdatePopupAppearance() is called frequently, so it 951 // should be really cheap, but in this case we could probably make 952 // things even cheaper by refactoring between the popup-placement 953 // code and the matrix-population code. 954 popup_view_->UpdatePopupAppearance(); 955 956 // Give controller a chance to rearrange decorations. 957 model()->OnChanged(); 958 } 959 960 void OmniboxViewMac::ClosePopup() { 961 OmniboxView::CloseOmniboxPopup(); 962 } 963 964 bool OmniboxViewMac::OnBackspacePressed() { 965 // Don't intercept if not in keyword search mode. 966 if (model()->is_keyword_hint() || model()->keyword().empty()) { 967 return false; 968 } 969 970 // Don't intercept if there is a selection, or the cursor isn't at 971 // the leftmost position. 972 const NSRange selection = GetSelectedRange(); 973 if (selection.length > 0 || selection.location > 0) { 974 return false; 975 } 976 977 // We're showing a keyword and the user pressed backspace at the 978 // beginning of the text. Delete the selected keyword. 979 model()->ClearKeyword(GetText()); 980 return true; 981 } 982 983 NSRange OmniboxViewMac::SelectionRangeForProposedRange(NSRange proposed_range) { 984 return proposed_range; 985 } 986 987 void OmniboxViewMac::OnControlKeyChanged(bool pressed) { 988 model()->OnControlKeyChanged(pressed); 989 } 990 991 void OmniboxViewMac::FocusLocation(bool select_all) { 992 if ([field_ isEditable]) { 993 // If the text field has a field editor, it's the first responder, meaning 994 // that it's already focused. makeFirstResponder: will select all, so only 995 // call it if this behavior is desired. 996 if (select_all || ![field_ currentEditor]) 997 [[field_ window] makeFirstResponder:field_]; 998 DCHECK_EQ([field_ currentEditor], [[field_ window] firstResponder]); 999 } 1000 } 1001 1002 // static 1003 NSFont* OmniboxViewMac::GetFieldFont(int style) { 1004 // This value should be kept in sync with InstantPage::InitializeFonts. 1005 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 1006 return rb.GetFontList(ui::ResourceBundle::BaseFont).Derive(1, style) 1007 .GetPrimaryFont().GetNativeFont(); 1008 } 1009 1010 int OmniboxViewMac::GetOmniboxTextLength() const { 1011 return static_cast<int>(GetTextLength()); 1012 } 1013 1014 NSUInteger OmniboxViewMac::GetTextLength() const { 1015 return [field_ currentEditor] ? [[[field_ currentEditor] string] length] : 1016 [[field_ stringValue] length]; 1017 } 1018 1019 bool OmniboxViewMac::IsCaretAtEnd() const { 1020 const NSRange selection = GetSelectedRange(); 1021 return NSMaxRange(selection) == GetTextLength(); 1022 } 1023