// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/autocomplete/autocomplete_edit_view_mac.h" #include <Carbon/Carbon.h> // kVK_Return #include "app/mac/nsimage_cache.h" #include "base/string_util.h" #include "base/sys_string_conversions.h" #include "base/utf_string_conversions.h" #include "chrome/browser/autocomplete/autocomplete_edit.h" #include "chrome/browser/autocomplete/autocomplete_match.h" #include "chrome/browser/autocomplete/autocomplete_popup_model.h" #include "chrome/browser/autocomplete/autocomplete_popup_view_mac.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/ui/cocoa/event_utils.h" #include "chrome/browser/ui/toolbar/toolbar_model.h" #include "content/browser/tab_contents/tab_contents.h" #include "grit/generated_resources.h" #include "grit/theme_resources.h" #include "net/base/escape.h" #import "third_party/mozilla/NSPasteboard+Utils.h" #include "ui/base/clipboard/clipboard.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/image.h" #include "ui/gfx/rect.h" // Focus-handling between |field_| and |model_| is a bit subtle. // Other platforms detect change of focus, which is inconvenient // without subclassing NSTextField (even with a subclass, the use of a // field editor may complicate things). // // |model_| doesn't actually do anything when it gains focus, it just // initializes. Visible activity happens only after the user edits. // NSTextField delegate receives messages around starting and ending // edits, so that suffices to catch focus changes. Since all calls // into |model_| start from AutocompleteEditViewMac, in the worst case // we can add code to sync up the sense of focus as needed. // // I've added DCHECK(IsFirstResponder()) in the places which I believe // should only be reachable when |field_| is being edited. If these // fire, it probably means someone unexpected is calling into // |model_|. // // Other platforms don't appear to have the sense of "key window" that // Mac does (I believe their fields lose focus when the window loses // focus). Rather than modifying focus outside the control's edit // scope, when the window resigns key the autocomplete popup is // closed. |model_| still believes it has focus, and the popup will // be regenerated on the user's next edit. That seems to match how // things work on other platforms. namespace { // TODO(shess): This is ugly, find a better way. Using it right now // so that I can crib from gtk and still be able to see that I'm using // the same values easily. NSColor* ColorWithRGBBytes(int rr, int gg, int bb) { DCHECK_LE(rr, 255); DCHECK_LE(bb, 255); DCHECK_LE(gg, 255); return [NSColor colorWithCalibratedRed:static_cast<float>(rr)/255.0 green:static_cast<float>(gg)/255.0 blue:static_cast<float>(bb)/255.0 alpha:1.0]; } NSColor* HostTextColor() { return [NSColor blackColor]; } NSColor* BaseTextColor() { return [NSColor darkGrayColor]; } NSColor* SuggestTextColor() { return [NSColor grayColor]; } NSColor* SecureSchemeColor() { return ColorWithRGBBytes(0x07, 0x95, 0x00); } NSColor* SecurityErrorSchemeColor() { return ColorWithRGBBytes(0xa2, 0x00, 0x00); } // Store's the model and view state across tab switches. struct AutocompleteEditViewMacState { AutocompleteEditViewMacState(const AutocompleteEditModel::State model_state, const bool has_focus, const NSRange& selection) : model_state(model_state), has_focus(has_focus), selection(selection) { } const AutocompleteEditModel::State model_state; const bool has_focus; const NSRange selection; }; // Returns a lazily initialized property bag accessor for saving our // state in a TabContents. When constructed |accessor| generates a // globally-unique id used to index into the per-tab PropertyBag used // to store the state data. PropertyAccessor<AutocompleteEditViewMacState>* GetStateAccessor() { static PropertyAccessor<AutocompleteEditViewMacState> accessor; return &accessor; } // Accessors for storing and getting the state from the tab. void StoreStateToTab(TabContents* tab, const AutocompleteEditViewMacState& state) { GetStateAccessor()->SetProperty(tab->property_bag(), state); } const AutocompleteEditViewMacState* GetStateFromTab(const TabContents* tab) { return GetStateAccessor()->GetProperty(tab->property_bag()); } // Helper to make converting url_parse ranges to NSRange easier to // read. NSRange ComponentToNSRange(const url_parse::Component& component) { return NSMakeRange(static_cast<NSInteger>(component.begin), static_cast<NSInteger>(component.len)); } } // namespace // static NSImage* AutocompleteEditViewMac::ImageForResource(int resource_id) { NSString* image_name = nil; switch(resource_id) { // From the autocomplete popup, or the star icon at the RHS of the // text field. case IDR_STAR: image_name = @"star.pdf"; break; case IDR_STAR_LIT: image_name = @"star_lit.pdf"; break; // Values from |AutocompleteMatch::TypeToIcon()|. case IDR_OMNIBOX_SEARCH: image_name = @"omnibox_search.pdf"; break; case IDR_OMNIBOX_HTTP: image_name = @"omnibox_http.pdf"; break; case IDR_OMNIBOX_HISTORY: image_name = @"omnibox_history.pdf"; break; case IDR_OMNIBOX_EXTENSION_APP: image_name = @"omnibox_extension_app.pdf"; break; // Values from |ToolbarModel::GetIcon()|. case IDR_OMNIBOX_HTTPS_VALID: image_name = @"omnibox_https_valid.pdf"; break; case IDR_OMNIBOX_HTTPS_WARNING: image_name = @"omnibox_https_warning.pdf"; break; case IDR_OMNIBOX_HTTPS_INVALID: image_name = @"omnibox_https_invalid.pdf"; break; } if (image_name) { if (NSImage* image = app::mac::GetCachedImageWithName(image_name)) { return image; } else { NOTREACHED() << "Missing image for " << base::SysNSStringToUTF8(image_name); } } ResourceBundle& rb = ResourceBundle::GetSharedInstance(); return rb.GetNativeImageNamed(resource_id); } AutocompleteEditViewMac::AutocompleteEditViewMac( AutocompleteEditController* controller, ToolbarModel* toolbar_model, Profile* profile, CommandUpdater* command_updater, AutocompleteTextField* field) : model_(new AutocompleteEditModel(this, controller, profile)), popup_view_(new AutocompletePopupViewMac(this, model_.get(), profile, field)), controller_(controller), toolbar_model_(toolbar_model), command_updater_(command_updater), field_(field), suggest_text_length_(0), delete_was_pressed_(false), delete_at_end_pressed_(false), line_height_(0) { DCHECK(controller); DCHECK(toolbar_model); DCHECK(profile); DCHECK(command_updater); DCHECK(field); [field_ setObserver:this]; // Needed so that editing doesn't lose the styling. [field_ setAllowsEditingTextAttributes:YES]; // Get the appropriate line height for the font that we use. scoped_nsobject<NSLayoutManager> layoutManager([[NSLayoutManager alloc] init]); [layoutManager setUsesScreenFonts:YES]; line_height_ = [layoutManager defaultLineHeightForFont:GetFieldFont()]; DCHECK_GT(line_height_, 0); } AutocompleteEditViewMac::~AutocompleteEditViewMac() { // Destroy popup view before this object in case it tries to call us // back in the destructor. Likewise for destroying the model before // this object. popup_view_.reset(); model_.reset(); // Disconnect from |field_|, it outlives this object. [field_ setObserver:NULL]; } AutocompleteEditModel* AutocompleteEditViewMac::model() { return model_.get(); } const AutocompleteEditModel* AutocompleteEditViewMac::model() const { return model_.get(); } void AutocompleteEditViewMac::SaveStateToTab(TabContents* tab) { DCHECK(tab); const bool hasFocus = [field_ currentEditor] ? true : false; NSRange range; if (hasFocus) { range = GetSelectedRange(); } else { // If we are not focussed, there is no selection. Manufacture // something reasonable in case it starts to matter in the future. range = NSMakeRange(0, GetTextLength()); } AutocompleteEditViewMacState state(model_->GetStateForTabSwitch(), hasFocus, range); StoreStateToTab(tab, state); } void AutocompleteEditViewMac::Update( const TabContents* tab_for_state_restoring) { // TODO(shess): It seems like if the tab is non-NULL, then this code // shouldn't need to be called at all. When coded that way, I find // that the field isn't always updated correctly. Figure out why // this is. Maybe this method should be refactored into more // specific cases. const bool user_visible = model_->UpdatePermanentText(WideToUTF16Hack(toolbar_model_->GetText())); if (tab_for_state_restoring) { RevertAll(); const AutocompleteEditViewMacState* state = GetStateFromTab(tab_for_state_restoring); if (state) { // Should restore the user's text via SetUserText(). model_->RestoreState(state->model_state); // Restore focus and selection if they were present when the tab // was switched away. if (state->has_focus) { // TODO(shess): Unfortunately, there is no safe way to update // this because TabStripController -selectTabWithContents:* is // also messing with focus. Both parties need to agree to // store existing state before anyone tries to setup the new // state. Anyhow, it would look something like this. #if 0 [[field_ window] makeFirstResponder:field_]; [[field_ currentEditor] setSelectedRange:state->selection]; #endif } } } else if (user_visible) { // Restore everything to the baseline look. RevertAll(); // TODO(shess): Figure out how this case is used, to make sure // we're getting the selection and popup right. } else { // TODO(shess): This corresponds to _win and _gtk, except those // guard it with a test for whether the security level changed. // But AFAICT, that can only change if the text changed, and that // code compares the toolbar_model_ security level with the local // security level. Dig in and figure out why this isn't a no-op // that should go away. EmphasizeURLComponents(); } } void AutocompleteEditViewMac::OpenURL(const GURL& url, WindowOpenDisposition disposition, PageTransition::Type transition, const GURL& alternate_nav_url, size_t selected_line, const string16& keyword) { // TODO(shess): Why is the caller passing an invalid url in the // first place? Make sure that case isn't being dropped on the // floor. if (!url.is_valid()) { return; } model_->OpenURL(url, disposition, transition, alternate_nav_url, selected_line, keyword); } string16 AutocompleteEditViewMac::GetText() const { return base::SysNSStringToUTF16(GetNonSuggestTextSubstring()); } bool AutocompleteEditViewMac::IsEditingOrEmpty() const { return model_->user_input_in_progress() || !GetTextLength(); } int AutocompleteEditViewMac::GetIcon() const { return IsEditingOrEmpty() ? AutocompleteMatch::TypeToIcon(model_->CurrentTextType()) : toolbar_model_->GetIcon(); } void AutocompleteEditViewMac::SetUserText(const string16& text) { SetUserText(text, text, true); } void AutocompleteEditViewMac::SetUserText(const string16& text, const string16& display_text, bool update_popup) { model_->SetUserText(text); // TODO(shess): TODO below from gtk. // TODO(deanm): something about selection / focus change here. SetText(display_text); if (update_popup) { UpdatePopup(); } model_->OnChanged(); } NSRange AutocompleteEditViewMac::GetSelectedRange() const { return [[field_ currentEditor] selectedRange]; } NSRange AutocompleteEditViewMac::GetMarkedRange() const { DCHECK([field_ currentEditor]); return [(NSTextView*)[field_ currentEditor] markedRange]; } void AutocompleteEditViewMac::SetSelectedRange(const NSRange range) { // This can be called when we don't have focus. For instance, when // the user clicks the "Go" button. if (model_->has_focus()) { // TODO(shess): If |model_| thinks we have focus, this should not // be necessary. Try to convert to DCHECK(IsFirstResponder()). if (![field_ currentEditor]) { [[field_ window] makeFirstResponder:field_]; } // TODO(shess): What if it didn't get first responder, and there is // no field editor? This will do nothing. Well, at least it won't // crash. Think of something more productive to do, or prove that // it cannot occur and DCHECK appropriately. [[field_ currentEditor] setSelectedRange:range]; } } void AutocompleteEditViewMac::SetWindowTextAndCaretPos(const string16& text, size_t caret_pos) { DCHECK_LE(caret_pos, text.size()); SetTextAndSelectedRange(text, NSMakeRange(caret_pos, caret_pos)); } void AutocompleteEditViewMac::SetForcedQuery() { // We need to do this first, else |SetSelectedRange()| won't work. FocusLocation(true); const string16 current_text(GetText()); const size_t start = current_text.find_first_not_of(kWhitespaceUTF16); if (start == string16::npos || (current_text[start] != '?')) { SetUserText(ASCIIToUTF16("?")); } else { NSRange range = NSMakeRange(start + 1, current_text.size() - start - 1); [[field_ currentEditor] setSelectedRange:range]; } } bool AutocompleteEditViewMac::IsSelectAll() { if (![field_ currentEditor]) return true; const NSRange all_range = NSMakeRange(0, GetTextLength()); return NSEqualRanges(all_range, GetSelectedRange()); } bool AutocompleteEditViewMac::DeleteAtEndPressed() { return delete_at_end_pressed_; } void AutocompleteEditViewMac::GetSelectionBounds(string16::size_type* start, string16::size_type* end) { if (![field_ currentEditor]) { *start = *end = 0; return; } const NSRange selected_range = GetSelectedRange(); *start = static_cast<size_t>(selected_range.location); *end = static_cast<size_t>(NSMaxRange(selected_range)); } void AutocompleteEditViewMac::SelectAll(bool reversed) { // TODO(shess): Figure out what |reversed| implies. The gtk version // has it imply inverting the selection front to back, but I don't // even know if that makes sense for Mac. // TODO(shess): Verify that we should be stealing focus at this // point. SetSelectedRange(NSMakeRange(0, GetTextLength())); } void AutocompleteEditViewMac::RevertAll() { ClosePopup(); model_->Revert(); model_->OnChanged(); [field_ clearUndoChain]; } void AutocompleteEditViewMac::UpdatePopup() { model_->SetInputInProgress(true); if (!model_->has_focus()) return; // Comment copied from AutocompleteEditViewWin::UpdatePopup(): // Don't inline autocomplete when: // * The user is deleting text // * The caret/selection isn't at the end of the text // * The user has just pasted in something that replaced all the text // * The user is trying to compose something in an IME bool prevent_inline_autocomplete = IsImeComposing(); NSTextView* editor = (NSTextView*)[field_ currentEditor]; if (editor) { if (NSMaxRange([editor selectedRange]) < [[editor textStorage] length] - suggest_text_length_) prevent_inline_autocomplete = true; } model_->StartAutocomplete([editor selectedRange].length != 0, prevent_inline_autocomplete); } void AutocompleteEditViewMac::ClosePopup() { model_->StopAutocomplete(); } void AutocompleteEditViewMac::SetFocus() { } void AutocompleteEditViewMac::SetText(const string16& display_text) { // If we are setting the text directly, there cannot be any suggest text. suggest_text_length_ = 0; SetTextInternal(display_text); } void AutocompleteEditViewMac::SetTextInternal( const string16& display_text) { NSString* ss = base::SysUTF16ToNSString(display_text); NSMutableAttributedString* as = [[[NSMutableAttributedString alloc] initWithString:ss] autorelease]; ApplyTextAttributes(display_text, as); [field_ setAttributedStringValue:as]; // TODO(shess): This may be an appropriate place to call: // model_->OnChanged(); // In the current implementation, this tells LocationBarViewMac to // mess around with |model_| and update |field_|. Unfortunately, // when I look at our peer implementations, it's not entirely clear // to me if this is safe. SetTextInternal() is sort of an utility method, // and different callers sometimes have different needs. Research // this issue so that it can be added safely. // TODO(shess): Also, consider whether this code couldn't just // manage things directly. Windows uses a series of overlaid view // objects to accomplish the hinting stuff that OnChanged() does, so // it makes sense to have it in the controller that lays those // things out. Mac instead pushes the support into a custom // text-field implementation. } void AutocompleteEditViewMac::SetTextAndSelectedRange( const string16& display_text, const NSRange range) { SetText(display_text); SetSelectedRange(range); } NSString* AutocompleteEditViewMac::GetNonSuggestTextSubstring() const { NSString* text = [field_ stringValue]; if (suggest_text_length_ > 0) { NSUInteger length = [text length]; DCHECK_LE(suggest_text_length_, length); text = [text substringToIndex:(length - suggest_text_length_)]; } return text; } NSString* AutocompleteEditViewMac::GetSuggestTextSubstring() const { if (suggest_text_length_ == 0) return nil; NSString* text = [field_ stringValue]; NSUInteger length = [text length]; DCHECK_LE(suggest_text_length_, length); return [text substringFromIndex:(length - suggest_text_length_)]; } void AutocompleteEditViewMac::EmphasizeURLComponents() { NSTextView* editor = (NSTextView*)[field_ currentEditor]; // If the autocomplete text field is in editing mode, then we can just change // its attributes through its editor. Otherwise, we simply reset its content. if (editor) { NSTextStorage* storage = [editor textStorage]; [storage beginEditing]; // Clear the existing attributes from the text storage, then // overlay the appropriate Omnibox attributes. [storage setAttributes:[NSDictionary dictionary] range:NSMakeRange(0, [storage length])]; ApplyTextAttributes(GetText(), storage); [storage endEditing]; } else { SetText(GetText()); } } void AutocompleteEditViewMac::ApplyTextAttributes( const string16& display_text, NSMutableAttributedString* as) { [as addAttribute:NSFontAttributeName value:GetFieldFont() range:NSMakeRange(0, [as length])]; // Make a paragraph style locking in the standard line height as the maximum, // otherwise the baseline may shift "downwards". scoped_nsobject<NSMutableParagraphStyle> paragraph_style([[NSMutableParagraphStyle alloc] init]); [paragraph_style setMaximumLineHeight:line_height_]; [as addAttribute:NSParagraphStyleAttributeName value:paragraph_style range:NSMakeRange(0, [as length])]; // Grey out the suggest text. [as addAttribute:NSForegroundColorAttributeName value:SuggestTextColor() range:NSMakeRange([as length] - suggest_text_length_, suggest_text_length_)]; url_parse::Component scheme, host; AutocompleteInput::ParseForEmphasizeComponents( display_text, model_->GetDesiredTLD(), &scheme, &host); const bool emphasize = model_->CurrentTextIsURL() && (host.len > 0); if (emphasize) { [as addAttribute:NSForegroundColorAttributeName value:BaseTextColor() range:NSMakeRange(0, [as length])]; [as addAttribute:NSForegroundColorAttributeName value:HostTextColor() range:ComponentToNSRange(host)]; } // TODO(shess): GTK has this as a member var, figure out why. // [Could it be to not change if no change? If so, I'm guessing // AppKit may already handle that.] const ToolbarModel::SecurityLevel security_level = toolbar_model_->GetSecurityLevel(); // Emphasize the scheme for security UI display purposes (if necessary). if (!model_->user_input_in_progress() && scheme.is_nonempty() && (security_level != ToolbarModel::NONE)) { NSColor* color; if (security_level == ToolbarModel::EV_SECURE || security_level == ToolbarModel::SECURE) { color = SecureSchemeColor(); } else if (security_level == ToolbarModel::SECURITY_ERROR) { color = SecurityErrorSchemeColor(); // Add a strikethrough through the scheme. [as addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInt:NSUnderlineStyleSingle] range:ComponentToNSRange(scheme)]; } else if (security_level == ToolbarModel::SECURITY_WARNING) { color = BaseTextColor(); } else { NOTREACHED(); color = BaseTextColor(); } [as addAttribute:NSForegroundColorAttributeName value:color range:ComponentToNSRange(scheme)]; } } void AutocompleteEditViewMac::OnTemporaryTextMaybeChanged( const string16& display_text, bool save_original_selection) { if (save_original_selection) saved_temporary_selection_ = GetSelectedRange(); suggest_text_length_ = 0; SetWindowTextAndCaretPos(display_text, display_text.size()); model_->OnChanged(); [field_ clearUndoChain]; } void AutocompleteEditViewMac::OnStartingIME() { // Reset the suggest text just before starting an IME composition session, // otherwise the IME composition may be interrupted when the suggest text // gets reset by the IME composition change. SetInstantSuggestion(string16(), false); } bool AutocompleteEditViewMac::OnInlineAutocompleteTextMaybeChanged( const string16& display_text, size_t user_text_length) { // TODO(shess): Make sure that this actually works. The round trip // to native form and back may mean that it's the same but not the // same. if (display_text == GetText()) return false; DCHECK_LE(user_text_length, display_text.size()); const NSRange range = NSMakeRange(user_text_length, display_text.size() - user_text_length); SetTextAndSelectedRange(display_text, range); model_->OnChanged(); [field_ clearUndoChain]; return true; } void AutocompleteEditViewMac::OnRevertTemporaryText() { SetSelectedRange(saved_temporary_selection_); } bool AutocompleteEditViewMac::IsFirstResponder() const { return [field_ currentEditor] != nil ? true : false; } void AutocompleteEditViewMac::OnBeforePossibleChange() { // We should only arrive here when the field is focussed. DCHECK(IsFirstResponder()); selection_before_change_ = GetSelectedRange(); text_before_change_ = GetText(); marked_range_before_change_ = GetMarkedRange(); } bool AutocompleteEditViewMac::OnAfterPossibleChange() { // We should only arrive here when the field is focussed. DCHECK(IsFirstResponder()); const NSRange new_selection(GetSelectedRange()); const string16 new_text(GetText()); const size_t length = new_text.length(); const bool selection_differs = (new_selection.length || selection_before_change_.length) && !NSEqualRanges(new_selection, selection_before_change_); const bool at_end_of_edit = (length == new_selection.location); const bool text_differs = (new_text != text_before_change_) || !NSEqualRanges(marked_range_before_change_, GetMarkedRange()); // When the user has deleted text, we don't allow inline // autocomplete. This is assumed if the text has gotten shorter AND // the selection has shifted towards the front of the text. During // normal typing the text will almost always be shorter (as the new // input replaces the autocomplete suggestion), but in that case the // selection point will have moved towards the end of the text. // TODO(shess): In our implementation, we can catch -deleteBackward: // and other methods to provide positive knowledge that a delete // occured, rather than intuiting it from context. Consider whether // that would be a stronger approach. const bool just_deleted_text = (length < text_before_change_.length() && new_selection.location <= selection_before_change_.location); delete_at_end_pressed_ = false; const bool something_changed = model_->OnAfterPossibleChange( new_text, new_selection.location, NSMaxRange(new_selection), selection_differs, text_differs, just_deleted_text, !IsImeComposing()); if (delete_was_pressed_ && at_end_of_edit) delete_at_end_pressed_ = true; // Restyle in case the user changed something. // TODO(shess): I believe there are multiple-redraw cases, here. // Linux watches for something_changed && text_differs, but that // fails for us in case you copy the URL and paste the identical URL // back (we'll lose the styling). EmphasizeURLComponents(); model_->OnChanged(); delete_was_pressed_ = false; return something_changed; } gfx::NativeView AutocompleteEditViewMac::GetNativeView() const { return field_; } CommandUpdater* AutocompleteEditViewMac::GetCommandUpdater() { return command_updater_; } void AutocompleteEditViewMac::SetInstantSuggestion( const string16& suggest_text, bool animate_to_complete) { NSString* text = GetNonSuggestTextSubstring(); bool needs_update = (suggest_text_length_ > 0); // Append the new suggest text. suggest_text_length_ = suggest_text.length(); if (suggest_text_length_ > 0) { text = [text stringByAppendingString:base::SysUTF16ToNSString( suggest_text)]; needs_update = true; } if (needs_update) { NSRange current_range = GetSelectedRange(); SetTextInternal(base::SysNSStringToUTF16(text)); if (NSMaxRange(current_range) <= [text length] - suggest_text_length_) SetSelectedRange(current_range); else SetSelectedRange(NSMakeRange([text length] - suggest_text_length_, 0)); } } string16 AutocompleteEditViewMac::GetInstantSuggestion() const { return suggest_text_length_ ? base::SysNSStringToUTF16(GetSuggestTextSubstring()) : string16(); } int AutocompleteEditViewMac::TextWidth() const { // Not used on mac. NOTREACHED(); return 0; } bool AutocompleteEditViewMac::IsImeComposing() const { return [(NSTextView*)[field_ currentEditor] hasMarkedText]; } void AutocompleteEditViewMac::OnDidBeginEditing() { // We should only arrive here when the field is focussed. DCHECK([field_ currentEditor]); } void AutocompleteEditViewMac::OnBeforeChange() { // Capture the current state. OnBeforePossibleChange(); } void AutocompleteEditViewMac::OnDidChange() { // Figure out what changed and notify the model_. OnAfterPossibleChange(); } void AutocompleteEditViewMac::OnDidEndEditing() { ClosePopup(); } bool AutocompleteEditViewMac::OnDoCommandBySelector(SEL cmd) { // We should only arrive here when the field is focussed. DCHECK(IsFirstResponder()); if (cmd != @selector(moveRight:) && cmd != @selector(insertTab:) && cmd != @selector(insertTabIgnoringFieldEditor:)) { // Reset the suggest text for any change other than key right or tab. // TODO(rohitrao): This is here to prevent complications when editing text. // See if this can be removed. SetInstantSuggestion(string16(), false); } if (cmd == @selector(deleteForward:)) delete_was_pressed_ = true; // Don't intercept up/down-arrow if the popup isn't open. if (popup_view_->IsOpen()) { if (cmd == @selector(moveDown:)) { model_->OnUpOrDownKeyPressed(1); return true; } if (cmd == @selector(moveUp:)) { model_->OnUpOrDownKeyPressed(-1); return true; } } if (cmd == @selector(moveRight:)) { // Only commit suggested text if the cursor is all the way to the right and // there is no selection. if (suggest_text_length_ > 0 && IsCaretAtEnd()) { model_->CommitSuggestedText(true); return true; } } if (cmd == @selector(scrollPageDown:)) { model_->OnUpOrDownKeyPressed(model_->result().size()); return true; } if (cmd == @selector(scrollPageUp:)) { model_->OnUpOrDownKeyPressed(-model_->result().size()); return true; } if (cmd == @selector(cancelOperation:)) { return model_->OnEscapeKeyPressed(); } if (cmd == @selector(insertTab:) || cmd == @selector(insertTabIgnoringFieldEditor:)) { if (model_->is_keyword_hint()) return model_->AcceptKeyword(); if (suggest_text_length_ > 0) { model_->CommitSuggestedText(true); return true; } if (!IsCaretAtEnd()) { PlaceCaretAt(GetTextLength()); // OnDidChange() will not be triggered when setting selected range in this // method, so we need to call it explicitly. OnDidChange(); return true; } if (model_->AcceptCurrentInstantPreview()) return true; } // |-noop:| is sent when the user presses Cmd+Return. Override the no-op // behavior with the proper WindowOpenDisposition. NSEvent* event = [NSApp currentEvent]; if (cmd == @selector(insertNewline:) || (cmd == @selector(noop:) && [event keyCode] == kVK_Return)) { WindowOpenDisposition disposition = event_utils::WindowOpenDispositionFromNSEvent(event); model_->AcceptInput(disposition, false); // Opening a URL in a background tab should also revert the omnibox contents // to their original state. We cannot do a blanket revert in OpenURL() // because middle-clicks also open in a new background tab, but those should // not revert the omnibox text. RevertAll(); return true; } // Option-Return if (cmd == @selector(insertNewlineIgnoringFieldEditor:)) { model_->AcceptInput(NEW_FOREGROUND_TAB, false); return true; } // When the user does Control-Enter, the existing content has "www." // prepended and ".com" appended. |model_| should already have // received notification when the Control key was depressed, but it // is safe to tell it twice. if (cmd == @selector(insertLineBreak:)) { OnControlKeyChanged(true); WindowOpenDisposition disposition = event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); model_->AcceptInput(disposition, false); return true; } if (cmd == @selector(deleteBackward:)) { if (OnBackspacePressed()) { return true; } } if (cmd == @selector(deleteForward:)) { const NSUInteger modifiers = [[NSApp currentEvent] modifierFlags]; if ((modifiers & NSShiftKeyMask) != 0) { if (model_->popup_model()->IsOpen()) { model_->popup_model()->TryDeletingCurrentItem(); return true; } } } return false; } void AutocompleteEditViewMac::OnSetFocus(bool control_down) { model_->OnSetFocus(control_down); controller_->OnSetFocus(); } void AutocompleteEditViewMac::OnKillFocus() { // Tell the model to reset itself. model_->OnWillKillFocus(NULL); model_->OnKillFocus(); controller_->OnKillFocus(); } bool AutocompleteEditViewMac::CanCopy() { const NSRange selection = GetSelectedRange(); return selection.length > 0; } void AutocompleteEditViewMac::CopyToPasteboard(NSPasteboard* pb) { DCHECK(CanCopy()); const NSRange selection = GetSelectedRange(); string16 text = base::SysNSStringToUTF16( [[field_ stringValue] substringWithRange:selection]); GURL url; bool write_url = false; model_->AdjustTextForCopy(selection.location, IsSelectAll(), &text, &url, &write_url); NSString* nstext = base::SysUTF16ToNSString(text); [pb declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; [pb setString:nstext forType:NSStringPboardType]; if (write_url) { [pb declareURLPasteboardWithAdditionalTypes:[NSArray array] owner:nil]; [pb setDataForURL:base::SysUTF8ToNSString(url.spec()) title:nstext]; } } void AutocompleteEditViewMac::OnPaste() { // This code currently expects |field_| to be focussed. DCHECK([field_ currentEditor]); string16 text = GetClipboardText(g_browser_process->clipboard()); if (text.empty()) { return; } NSString* s = base::SysUTF16ToNSString(text); // -shouldChangeTextInRange:* and -didChangeText are documented in // NSTextView as things you need to do if you write additional // user-initiated editing functions. They cause the appropriate // delegate methods to be called. // TODO(shess): It would be nice to separate the Cocoa-specific code // from the Chrome-specific code. NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]); const NSRange selectedRange = GetSelectedRange(); if ([editor shouldChangeTextInRange:selectedRange replacementString:s]) { // Record this paste, so we can do different behavior. model_->on_paste(); // Force a Paste operation to trigger the text_changed code in // OnAfterPossibleChange(), even if identical contents are pasted // into the text box. text_before_change_.clear(); [editor replaceCharactersInRange:selectedRange withString:s]; [editor didChangeText]; } } bool AutocompleteEditViewMac::CanPasteAndGo() { return model_->CanPasteAndGo(GetClipboardText(g_browser_process->clipboard())); } int AutocompleteEditViewMac::GetPasteActionStringId() { DCHECK(CanPasteAndGo()); // Use PASTE_AND_SEARCH as the default fallback (although the DCHECK above // should never trigger). if (!model_->is_paste_and_search()) return IDS_PASTE_AND_GO; else return IDS_PASTE_AND_SEARCH; } void AutocompleteEditViewMac::OnPasteAndGo() { if (CanPasteAndGo()) model_->PasteAndGo(); } void AutocompleteEditViewMac::OnFrameChanged() { // TODO(shess): UpdatePopupAppearance() is called frequently, so it // should be really cheap, but in this case we could probably make // things even cheaper by refactoring between the popup-placement // code and the matrix-population code. popup_view_->UpdatePopupAppearance(); model_->PopupBoundsChangedTo(popup_view_->GetTargetBounds()); // Give controller a chance to rearrange decorations. model_->OnChanged(); } bool AutocompleteEditViewMac::OnBackspacePressed() { // Don't intercept if not in keyword search mode. if (model_->is_keyword_hint() || model_->keyword().empty()) { return false; } // Don't intercept if there is a selection, or the cursor isn't at // the leftmost position. const NSRange selection = GetSelectedRange(); if (selection.length > 0 || selection.location > 0) { return false; } // We're showing a keyword and the user pressed backspace at the // beginning of the text. Delete the selected keyword. model_->ClearKeyword(GetText()); return true; } NSRange AutocompleteEditViewMac::SelectionRangeForProposedRange( NSRange proposed_range) { // Should never call this function unless editing is in progress. DCHECK([field_ currentEditor]); if (![field_ currentEditor]) return proposed_range; // Do not use [field_ stringValue] here, as that forces a sync between the // field and the editor. This sync will end up setting the selection, which // in turn calls this method, leading to an infinite loop. Instead, retrieve // the current string value directly from the editor. size_t text_length = [[[field_ currentEditor] string] length]; // Cannot select suggested text. size_t max = text_length - suggest_text_length_; NSUInteger start = proposed_range.location; NSUInteger end = proposed_range.location + proposed_range.length; if (start > max) start = max; if (end > max) end = max; return NSMakeRange(start, end - start); } void AutocompleteEditViewMac::OnControlKeyChanged(bool pressed) { model_->OnControlKeyChanged(pressed); } void AutocompleteEditViewMac::FocusLocation(bool select_all) { if ([field_ isEditable]) { // If the text field has a field editor, it's the first responder, meaning // that it's already focused. makeFirstResponder: will select all, so only // call it if this behavior is desired. if (select_all || ![field_ currentEditor]) [[field_ window] makeFirstResponder:field_]; DCHECK_EQ([field_ currentEditor], [[field_ window] firstResponder]); } } // TODO(shess): Copied from autocomplete_edit_view_win.cc. Could this // be pushed into the model? string16 AutocompleteEditViewMac::GetClipboardText( ui::Clipboard* clipboard) { // autocomplete_edit_view_win.cc assumes this can never happen, we // will too. DCHECK(clipboard); if (clipboard->IsFormatAvailable(ui::Clipboard::GetPlainTextWFormatType(), ui::Clipboard::BUFFER_STANDARD)) { string16 text16; clipboard->ReadText(ui::Clipboard::BUFFER_STANDARD, &text16); // Note: Unlike in the find popup and textfield view, here we completely // remove whitespace strings containing newlines. We assume users are // most likely pasting in URLs that may have been split into multiple // lines in terminals, email programs, etc., and so linebreaks indicate // completely bogus whitespace that would just cause the input to be // invalid. return CollapseWhitespace(text16, true); } // Try bookmark format. // // It is tempting to try bookmark format first, but the URL we get out of a // bookmark has been cannonicalized via GURL. This means if a user copies // and pastes from the URL bar to itself, the text will get fixed up and // cannonicalized, which is not what the user expects. By pasting in this // order, we are sure to paste what the user copied. if (clipboard->IsFormatAvailable(ui::Clipboard::GetUrlWFormatType(), ui::Clipboard::BUFFER_STANDARD)) { std::string url_str; clipboard->ReadBookmark(NULL, &url_str); // pass resulting url string through GURL to normalize GURL url(url_str); if (url.is_valid()) { return UTF8ToUTF16(url.spec()); } } return string16(); } // static NSFont* AutocompleteEditViewMac::GetFieldFont() { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); return rb.GetFont(ResourceBundle::BaseFont).GetNativeFont(); } NSUInteger AutocompleteEditViewMac::GetTextLength() const { return ([field_ currentEditor] ? [[[field_ currentEditor] string] length] : [[field_ stringValue] length]) - suggest_text_length_; } void AutocompleteEditViewMac::PlaceCaretAt(NSUInteger pos) { DCHECK(pos <= GetTextLength()); SetSelectedRange(NSMakeRange(pos, pos)); } bool AutocompleteEditViewMac::IsCaretAtEnd() const { const NSRange selection = GetSelectedRange(); return selection.length == 0 && selection.location == GetTextLength(); }