blob: de3d666812823c1a0c0ee7d313fd739d6750cb50 [file] [log] [blame]
// Copyright (c) 2012 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/ui/cocoa/omnibox/omnibox_view_mac.h"
#include <Carbon/Carbon.h> // kVK_Return
#include "base/auto_reset.h"
#include "base/mac/foundation_util.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/search/search.h"
#include "chrome/browser/themes/theme_service.h"
#import "chrome/browser/ui/cocoa/browser_window_controller.h"
#import "chrome/browser/ui/cocoa/l10n_util.h"
#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
#include "chrome/browser/ui/cocoa/omnibox/omnibox_popup_view_mac.h"
#include "chrome/browser/ui/omnibox/chrome_omnibox_client.h"
#include "chrome/browser/ui/omnibox/clipboard_utils.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/grit/components_scaled_resources.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/omnibox_edit_controller.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/omnibox_popup_model.h"
#include "components/toolbar/toolbar_model.h"
#include "content/public/browser/web_contents.h"
#include "extensions/common/constants.h"
#import "skia/ext/skia_utils_mac.h"
#import "third_party/mozilla/NSPasteboard+Utils.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/clipboard_util_mac.h"
#import "ui/base/cocoa/cocoa_base_utils.h"
#import "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/material_design/material_design_controller.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/font.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/geometry/rect.h"
// TODO(ellyjones): Remove this when the deployment target is 10.9 or later.
extern NSString* const NSAccessibilityPriorityKey;
using content::WebContents;
// 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 OmniboxViewMac, 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 {
const int kOmniboxLargeFontSizeDelta = 9;
const int kOmniboxNormalFontSizeDelta = 1;
const int kOmniboxSmallMaterialFontSizeDelta = -1;
NSColor* HostTextColor(bool in_dark_mode) {
return in_dark_mode ? [NSColor whiteColor] : [NSColor blackColor];
}
NSColor* SecureSchemeColor(bool in_dark_mode) {
return in_dark_mode ? skia::SkColorToSRGBNSColor(SK_ColorWHITE)
: skia::SkColorToSRGBNSColor(gfx::kGoogleGreen700);
}
NSColor* SecurityWarningSchemeColor(bool in_dark_mode) {
return in_dark_mode
? skia::SkColorToSRGBNSColor(SkColorSetA(SK_ColorWHITE, 0x7F))
: skia::SkColorToSRGBNSColor(gfx::kGoogleYellow700);
}
NSColor* SecurityErrorSchemeColor(bool in_dark_mode) {
return in_dark_mode
? skia::SkColorToSRGBNSColor(SkColorSetA(SK_ColorWHITE, 0x7F))
: skia::SkColorToSRGBNSColor(gfx::kGoogleRed700);
}
const char kOmniboxViewMacStateKey[] = "OmniboxViewMacState";
// Store's the model and view state across tab switches.
struct OmniboxViewMacState : public base::SupportsUserData::Data {
OmniboxViewMacState(const OmniboxEditModel::State model_state,
const bool has_focus,
const NSRange& selection)
: model_state(model_state),
has_focus(has_focus),
selection(selection) {
}
~OmniboxViewMacState() override {}
const OmniboxEditModel::State model_state;
const bool has_focus;
const NSRange selection;
};
// Accessors for storing and getting the state from the tab.
void StoreStateToTab(WebContents* tab,
std::unique_ptr<OmniboxViewMacState> state) {
tab->SetUserData(kOmniboxViewMacStateKey, std::move(state));
}
const OmniboxViewMacState* GetStateFromTab(const WebContents* tab) {
return static_cast<OmniboxViewMacState*>(
tab->GetUserData(&kOmniboxViewMacStateKey));
}
} // namespace
// static
NSImage* OmniboxViewMac::ImageForResource(int resource_id) {
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
return rb.GetNativeImageNamed(resource_id).ToNSImage();
}
// static
NSColor* OmniboxViewMac::SuggestTextColor() {
return [NSColor colorWithCalibratedWhite:0.0 alpha:0.5];
}
//static
SkColor OmniboxViewMac::BaseTextColorSkia(bool in_dark_mode) {
return in_dark_mode ? SkColorSetA(SK_ColorWHITE, 0x7F)
: SkColorSetA(SK_ColorBLACK, 0x7F);
}
// static
NSColor* OmniboxViewMac::BaseTextColor(bool in_dark_mode) {
return skia::SkColorToSRGBNSColor(BaseTextColorSkia(in_dark_mode));
}
// static
NSColor* OmniboxViewMac::GetSecureTextColor(
security_state::SecurityLevel security_level,
bool in_dark_mode) {
if (security_level == security_state::EV_SECURE ||
security_level == security_state::SECURE) {
return SecureSchemeColor(in_dark_mode);
}
if (security_level == security_state::DANGEROUS)
return SecurityErrorSchemeColor(in_dark_mode);
DCHECK_EQ(security_state::SECURITY_WARNING, security_level);
return SecurityWarningSchemeColor(in_dark_mode);
}
OmniboxViewMac::OmniboxViewMac(OmniboxEditController* controller,
Profile* profile,
CommandUpdater* command_updater,
AutocompleteTextField* field)
: OmniboxView(
controller,
base::WrapUnique(new ChromeOmniboxClient(controller, profile))),
profile_(profile),
popup_view_(new OmniboxPopupViewMac(this, model(), field)),
field_(field),
saved_temporary_selection_(NSMakeRange(0, 0)),
marked_range_before_change_(NSMakeRange(0, 0)),
delete_was_pressed_(false),
delete_at_end_pressed_(false),
in_coalesced_update_block_(false),
do_coalesced_text_update_(false),
do_coalesced_range_update_(false),
attributing_display_string_(nil) {
[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.
base::scoped_nsobject<NSLayoutManager> layoutManager(
[[NSLayoutManager alloc] init]);
[layoutManager setUsesScreenFonts:YES];
}
OmniboxViewMac::~OmniboxViewMac() {
// Destroy popup view before this object in case it tries to call us
// back in the destructor.
popup_view_.reset();
// Disconnect from |field_|, it outlives this object.
[field_ setObserver:NULL];
}
void OmniboxViewMac::SaveStateToTab(WebContents* tab) {
DCHECK(tab);
const bool hasFocus = [field_ currentEditor] ? true : false;
NSRange range;
if (hasFocus) {
range = GetSelectedRange();
} else {
// If we are not focused, there is no selection. Manufacture
// something reasonable in case it starts to matter in the future.
range = NSMakeRange(0, GetTextLength());
}
StoreStateToTab(tab, base::MakeUnique<OmniboxViewMacState>(
model()->GetStateForTabSwitch(), hasFocus, range));
}
void OmniboxViewMac::OnTabChanged(const WebContents* web_contents) {
const OmniboxViewMacState* state = GetStateFromTab(web_contents);
model()->RestoreState(state ? &state->model_state : NULL);
// Restore focus and selection if they were present when the tab
// was switched away.
if (state && 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
}
}
void OmniboxViewMac::ResetTabState(WebContents* web_contents) {
StoreStateToTab(web_contents, nullptr);
}
void OmniboxViewMac::Update() {
if (model()->UpdatePermanentText()) {
const bool was_select_all = IsSelectAll();
NSTextView* text_view =
base::mac::ObjCCastStrict<NSTextView>([field_ currentEditor]);
const bool was_reversed =
[text_view selectionAffinity] == NSSelectionAffinityUpstream;
// Restore everything to the baseline look.
RevertAll();
// Only select all when we have focus. If we don't have focus, selecting
// all is unnecessary since the selection will change on regaining focus,
// and can in fact cause artifacts, e.g. if the user is on the NTP and
// clicks a link to navigate, causing |was_select_all| to be vacuously true
// for the empty omnibox, and we then select all here, leading to the
// trailing portion of a long URL being scrolled into view. We could try
// and address cases like this, but it seems better to just not muck with
// things when the omnibox isn't focused to begin with.
if (was_select_all && model()->has_focus())
SelectAll(was_reversed);
} 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 OmniboxViewMac::OpenMatch(const AutocompleteMatch& match,
WindowOpenDisposition disposition,
const GURL& alternate_nav_url,
const base::string16& pasted_text,
size_t selected_line) {
// Coalesce text and selection updates from the following function. If we
// don't do this, the user may see intermediate states as brief flickers.
in_coalesced_update_block_ = true;
OmniboxView::OpenMatch(
match, disposition, alternate_nav_url, pasted_text, selected_line);
in_coalesced_update_block_ = false;
if (do_coalesced_text_update_) {
SetText(coalesced_text_update_);
// Ensure location bar icon is updated to reflect text.
controller()->OnChanged();
}
do_coalesced_text_update_ = false;
if (do_coalesced_range_update_)
SetSelectedRange(coalesced_range_update_);
do_coalesced_range_update_ = false;
}
base::string16 OmniboxViewMac::GetText() const {
return base::SysNSStringToUTF16([field_ stringValue]);
}
NSRange OmniboxViewMac::GetSelectedRange() const {
return [[field_ currentEditor] selectedRange];
}
NSRange OmniboxViewMac::GetMarkedRange() const {
DCHECK([field_ currentEditor]);
return [(NSTextView*)[field_ currentEditor] markedRange];
}
void OmniboxViewMac::SetSelectedRange(const NSRange range) {
if (in_coalesced_update_block_) {
do_coalesced_range_update_ = true;
coalesced_range_update_ = range;
return;
}
// 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 OmniboxViewMac::SetWindowTextAndCaretPos(const base::string16& text,
size_t caret_pos,
bool update_popup,
bool notify_text_changed) {
DCHECK_LE(caret_pos, text.size());
SetTextAndSelectedRange(text, NSMakeRange(caret_pos, 0));
if (update_popup)
UpdatePopup();
if (notify_text_changed)
TextChanged();
}
void OmniboxViewMac::SetCaretPos(size_t caret_pos) {
size_t pos = std::min(caret_pos, GetTextLength());
SetSelectedRange(NSMakeRange(pos, 0));
}
void OmniboxViewMac::EnterKeywordModeForDefaultSearchProvider() {
// We need to do this first, else |SetSelectedRange()| won't work.
FocusLocation(true);
// Transition the user into keyword mode using their default search provider.
model()->EnterKeywordModeForDefaultSearchProvider(
KeywordModeEntryMethod::KEYBOARD_SHORTCUT);
}
bool OmniboxViewMac::IsSelectAll() const {
if (![field_ currentEditor])
return true;
const NSRange all_range = NSMakeRange(0, GetTextLength());
return NSEqualRanges(all_range, GetSelectedRange());
}
bool OmniboxViewMac::DeleteAtEndPressed() {
return delete_at_end_pressed_;
}
void OmniboxViewMac::GetSelectionBounds(base::string16::size_type* start,
base::string16::size_type* end) const {
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 OmniboxViewMac::SelectAll(bool reversed) {
if (!model()->has_focus())
return;
NSRange full_range = NSMakeRange(0, GetTextLength());
// When coalescing updates, just set the range and not the direction. It's
// unlikely that the direction will matter after OpenMatch() applies updates.
if (in_coalesced_update_block_) {
SetSelectedRange(full_range);
return;
}
NSTextView* text_view =
base::mac::ObjCCastStrict<NSTextView>([field_ currentEditor]);
NSSelectionAffinity affinity =
reversed ? NSSelectionAffinityUpstream : NSSelectionAffinityDownstream;
[text_view setSelectedRange:full_range affinity:affinity stillSelecting:NO];
}
void OmniboxViewMac::RevertAll() {
OmniboxView::RevertAll();
[field_ clearUndoChain];
}
void OmniboxViewMac::UpdatePopup() {
model()->SetInputInProgress(true);
if (!model()->has_focus())
return;
// Comment copied from OmniboxViewWin::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])
prevent_inline_autocomplete = true;
}
model()->StartAutocomplete([editor selectedRange].length != 0,
prevent_inline_autocomplete);
}
void OmniboxViewMac::CloseOmniboxPopup() {
// Call both base class methods.
ClosePopup();
OmniboxView::CloseOmniboxPopup();
}
void OmniboxViewMac::SetFocus() {
FocusLocation(false);
model()->SetCaretVisibility(true);
}
void OmniboxViewMac::ApplyCaretVisibility() {
[[field_ cell] setHideFocusState:!model()->is_caret_visible()
ofView:field_];
}
void OmniboxViewMac::SetText(const base::string16& display_text) {
SetTextInternal(display_text);
}
void OmniboxViewMac::SetTextInternal(const base::string16& display_text) {
if (in_coalesced_update_block_) {
do_coalesced_text_update_ = true;
coalesced_text_update_ = display_text;
// Don't do any selection changes, since they apply to the previous text.
do_coalesced_range_update_ = false;
return;
}
NSString* ss = base::SysUTF16ToNSString(display_text);
NSMutableAttributedString* attributedString =
[[[NSMutableAttributedString alloc] initWithString:ss] autorelease];
ApplyTextAttributes(display_text, attributedString);
[field_ setAttributedStringValue:attributedString];
// 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 OmniboxViewMac::SetTextAndSelectedRange(const base::string16& display_text,
const NSRange range) {
SetText(display_text);
SetSelectedRange(range);
}
void OmniboxViewMac::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];
// This function can be called during the editor's -resignFirstResponder. If
// that happens, |storage| and |field_| will not be synced automatically any
// more. Calling -stringValue ensures that |field_| reflects the changes to
// |storage|.
[field_ stringValue];
} else {
SetText(GetText());
}
}
void OmniboxViewMac::ApplyTextStyle(
NSMutableAttributedString* attributedString) {
[attributedString addAttribute:NSFontAttributeName
value:GetNormalFieldFont()
range:NSMakeRange(0, [attributedString length])];
// Make a paragraph style locking in the standard line height as the maximum,
// otherwise the baseline may shift "downwards".
base::scoped_nsobject<NSMutableParagraphStyle> paragraph_style(
[[NSMutableParagraphStyle alloc] init]);
CGFloat line_height = [[field_ cell] lineHeight];
[paragraph_style setMaximumLineHeight:line_height];
[paragraph_style setMinimumLineHeight:line_height];
[paragraph_style setLineBreakMode:NSLineBreakByTruncatingTail];
// Set an explicit alignment so it isn't implied from writing direction.
[paragraph_style setAlignment:cocoa_l10n_util::ShouldDoExperimentalRTLLayout()
? NSRightTextAlignment
: NSLeftTextAlignment];
// If this is a URL, set the top-level paragraph direction to LTR (avoids RTL
// characters from making the URL render from right to left, as per RFC 3987
// Section 4.1).
if (model()->CurrentTextIsURL())
[paragraph_style setBaseWritingDirection:NSWritingDirectionLeftToRight];
[attributedString addAttribute:NSParagraphStyleAttributeName
value:paragraph_style
range:NSMakeRange(0, [attributedString length])];
}
void OmniboxViewMac::SetEmphasis(bool emphasize, const gfx::Range& range) {
bool in_dark_mode = [[field_ window] inIncognitoModeWithSystemTheme];
NSRange ns_range = range.IsValid()
? range.ToNSRange()
: NSMakeRange(0, [attributing_display_string_ length]);
[attributing_display_string_
addAttribute:NSForegroundColorAttributeName
value:(emphasize) ? HostTextColor(in_dark_mode)
: BaseTextColor(in_dark_mode)
range:ns_range];
}
void OmniboxViewMac::UpdateSchemeStyle(const gfx::Range& range) {
if (!range.IsValid())
return;
const security_state::SecurityLevel security_level =
controller()->GetToolbarModel()->GetSecurityLevel(false);
if ((security_level == security_state::NONE) ||
(security_level == security_state::HTTP_SHOW_WARNING))
return;
if (security_level == security_state::DANGEROUS) {
// Add a strikethrough through the scheme.
[attributing_display_string_
addAttribute:NSStrikethroughStyleAttributeName
value:[NSNumber numberWithInt:NSUnderlineStyleSingle]
range:range.ToNSRange()];
}
bool in_dark_mode = [[field_ window] inIncognitoModeWithSystemTheme];
[attributing_display_string_
addAttribute:NSForegroundColorAttributeName
value:GetSecureTextColor(security_level, in_dark_mode)
range:range.ToNSRange()];
}
void OmniboxViewMac::ApplyTextAttributes(
const base::string16& display_text,
NSMutableAttributedString* attributed_string) {
NSUInteger as_length = [attributed_string length];
if (as_length == 0) {
return;
}
ApplyTextStyle(attributed_string);
// A kinda hacky way to add breaking at periods. This is what Safari does.
// This works for IDNs too, despite the "en_US".
[attributed_string addAttribute:@"NSLanguage"
value:@"en_US_POSIX"
range:NSMakeRange(0, as_length)];
// Cache a pointer to the attributed string to allow the superclass'
// virtual method invocations to add attributes.
DCHECK(attributing_display_string_ == nil);
base::AutoReset<NSMutableAttributedString*> resetter(
&attributing_display_string_, attributed_string);
UpdateTextStyle(display_text, ChromeAutocompleteSchemeClassifier(profile_));
}
void OmniboxViewMac::OnTemporaryTextMaybeChanged(
const base::string16& display_text,
bool save_original_selection,
bool notify_text_changed) {
if (save_original_selection)
saved_temporary_selection_ = GetSelectedRange();
SetWindowTextAndCaretPos(display_text, display_text.size(), false, false);
if (notify_text_changed)
model()->OnChanged();
[field_ clearUndoChain];
}
bool OmniboxViewMac::OnInlineAutocompleteTextMaybeChanged(
const base::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];
AnnounceAutocompleteForScreenReader(display_text);
return true;
}
void OmniboxViewMac::OnInlineAutocompleteTextCleared() {
}
void OmniboxViewMac::OnRevertTemporaryText() {
SetSelectedRange(saved_temporary_selection_);
// We got here because the user hit the Escape key. We explicitly don't call
// TextChanged(), since OmniboxPopupModel::ResetToDefaultMatch() has already
// been called by now, and it would've called TextChanged() if it was
// warranted.
}
bool OmniboxViewMac::IsFirstResponder() const {
return [field_ currentEditor] != nil ? true : false;
}
void OmniboxViewMac::OnBeforePossibleChange() {
// We should only arrive here when the field is focused.
DCHECK(IsFirstResponder());
GetState(&state_before_change_);
marked_range_before_change_ = GetMarkedRange();
}
bool OmniboxViewMac::OnAfterPossibleChange(bool allow_keyword_ui_change) {
// We should only arrive here when the field is focused.
DCHECK(IsFirstResponder());
State new_state;
GetState(&new_state);
OmniboxView::StateChanges state_changes =
GetStateChanges(state_before_change_, new_state);
const bool at_end_of_edit = (new_state.text.length() == new_state.sel_end);
delete_at_end_pressed_ = false;
const bool something_changed = model()->OnAfterPossibleChange(
state_changes, allow_keyword_ui_change && !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).
TextChanged();
delete_was_pressed_ = false;
return something_changed;
}
gfx::NativeView OmniboxViewMac::GetNativeView() const {
return field_;
}
gfx::NativeView OmniboxViewMac::GetRelativeWindowForPopup() const {
// Not used on mac.
NOTREACHED();
return NULL;
}
int OmniboxViewMac::GetTextWidth() const {
// Not used on mac.
NOTREACHED();
return 0;
}
int OmniboxViewMac::GetWidth() const {
return ceil([field_ bounds].size.width);
}
bool OmniboxViewMac::IsImeComposing() const {
return [(NSTextView*)[field_ currentEditor] hasMarkedText];
}
void OmniboxViewMac::OnDidBeginEditing() {
// We should only arrive here when the field is focused.
DCHECK([field_ currentEditor]);
}
void OmniboxViewMac::OnBeforeChange() {
// Capture the current state.
OnBeforePossibleChange();
}
void OmniboxViewMac::OnDidChange() {
// Figure out what changed and notify the model.
OnAfterPossibleChange(true);
}
void OmniboxViewMac::OnDidEndEditing() {
ClosePopup();
}
void OmniboxViewMac::OnInsertText() {
// If |insert_char_time_| is not null, there's a pending insert char operation
// that hasn't been painted yet. Keep the earlier time.
if (insert_char_time_.is_null())
insert_char_time_ = base::TimeTicks::Now();
}
void OmniboxViewMac::OnBeforeDrawRect() {
draw_rect_start_time_ = base::TimeTicks::Now();
}
void OmniboxViewMac::OnDidDrawRect() {
base::TimeTicks now = base::TimeTicks::Now();
UMA_HISTOGRAM_TIMES("Omnibox.PaintTime", now - draw_rect_start_time_);
if (!insert_char_time_.is_null()) {
UMA_HISTOGRAM_TIMES("Omnibox.CharTypedToRepaintLatency",
now - insert_char_time_);
insert_char_time_ = base::TimeTicks();
}
}
bool OmniboxViewMac::OnDoCommandBySelector(SEL cmd) {
if (cmd == @selector(deleteForward:))
delete_was_pressed_ = true;
if (cmd == @selector(moveDown:)) {
model()->OnUpOrDownKeyPressed(1);
return true;
}
if (cmd == @selector(moveUp:)) {
model()->OnUpOrDownKeyPressed(-1);
return true;
}
if (model()->popup_model()->IsOpen()) {
if (cmd == @selector(insertBacktab:)) {
if (model()->popup_model()->selected_line_state() ==
OmniboxPopupModel::KEYWORD) {
model()->ClearKeyword();
return true;
} else {
model()->OnUpOrDownKeyPressed(-1);
return true;
}
}
if ((cmd == @selector(insertTab:) ||
cmd == @selector(insertTabIgnoringFieldEditor:)) &&
!model()->is_keyword_hint()) {
model()->OnUpOrDownKeyPressed(1);
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:)) &&
model()->is_keyword_hint()) {
return model()->AcceptKeyword(KeywordModeEntryMethod::TAB);
}
// |-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 type] == NSKeyDown || [event type] == NSKeyUp) &&
[event keyCode] == kVK_Return)) {
// If the user hasn't entered any text in keyword search mode, we need to
// return early in order to avoid cancelling the search.
if (GetTextLength() == 0)
return true;
WindowOpenDisposition disposition =
ui::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(WindowOpenDisposition::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 =
ui::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 OmniboxViewMac::OnSetFocus(bool control_down) {
model()->OnSetFocus(control_down);
// Update the keyword and search hint states.
controller()->OnChanged();
}
void OmniboxViewMac::OnKillFocus() {
// Tell the model to reset itself.
model()->OnWillKillFocus();
model()->OnKillFocus();
}
void OmniboxViewMac::OnMouseDown(NSInteger button_number) {
// Restore caret visibility whenever the user clicks in the the omnibox. This
// is not always covered by OnSetFocus() because when clicking while the
// omnibox has invisible focus does not trigger a new OnSetFocus() call.
if (button_number == 0 || button_number == 1)
model()->SetCaretVisibility(true);
}
bool OmniboxViewMac::CanCopy() {
const NSRange selection = GetSelectedRange();
return selection.length > 0;
}
base::scoped_nsobject<NSPasteboardItem> OmniboxViewMac::CreatePasteboardItem() {
DCHECK(CanCopy());
const NSRange selection = GetSelectedRange();
base::string16 text = base::SysNSStringToUTF16(
[[field_ stringValue] substringWithRange:selection]);
// Copy the URL.
GURL url;
bool write_url = false;
model()->AdjustTextForCopy(selection.location, IsSelectAll(), &text, &url,
&write_url);
if (IsSelectAll())
UMA_HISTOGRAM_COUNTS(OmniboxEditModel::kCutOrCopyAllTextHistogram, 1);
NSString* nstext = base::SysUTF16ToNSString(text);
if (write_url) {
return ui::ClipboardUtil::PasteboardItemFromUrl(
base::SysUTF8ToNSString(url.spec()), nstext);
} else {
return ui::ClipboardUtil::PasteboardItemFromString(nstext);
}
}
void OmniboxViewMac::CopyToPasteboard(NSPasteboard* pboard) {
[pboard clearContents];
base::scoped_nsobject<NSPasteboardItem> item(CreatePasteboardItem());
[pboard writeObjects:@[ item.get() ]];
}
void OmniboxViewMac::OnPaste() {
// This code currently expects |field_| to be focused.
DCHECK([field_ currentEditor]);
base::string16 text = GetClipboardText();
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()->OnPaste();
// Force a Paste operation to trigger the text_changed code in
// OnAfterPossibleChange(), even if identical contents are pasted
// into the text box.
state_before_change_.text.clear();
[editor replaceCharactersInRange:selectedRange withString:s];
[editor didChangeText];
}
}
bool OmniboxViewMac::CanPasteAndGo() {
return model()->CanPasteAndGo(GetClipboardText());
}
int OmniboxViewMac::GetPasteActionStringId() {
base::string16 text(GetClipboardText());
DCHECK(model()->CanPasteAndGo(text));
return model()->IsPasteAndSearch(text) ?
IDS_PASTE_AND_SEARCH : IDS_PASTE_AND_GO;
}
void OmniboxViewMac::OnPasteAndGo() {
base::string16 text(GetClipboardText());
if (model()->CanPasteAndGo(text))
model()->PasteAndGo(text);
}
void OmniboxViewMac::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();
// Give controller a chance to rearrange decorations.
model()->OnChanged();
}
void OmniboxViewMac::ClosePopup() {
OmniboxView::CloseOmniboxPopup();
}
bool OmniboxViewMac::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();
return true;
}
NSRange OmniboxViewMac::SelectionRangeForProposedRange(NSRange proposed_range) {
return proposed_range;
}
void OmniboxViewMac::OnControlKeyChanged(bool pressed) {
model()->OnControlKeyChanged(pressed);
}
void OmniboxViewMac::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]);
}
}
// static
NSFont* OmniboxViewMac::GetNormalFieldFont() {
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
return rb
.GetFontWithDelta(kOmniboxNormalFontSizeDelta, gfx::Font::NORMAL,
gfx::Font::Weight::NORMAL)
.GetNativeFont();
}
NSFont* OmniboxViewMac::GetBoldFieldFont() {
// Request a bold font, then make it larger. ResourceBundle will do the
// opposite which makes a large system normal font a non-system bold font.
// That gives a different baseline to making the non-system bold font larger.
// And while the omnibox locks the baseline in ApplyTextStyle(),
// OmniboxPopupCellData does not.
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
return rb
.GetFontWithDelta(0, gfx::Font::NORMAL, gfx::Font::Weight::BOLD)
.Derive(kOmniboxNormalFontSizeDelta, gfx::Font::NORMAL,
gfx::Font::Weight::BOLD)
.GetNativeFont();
}
NSFont* OmniboxViewMac::GetLargeFont() {
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
return rb
.GetFontWithDelta(kOmniboxLargeFontSizeDelta, gfx::Font::NORMAL,
gfx::Font::Weight::NORMAL)
.GetNativeFont();
}
NSFont* OmniboxViewMac::GetSmallFont() {
return ui::ResourceBundle::GetSharedInstance()
.GetFontWithDelta(kOmniboxSmallMaterialFontSizeDelta, gfx::Font::NORMAL,
gfx::Font::Weight::NORMAL)
.GetNativeFont();
}
int OmniboxViewMac::GetOmniboxTextLength() const {
return static_cast<int>(GetTextLength());
}
NSUInteger OmniboxViewMac::GetTextLength() const {
return [field_ currentEditor] ? [[[field_ currentEditor] string] length] :
[[field_ stringValue] length];
}
bool OmniboxViewMac::IsCaretAtEnd() const {
const NSRange selection = GetSelectedRange();
return NSMaxRange(selection) == GetTextLength();
}
void OmniboxViewMac::AnnounceAutocompleteForScreenReader(
const base::string16& display_text) {
NSString* announcement =
l10n_util::GetNSStringF(IDS_ANNOUNCEMENT_COMPLETION_AVAILABLE_MAC,
display_text);
NSDictionary* notification_info = @{
NSAccessibilityAnnouncementKey : announcement,
NSAccessibilityPriorityKey : @(NSAccessibilityPriorityHigh)
};
NSAccessibilityPostNotificationWithUserInfo(
[field_ window],
NSAccessibilityAnnouncementRequestedNotification,
notification_info);
}