1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097
|
// 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/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,
OmniboxViewMacState* state) {
tab->SetUserData(kOmniboxViewMacStateKey, state);
}
const OmniboxViewMacState* GetStateFromTab(const WebContents* tab) {
return static_cast<OmniboxViewMacState*>(
tab->GetUserData(&kOmniboxViewMacStateKey));
}
// Helper to make converting url ranges to NSRange easier to
// read.
NSRange ComponentToNSRange(const url::Component& component) {
return NSMakeRange(static_cast<NSInteger>(component.begin),
static_cast<NSInteger>(component.len));
}
} // 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) {
[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());
}
OmniboxViewMacState* state =
new OmniboxViewMacState(model()->GetStateForTabSwitch(), hasFocus, range);
StoreStateToTab(tab, state);
}
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::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) {
DCHECK(!in_coalesced_update_block_);
if (!model()->has_focus())
return;
NSTextView* text_view =
base::mac::ObjCCastStrict<NSTextView>([field_ currentEditor]);
NSSelectionAffinity affinity =
reversed ? NSSelectionAffinityUpstream : NSSelectionAffinityDownstream;
NSRange range = NSMakeRange(0, GetTextLength());
[text_view setSelectedRange: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::ApplyTextAttributes(
const base::string16& display_text,
NSMutableAttributedString* attributedString) {
NSUInteger as_length = [attributedString length];
if (as_length == 0) {
return;
}
NSRange as_entire_string = NSMakeRange(0, as_length);
bool in_dark_mode = [[field_ window] inIncognitoModeWithSystemTheme];
ApplyTextStyle(attributedString);
// A kinda hacky way to add breaking at periods. This is what Safari does.
// This works for IDNs too, despite the "en_US".
[attributedString addAttribute:@"NSLanguage"
value:@"en_US_POSIX"
range:as_entire_string];
[attributedString addAttribute:NSForegroundColorAttributeName
value:HostTextColor(in_dark_mode)
range:as_entire_string];
url::Component scheme, host;
AutocompleteInput::ParseForEmphasizeComponents(
display_text, ChromeAutocompleteSchemeClassifier(profile_), &scheme,
&host);
bool grey_out_url = display_text.substr(scheme.begin, scheme.len) ==
base::UTF8ToUTF16(extensions::kExtensionScheme);
if (model()->CurrentTextIsURL() &&
(host.is_nonempty() || grey_out_url)) {
[attributedString addAttribute:NSForegroundColorAttributeName
value:BaseTextColor(in_dark_mode)
range:as_entire_string];
if (!grey_out_url) {
[attributedString addAttribute:NSForegroundColorAttributeName
value:HostTextColor(in_dark_mode)
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 security_state::SecurityLevel security_level =
controller()->GetToolbarModel()->GetSecurityLevel(false);
// Emphasize the scheme for security UI display purposes (if necessary).
if (!model()->user_input_in_progress() && model()->CurrentTextIsURL() &&
scheme.is_nonempty() &&
(security_level != security_state::NONE) &&
(security_level != security_state::HTTP_SHOW_WARNING)) {
if (security_level == security_state::DANGEROUS) {
// Add a strikethrough through the scheme.
[attributedString addAttribute:NSStrikethroughStyleAttributeName
value:[NSNumber numberWithInt:NSUnderlineStyleSingle]
range:ComponentToNSRange(scheme)];
}
[attributedString
addAttribute:NSForegroundColorAttributeName
value:GetSecureTextColor(security_level, in_dark_mode)
range:ComponentToNSRange(scheme)];
}
}
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)) {
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);
}
|