blob: 5157b9ae0c684457c533f1da10d30d970a7df423 [file] [log] [blame]
// Copyright (c) 2010 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/clipboard/clipboard.h"
#include "app/clipboard/scoped_clipboard_writer.h"
#include "app/resource_bundle.h"
#include "base/nsimage_cache_mac.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/cocoa/event_utils.h"
#include "chrome/browser/tab_contents/tab_contents.h"
#include "chrome/browser/toolbar_model.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "net/base/escape.h"
#import "third_party/mozilla/NSPasteboard+Utils.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_MORE: image_name = @"omnibox_more.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 = nsimage_cache::ImageNamed(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(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];
}
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, [[field_ stringValue] length]);
}
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(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 std::wstring& 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);
}
std::wstring AutocompleteEditViewMac::GetText() const {
return base::SysNSStringToWide(GetNonSuggestTextSubstring());
}
bool AutocompleteEditViewMac::IsEditingOrEmpty() const {
return model_->user_input_in_progress() ||
([[field_ stringValue] length] == 0);
}
int AutocompleteEditViewMac::GetIcon() const {
return IsEditingOrEmpty() ?
AutocompleteMatch::TypeToIcon(model_->CurrentTextType()) :
toolbar_model_->GetIcon();
}
void AutocompleteEditViewMac::SetUserText(const std::wstring& text) {
SetUserText(text, text, true);
}
void AutocompleteEditViewMac::SetUserText(const std::wstring& text,
const std::wstring& 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();
}
controller_->OnChanged();
}
NSRange AutocompleteEditViewMac::GetSelectedRange() const {
DCHECK([field_ currentEditor]);
return [[field_ currentEditor] selectedRange];
}
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 std::wstring& 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 std::wstring current_text(GetText());
const size_t start = current_text.find_first_not_of(kWhitespaceWide);
if (start == std::wstring::npos || (current_text[start] != '?')) {
SetUserText(L"?");
} 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, GetText().length());
return NSEqualRanges(all_range, GetSelectedRange());
}
bool AutocompleteEditViewMac::DeleteAtEndPressed() {
return delete_at_end_pressed_;
}
void AutocompleteEditViewMac::GetSelectionBounds(std::wstring::size_type* start,
std::wstring::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, GetText().length()));
}
void AutocompleteEditViewMac::RevertAll() {
ClosePopup();
model_->Revert();
// TODO(shess): This should be a no-op, the results from GetText()
// could only get there via UpdateAndStyleText() in the first place.
// Dig into where this code can be called from and see if this line
// can be removed.
EmphasizeURLComponents();
controller_->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 = false;
NSTextView* editor = (NSTextView*)[field_ currentEditor];
if (editor) {
if ([editor hasMarkedText])
prevent_inline_autocomplete = true;
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() {
if (popup_view_->GetModel()->IsOpen())
controller_->OnAutocompleteWillClosePopup();
popup_view_->GetModel()->StopAutocomplete();
}
void AutocompleteEditViewMac::SetFocus() {
}
void AutocompleteEditViewMac::SetSuggestText(const string16& suggest_text) {
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::SysNSStringToWide(text));
if (NSMaxRange(current_range) <= [text length] - suggest_text_length_)
SetSelectedRange(current_range);
else
SetSelectedRange(NSMakeRange([text length] - suggest_text_length_, 0));
}
}
bool AutocompleteEditViewMac::CommitSuggestText() {
if (suggest_text_length_ == 0)
return false;
std::wstring input_text(GetText());
suggest_text_length_ = 0;
std::wstring text(GetText());
// Call SetText() to force a redraw and move the cursor to the end.
SetText(text);
model()->FinalizeInstantQuery(input_text, text.substr(input_text.size()));
return true;
}
void AutocompleteEditViewMac::SetText(const std::wstring& 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 std::wstring& display_text) {
NSString* ss = base::SysWideToNSString(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:
// controller_->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 std::wstring& 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;
}
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 std::wstring& 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 std::wstring& display_text, bool save_original_selection) {
if (save_original_selection)
saved_temporary_selection_ = GetSelectedRange();
suggest_text_length_ = 0;
SetWindowTextAndCaretPos(display_text, display_text.size());
controller_->OnChanged();
[field_ clearUndoChain];
}
bool AutocompleteEditViewMac::OnInlineAutocompleteTextMaybeChanged(
const std::wstring& 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);
controller_->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();
}
bool AutocompleteEditViewMac::OnAfterPossibleChange() {
// We should only arrive here when the field is focussed.
DCHECK(IsFirstResponder());
const NSRange new_selection(GetSelectedRange());
const std::wstring new_text(GetText());
const size_t length = new_text.length();
const bool selection_differs = !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_);
// 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,
selection_differs, text_differs, just_deleted_text, at_end_of_edit);
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();
controller_->OnChanged();
delete_was_pressed_ = false;
return something_changed;
}
gfx::NativeView AutocompleteEditViewMac::GetNativeView() const {
return field_;
}
CommandUpdater* AutocompleteEditViewMac::GetCommandUpdater() {
return command_updater_;
}
void AutocompleteEditViewMac::OnDidBeginEditing() {
// We should only arrive here when the field is focussed.
DCHECK([field_ currentEditor]);
// Capture the current state.
OnBeforePossibleChange();
}
void AutocompleteEditViewMac::OnDidChange() {
// Figure out what changed and notify the model_.
OnAfterPossibleChange();
// Then capture the new state.
OnBeforePossibleChange();
}
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.
SetSuggestText(string16());
}
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.
NSRange range = GetSelectedRange();
if (range.length == 0 &&
suggest_text_length_ > 0 &&
(range.location + suggest_text_length_ ==
[[field_ stringValue] length])) {
controller_->OnCommitSuggestedText(GetText());
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() && !model_->keyword().empty()) {
model_->AcceptKeyword();
return true;
}
if (suggest_text_length_ > 0) {
controller_->OnCommitSuggestedText(GetText());
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 (popup_view_->IsOpen()) {
popup_view_->GetModel()->TryDeletingCurrentItem();
return true;
}
}
}
// Capture the state before the operation changes the content.
// TODO(shess): Determine if this is always redundent WRT the call
// in -controlTextDidChange:.
OnBeforePossibleChange();
return false;
}
void AutocompleteEditViewMac::OnSetFocus(bool control_down) {
model_->OnSetFocus(control_down);
controller_->OnSetFocus();
}
void AutocompleteEditViewMac::OnKillFocus() {
// Tell the model to reset itself.
controller_->OnAutocompleteLosingFocus(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();
std::wstring text = base::SysNSStringToWide(
[[field_ stringValue] substringWithRange:selection]);
GURL url;
bool write_url = false;
model_->AdjustTextForCopy(selection.location, IsSelectAll(), &text, &url,
&write_url);
NSString* nstext = base::SysWideToNSString(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]);
std::wstring text = GetClipboardText(g_browser_process->clipboard());
if (text.empty()) {
return;
}
NSString* s = base::SysWideToNSString(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]) {
// If this paste will be replacing all the text, record that, so
// we can do different behaviors in such a case.
if (IsSelectAll())
model_->on_paste_replacing_all();
// 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.
controller_->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?
std::wstring AutocompleteEditViewMac::GetClipboardText(Clipboard* clipboard) {
// autocomplete_edit_view_win.cc assumes this can never happen, we
// will too.
DCHECK(clipboard);
if (clipboard->IsFormatAvailable(Clipboard::GetPlainTextWFormatType(),
Clipboard::BUFFER_STANDARD)) {
string16 text16;
clipboard->ReadText(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(UTF16ToWide(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(Clipboard::GetUrlWFormatType(),
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 UTF8ToWide(url.spec());
}
}
return std::wstring();
}
// static
NSFont* AutocompleteEditViewMac::GetFieldFont() {
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
return rb.GetFont(ResourceBundle::BaseFont).GetNativeFont();
}