| // 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); |
| } |