• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/ui/cocoa/omnibox/omnibox_view_mac.h"
6
7#include <Carbon/Carbon.h>  // kVK_Return
8
9#include "base/mac/foundation_util.h"
10#include "base/metrics/histogram.h"
11#include "base/strings/string_util.h"
12#include "base/strings/sys_string_conversions.h"
13#include "base/strings/utf_string_conversions.h"
14#include "chrome/browser/autocomplete/autocomplete_input.h"
15#include "chrome/browser/autocomplete/autocomplete_match.h"
16#include "chrome/browser/browser_process.h"
17#include "chrome/browser/search/search.h"
18#include "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
19#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
20#include "chrome/browser/ui/cocoa/omnibox/omnibox_popup_view_mac.h"
21#include "chrome/browser/ui/omnibox/omnibox_edit_controller.h"
22#include "chrome/browser/ui/omnibox/omnibox_popup_model.h"
23#include "chrome/browser/ui/toolbar/toolbar_model.h"
24#include "content/public/browser/web_contents.h"
25#include "extensions/common/constants.h"
26#include "grit/generated_resources.h"
27#include "grit/theme_resources.h"
28#import "third_party/mozilla/NSPasteboard+Utils.h"
29#import "ui/base/cocoa/cocoa_base_utils.h"
30#include "ui/base/clipboard/clipboard.h"
31#include "ui/base/resource/resource_bundle.h"
32#include "ui/gfx/font.h"
33#include "ui/gfx/font_list.h"
34#include "ui/gfx/geometry/rect.h"
35
36using content::WebContents;
37
38// Focus-handling between |field_| and model() is a bit subtle.
39// Other platforms detect change of focus, which is inconvenient
40// without subclassing NSTextField (even with a subclass, the use of a
41// field editor may complicate things).
42//
43// model() doesn't actually do anything when it gains focus, it just
44// initializes.  Visible activity happens only after the user edits.
45// NSTextField delegate receives messages around starting and ending
46// edits, so that suffices to catch focus changes.  Since all calls
47// into model() start from OmniboxViewMac, in the worst case
48// we can add code to sync up the sense of focus as needed.
49//
50// I've added DCHECK(IsFirstResponder()) in the places which I believe
51// should only be reachable when |field_| is being edited.  If these
52// fire, it probably means someone unexpected is calling into
53// model().
54//
55// Other platforms don't appear to have the sense of "key window" that
56// Mac does (I believe their fields lose focus when the window loses
57// focus).  Rather than modifying focus outside the control's edit
58// scope, when the window resigns key the autocomplete popup is
59// closed.  model() still believes it has focus, and the popup will
60// be regenerated on the user's next edit.  That seems to match how
61// things work on other platforms.
62
63namespace {
64
65// TODO(shess): This is ugly, find a better way.  Using it right now
66// so that I can crib from gtk and still be able to see that I'm using
67// the same values easily.
68NSColor* ColorWithRGBBytes(int rr, int gg, int bb) {
69  DCHECK_LE(rr, 255);
70  DCHECK_LE(bb, 255);
71  DCHECK_LE(gg, 255);
72  return [NSColor colorWithCalibratedRed:static_cast<float>(rr)/255.0
73                                   green:static_cast<float>(gg)/255.0
74                                    blue:static_cast<float>(bb)/255.0
75                                   alpha:1.0];
76}
77
78NSColor* HostTextColor() {
79  return [NSColor blackColor];
80}
81NSColor* BaseTextColor() {
82  return [NSColor darkGrayColor];
83}
84NSColor* SecureSchemeColor() {
85  return ColorWithRGBBytes(0x07, 0x95, 0x00);
86}
87NSColor* SecurityErrorSchemeColor() {
88  return ColorWithRGBBytes(0xa2, 0x00, 0x00);
89}
90
91const char kOmniboxViewMacStateKey[] = "OmniboxViewMacState";
92
93// Store's the model and view state across tab switches.
94struct OmniboxViewMacState : public base::SupportsUserData::Data {
95  OmniboxViewMacState(const OmniboxEditModel::State model_state,
96                      const bool has_focus,
97                      const NSRange& selection)
98      : model_state(model_state),
99        has_focus(has_focus),
100        selection(selection) {
101  }
102  virtual ~OmniboxViewMacState() {}
103
104  const OmniboxEditModel::State model_state;
105  const bool has_focus;
106  const NSRange selection;
107};
108
109// Accessors for storing and getting the state from the tab.
110void StoreStateToTab(WebContents* tab,
111                     OmniboxViewMacState* state) {
112  tab->SetUserData(kOmniboxViewMacStateKey, state);
113}
114const OmniboxViewMacState* GetStateFromTab(const WebContents* tab) {
115  return static_cast<OmniboxViewMacState*>(
116      tab->GetUserData(&kOmniboxViewMacStateKey));
117}
118
119// Helper to make converting url ranges to NSRange easier to
120// read.
121NSRange ComponentToNSRange(const url::Component& component) {
122  return NSMakeRange(static_cast<NSInteger>(component.begin),
123                     static_cast<NSInteger>(component.len));
124}
125
126}  // namespace
127
128// static
129NSImage* OmniboxViewMac::ImageForResource(int resource_id) {
130  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
131  return rb.GetNativeImageNamed(resource_id).ToNSImage();
132}
133
134// static
135NSColor* OmniboxViewMac::SuggestTextColor() {
136  return [NSColor colorWithCalibratedWhite:0.0 alpha:0.5];
137}
138
139OmniboxViewMac::OmniboxViewMac(OmniboxEditController* controller,
140                               Profile* profile,
141                               CommandUpdater* command_updater,
142                               AutocompleteTextField* field)
143    : OmniboxView(profile, controller, command_updater),
144      popup_view_(new OmniboxPopupViewMac(this, model(), field)),
145      field_(field),
146      saved_temporary_selection_(NSMakeRange(0, 0)),
147      selection_before_change_(NSMakeRange(0, 0)),
148      marked_range_before_change_(NSMakeRange(0, 0)),
149      delete_was_pressed_(false),
150      delete_at_end_pressed_(false),
151      in_coalesced_update_block_(false),
152      do_coalesced_text_update_(false),
153      do_coalesced_range_update_(false) {
154  [field_ setObserver:this];
155
156  // Needed so that editing doesn't lose the styling.
157  [field_ setAllowsEditingTextAttributes:YES];
158
159  // Get the appropriate line height for the font that we use.
160  base::scoped_nsobject<NSLayoutManager> layoutManager(
161      [[NSLayoutManager alloc] init]);
162  [layoutManager setUsesScreenFonts:YES];
163}
164
165OmniboxViewMac::~OmniboxViewMac() {
166  // Destroy popup view before this object in case it tries to call us
167  // back in the destructor.
168  popup_view_.reset();
169
170  // Disconnect from |field_|, it outlives this object.
171  [field_ setObserver:NULL];
172}
173
174void OmniboxViewMac::SaveStateToTab(WebContents* tab) {
175  DCHECK(tab);
176
177  const bool hasFocus = [field_ currentEditor] ? true : false;
178
179  NSRange range;
180  if (hasFocus) {
181    range = GetSelectedRange();
182  } else {
183    // If we are not focussed, there is no selection.  Manufacture
184    // something reasonable in case it starts to matter in the future.
185    range = NSMakeRange(0, GetTextLength());
186  }
187
188  OmniboxViewMacState* state =
189      new OmniboxViewMacState(model()->GetStateForTabSwitch(), hasFocus, range);
190  StoreStateToTab(tab, state);
191}
192
193void OmniboxViewMac::OnTabChanged(const WebContents* web_contents) {
194  const OmniboxViewMacState* state = GetStateFromTab(web_contents);
195  model()->RestoreState(state ? &state->model_state : NULL);
196  // Restore focus and selection if they were present when the tab
197  // was switched away.
198  if (state && state->has_focus) {
199    // TODO(shess): Unfortunately, there is no safe way to update
200    // this because TabStripController -selectTabWithContents:* is
201    // also messing with focus.  Both parties need to agree to
202    // store existing state before anyone tries to setup the new
203    // state.  Anyhow, it would look something like this.
204#if 0
205    [[field_ window] makeFirstResponder:field_];
206    [[field_ currentEditor] setSelectedRange:state->selection];
207#endif
208  }
209}
210
211void OmniboxViewMac::Update() {
212  if (chrome::ShouldDisplayOriginChip()) {
213    NSDictionary* placeholder_attributes = @{
214      NSFontAttributeName : GetFieldFont(gfx::Font::NORMAL),
215      NSForegroundColorAttributeName : [NSColor disabledControlTextColor]
216    };
217    base::scoped_nsobject<NSMutableAttributedString> placeholder_text(
218        [[NSMutableAttributedString alloc]
219            initWithString:base::SysUTF16ToNSString(GetHintText())
220                attributes:placeholder_attributes]);
221    [[field_ cell] setPlaceholderAttributedString:placeholder_text];
222  }
223  if (model()->UpdatePermanentText()) {
224    // Something visibly changed.  Re-enable URL replacement.
225    controller()->GetToolbarModel()->set_url_replacement_enabled(true);
226    model()->UpdatePermanentText();
227
228    // Restore everything to the baseline look.
229    RevertAll();
230
231    // TODO(shess): Figure out how this case is used, to make sure
232    // we're getting the selection and popup right.
233  } else {
234    // TODO(shess): This corresponds to _win and _gtk, except those
235    // guard it with a test for whether the security level changed.
236    // But AFAICT, that can only change if the text changed, and that
237    // code compares the toolbar model security level with the local
238    // security level.  Dig in and figure out why this isn't a no-op
239    // that should go away.
240    EmphasizeURLComponents();
241  }
242}
243
244void OmniboxViewMac::OpenMatch(const AutocompleteMatch& match,
245                               WindowOpenDisposition disposition,
246                               const GURL& alternate_nav_url,
247                               const base::string16& pasted_text,
248                               size_t selected_line) {
249  // Coalesce text and selection updates from the following function. If we
250  // don't do this, the user may see intermediate states as brief flickers.
251  in_coalesced_update_block_ = true;
252  OmniboxView::OpenMatch(
253      match, disposition, alternate_nav_url, pasted_text, selected_line);
254  in_coalesced_update_block_ = false;
255  if (do_coalesced_text_update_)
256    SetText(coalesced_text_update_);
257  do_coalesced_text_update_ = false;
258  if (do_coalesced_range_update_)
259    SetSelectedRange(coalesced_range_update_);
260  do_coalesced_range_update_ = false;
261}
262
263base::string16 OmniboxViewMac::GetText() const {
264  return base::SysNSStringToUTF16([field_ stringValue]);
265}
266
267NSRange OmniboxViewMac::GetSelectedRange() const {
268  return [[field_ currentEditor] selectedRange];
269}
270
271NSRange OmniboxViewMac::GetMarkedRange() const {
272  DCHECK([field_ currentEditor]);
273  return [(NSTextView*)[field_ currentEditor] markedRange];
274}
275
276void OmniboxViewMac::SetSelectedRange(const NSRange range) {
277  if (in_coalesced_update_block_) {
278    do_coalesced_range_update_ = true;
279    coalesced_range_update_ = range;
280    return;
281  }
282
283  // This can be called when we don't have focus.  For instance, when
284  // the user clicks the "Go" button.
285  if (model()->has_focus()) {
286    // TODO(shess): If model() thinks we have focus, this should not
287    // be necessary.  Try to convert to DCHECK(IsFirstResponder()).
288    if (![field_ currentEditor]) {
289      [[field_ window] makeFirstResponder:field_];
290    }
291
292    // TODO(shess): What if it didn't get first responder, and there is
293    // no field editor?  This will do nothing.  Well, at least it won't
294    // crash.  Think of something more productive to do, or prove that
295    // it cannot occur and DCHECK appropriately.
296    [[field_ currentEditor] setSelectedRange:range];
297  }
298}
299
300void OmniboxViewMac::SetWindowTextAndCaretPos(const base::string16& text,
301                                              size_t caret_pos,
302                                              bool update_popup,
303                                              bool notify_text_changed) {
304  DCHECK_LE(caret_pos, text.size());
305  SetTextAndSelectedRange(text, NSMakeRange(caret_pos, 0));
306
307  if (update_popup)
308    UpdatePopup();
309
310  if (notify_text_changed)
311    TextChanged();
312}
313
314void OmniboxViewMac::SetForcedQuery() {
315  // We need to do this first, else |SetSelectedRange()| won't work.
316  FocusLocation(true);
317
318  const base::string16 current_text(GetText());
319  const size_t start = current_text.find_first_not_of(base::kWhitespaceUTF16);
320  if (start == base::string16::npos || (current_text[start] != '?')) {
321    SetUserText(base::ASCIIToUTF16("?"));
322  } else {
323    NSRange range = NSMakeRange(start + 1, current_text.size() - start - 1);
324    [[field_ currentEditor] setSelectedRange:range];
325  }
326}
327
328bool OmniboxViewMac::IsSelectAll() const {
329  if (![field_ currentEditor])
330    return true;
331  const NSRange all_range = NSMakeRange(0, GetTextLength());
332  return NSEqualRanges(all_range, GetSelectedRange());
333}
334
335bool OmniboxViewMac::DeleteAtEndPressed() {
336  return delete_at_end_pressed_;
337}
338
339void OmniboxViewMac::GetSelectionBounds(base::string16::size_type* start,
340                                        base::string16::size_type* end) const {
341  if (![field_ currentEditor]) {
342    *start = *end = 0;
343    return;
344  }
345
346  const NSRange selected_range = GetSelectedRange();
347  *start = static_cast<size_t>(selected_range.location);
348  *end = static_cast<size_t>(NSMaxRange(selected_range));
349}
350
351void OmniboxViewMac::SelectAll(bool reversed) {
352  // TODO(shess): Figure out what |reversed| implies.  The gtk version
353  // has it imply inverting the selection front to back, but I don't
354  // even know if that makes sense for Mac.
355
356  // TODO(shess): Verify that we should be stealing focus at this
357  // point.
358  SetSelectedRange(NSMakeRange(0, GetTextLength()));
359}
360
361void OmniboxViewMac::RevertAll() {
362  OmniboxView::RevertAll();
363  [field_ clearUndoChain];
364}
365
366void OmniboxViewMac::UpdatePopup() {
367  model()->SetInputInProgress(true);
368  if (!model()->has_focus())
369    return;
370
371  // Comment copied from OmniboxViewWin::UpdatePopup():
372  // Don't inline autocomplete when:
373  //   * The user is deleting text
374  //   * The caret/selection isn't at the end of the text
375  //   * The user has just pasted in something that replaced all the text
376  //   * The user is trying to compose something in an IME
377  bool prevent_inline_autocomplete = IsImeComposing();
378  NSTextView* editor = (NSTextView*)[field_ currentEditor];
379  if (editor) {
380    if (NSMaxRange([editor selectedRange]) < [[editor textStorage] length])
381      prevent_inline_autocomplete = true;
382  }
383
384  model()->StartAutocomplete([editor selectedRange].length != 0,
385                            prevent_inline_autocomplete);
386}
387
388void OmniboxViewMac::CloseOmniboxPopup() {
389  // Call both base class methods.
390  ClosePopup();
391  OmniboxView::CloseOmniboxPopup();
392}
393
394void OmniboxViewMac::SetFocus() {
395  FocusLocation(false);
396  model()->SetCaretVisibility(true);
397}
398
399void OmniboxViewMac::ApplyCaretVisibility() {
400  [[field_ cell] setHideFocusState:!model()->is_caret_visible()
401                            ofView:field_];
402}
403
404void OmniboxViewMac::SetText(const base::string16& display_text) {
405  SetTextInternal(display_text);
406}
407
408void OmniboxViewMac::SetTextInternal(const base::string16& display_text) {
409  if (in_coalesced_update_block_) {
410    do_coalesced_text_update_ = true;
411    coalesced_text_update_ = display_text;
412    // Don't do any selection changes, since they apply to the previous text.
413    do_coalesced_range_update_ = false;
414    return;
415  }
416
417  NSString* ss = base::SysUTF16ToNSString(display_text);
418  NSMutableAttributedString* as =
419      [[[NSMutableAttributedString alloc] initWithString:ss] autorelease];
420
421  ApplyTextAttributes(display_text, as);
422  [field_ setAttributedStringValue:as];
423
424  // TODO(shess): This may be an appropriate place to call:
425  //   model()->OnChanged();
426  // In the current implementation, this tells LocationBarViewMac to
427  // mess around with model() and update |field_|.  Unfortunately,
428  // when I look at our peer implementations, it's not entirely clear
429  // to me if this is safe.  SetTextInternal() is sort of an utility method,
430  // and different callers sometimes have different needs.  Research
431  // this issue so that it can be added safely.
432
433  // TODO(shess): Also, consider whether this code couldn't just
434  // manage things directly.  Windows uses a series of overlaid view
435  // objects to accomplish the hinting stuff that OnChanged() does, so
436  // it makes sense to have it in the controller that lays those
437  // things out.  Mac instead pushes the support into a custom
438  // text-field implementation.
439}
440
441void OmniboxViewMac::SetTextAndSelectedRange(const base::string16& display_text,
442                                             const NSRange range) {
443  SetText(display_text);
444  SetSelectedRange(range);
445}
446
447void OmniboxViewMac::EmphasizeURLComponents() {
448  NSTextView* editor = (NSTextView*)[field_ currentEditor];
449  // If the autocomplete text field is in editing mode, then we can just change
450  // its attributes through its editor. Otherwise, we simply reset its content.
451  if (editor) {
452    NSTextStorage* storage = [editor textStorage];
453    [storage beginEditing];
454
455    // Clear the existing attributes from the text storage, then
456    // overlay the appropriate Omnibox attributes.
457    [storage setAttributes:[NSDictionary dictionary]
458                     range:NSMakeRange(0, [storage length])];
459    ApplyTextAttributes(GetText(), storage);
460
461    [storage endEditing];
462
463    // This function can be called during the editor's -resignFirstResponder. If
464    // that happens, |storage| and |field_| will not be synced automatically any
465    // more. Calling -stringValue ensures that |field_| reflects the changes to
466    // |storage|.
467    [field_ stringValue];
468  } else {
469    SetText(GetText());
470  }
471}
472
473void OmniboxViewMac::ApplyTextAttributes(const base::string16& display_text,
474                                         NSMutableAttributedString* as) {
475  NSUInteger as_length = [as length];
476  NSRange as_entire_string = NSMakeRange(0, as_length);
477
478  [as addAttribute:NSFontAttributeName value:GetFieldFont(gfx::Font::NORMAL)
479             range:as_entire_string];
480
481  // A kinda hacky way to add breaking at periods. This is what Safari does.
482  // This works for IDNs too, despite the "en_US".
483  [as addAttribute:@"NSLanguage" value:@"en_US_POSIX"
484             range:as_entire_string];
485
486  // Make a paragraph style locking in the standard line height as the maximum,
487  // otherwise the baseline may shift "downwards".
488  base::scoped_nsobject<NSMutableParagraphStyle> paragraph_style(
489      [[NSMutableParagraphStyle alloc] init]);
490  CGFloat line_height = [[field_ cell] lineHeight];
491  [paragraph_style setMaximumLineHeight:line_height];
492  [paragraph_style setMinimumLineHeight:line_height];
493  [paragraph_style setLineBreakMode:NSLineBreakByTruncatingTail];
494  [as addAttribute:NSParagraphStyleAttributeName value:paragraph_style
495             range:as_entire_string];
496
497  url::Component scheme, host;
498  AutocompleteInput::ParseForEmphasizeComponents(
499      display_text, &scheme, &host);
500  bool grey_out_url = display_text.substr(scheme.begin, scheme.len) ==
501      base::UTF8ToUTF16(extensions::kExtensionScheme);
502  if (model()->CurrentTextIsURL() &&
503      (host.is_nonempty() || grey_out_url)) {
504    [as addAttribute:NSForegroundColorAttributeName value:BaseTextColor()
505               range:as_entire_string];
506
507    if (!grey_out_url) {
508      [as addAttribute:NSForegroundColorAttributeName value:HostTextColor()
509               range:ComponentToNSRange(host)];
510    }
511  }
512
513  // TODO(shess): GTK has this as a member var, figure out why.
514  // [Could it be to not change if no change?  If so, I'm guessing
515  // AppKit may already handle that.]
516  const ToolbarModel::SecurityLevel security_level =
517      controller()->GetToolbarModel()->GetSecurityLevel(false);
518
519  // Emphasize the scheme for security UI display purposes (if necessary).
520  if (!model()->user_input_in_progress() && model()->CurrentTextIsURL() &&
521      scheme.is_nonempty() && (security_level != ToolbarModel::NONE)) {
522    NSColor* color;
523    if (security_level == ToolbarModel::EV_SECURE ||
524        security_level == ToolbarModel::SECURE) {
525      color = SecureSchemeColor();
526    } else if (security_level == ToolbarModel::SECURITY_ERROR) {
527      color = SecurityErrorSchemeColor();
528      // Add a strikethrough through the scheme.
529      [as addAttribute:NSStrikethroughStyleAttributeName
530                 value:[NSNumber numberWithInt:NSUnderlineStyleSingle]
531                 range:ComponentToNSRange(scheme)];
532    } else if (security_level == ToolbarModel::SECURITY_WARNING) {
533      color = BaseTextColor();
534    } else {
535      NOTREACHED();
536      color = BaseTextColor();
537    }
538    [as addAttribute:NSForegroundColorAttributeName value:color
539               range:ComponentToNSRange(scheme)];
540  }
541}
542
543void OmniboxViewMac::OnTemporaryTextMaybeChanged(
544    const base::string16& display_text,
545    bool save_original_selection,
546    bool notify_text_changed) {
547  if (save_original_selection)
548    saved_temporary_selection_ = GetSelectedRange();
549
550  SetWindowTextAndCaretPos(display_text, display_text.size(), false, false);
551  if (notify_text_changed)
552    model()->OnChanged();
553  [field_ clearUndoChain];
554}
555
556bool OmniboxViewMac::OnInlineAutocompleteTextMaybeChanged(
557    const base::string16& display_text,
558    size_t user_text_length) {
559  // TODO(shess): Make sure that this actually works.  The round trip
560  // to native form and back may mean that it's the same but not the
561  // same.
562  if (display_text == GetText())
563    return false;
564
565  DCHECK_LE(user_text_length, display_text.size());
566  const NSRange range =
567      NSMakeRange(user_text_length, display_text.size() - user_text_length);
568  SetTextAndSelectedRange(display_text, range);
569  model()->OnChanged();
570  [field_ clearUndoChain];
571
572  return true;
573}
574
575void OmniboxViewMac::OnInlineAutocompleteTextCleared() {
576}
577
578void OmniboxViewMac::OnRevertTemporaryText() {
579  SetSelectedRange(saved_temporary_selection_);
580  // We got here because the user hit the Escape key. We explicitly don't call
581  // TextChanged(), since OmniboxPopupModel::ResetToDefaultMatch() has already
582  // been called by now, and it would've called TextChanged() if it was
583  // warranted.
584}
585
586bool OmniboxViewMac::IsFirstResponder() const {
587  return [field_ currentEditor] != nil ? true : false;
588}
589
590void OmniboxViewMac::OnBeforePossibleChange() {
591  // We should only arrive here when the field is focussed.
592  DCHECK(IsFirstResponder());
593
594  selection_before_change_ = GetSelectedRange();
595  text_before_change_ = GetText();
596  marked_range_before_change_ = GetMarkedRange();
597}
598
599bool OmniboxViewMac::OnAfterPossibleChange() {
600  // We should only arrive here when the field is focussed.
601  DCHECK(IsFirstResponder());
602
603  const NSRange new_selection(GetSelectedRange());
604  const base::string16 new_text(GetText());
605  const size_t length = new_text.length();
606
607  const bool selection_differs =
608      (new_selection.length || selection_before_change_.length) &&
609      !NSEqualRanges(new_selection, selection_before_change_);
610  const bool at_end_of_edit = (length == new_selection.location);
611  const bool text_differs = (new_text != text_before_change_) ||
612      !NSEqualRanges(marked_range_before_change_, GetMarkedRange());
613
614  // When the user has deleted text, we don't allow inline
615  // autocomplete.  This is assumed if the text has gotten shorter AND
616  // the selection has shifted towards the front of the text.  During
617  // normal typing the text will almost always be shorter (as the new
618  // input replaces the autocomplete suggestion), but in that case the
619  // selection point will have moved towards the end of the text.
620  // TODO(shess): In our implementation, we can catch -deleteBackward:
621  // and other methods to provide positive knowledge that a delete
622  // occured, rather than intuiting it from context.  Consider whether
623  // that would be a stronger approach.
624  const bool just_deleted_text =
625      (length < text_before_change_.length() &&
626       new_selection.location <= selection_before_change_.location);
627
628  delete_at_end_pressed_ = false;
629
630  const bool something_changed = model()->OnAfterPossibleChange(
631      text_before_change_, new_text, new_selection.location,
632      NSMaxRange(new_selection), selection_differs, text_differs,
633      just_deleted_text, !IsImeComposing());
634
635  if (delete_was_pressed_ && at_end_of_edit)
636    delete_at_end_pressed_ = true;
637
638  // Restyle in case the user changed something.
639  // TODO(shess): I believe there are multiple-redraw cases, here.
640  // Linux watches for something_changed && text_differs, but that
641  // fails for us in case you copy the URL and paste the identical URL
642  // back (we'll lose the styling).
643  TextChanged();
644
645  delete_was_pressed_ = false;
646
647  return something_changed;
648}
649
650gfx::NativeView OmniboxViewMac::GetNativeView() const {
651  return field_;
652}
653
654gfx::NativeView OmniboxViewMac::GetRelativeWindowForPopup() const {
655  // Not used on mac.
656  NOTREACHED();
657  return NULL;
658}
659
660void OmniboxViewMac::SetGrayTextAutocompletion(
661    const base::string16& suggest_text) {
662  if (suggest_text == suggest_text_)
663    return;
664  suggest_text_ = suggest_text;
665  [field_ setGrayTextAutocompletion:base::SysUTF16ToNSString(suggest_text)
666                          textColor:SuggestTextColor()];
667}
668
669base::string16 OmniboxViewMac::GetGrayTextAutocompletion() const {
670  return suggest_text_;
671}
672
673int OmniboxViewMac::GetTextWidth() const {
674  // Not used on mac.
675  NOTREACHED();
676  return 0;
677}
678
679int OmniboxViewMac::GetWidth() const {
680  return ceil([field_ bounds].size.width);
681}
682
683bool OmniboxViewMac::IsImeComposing() const {
684  return [(NSTextView*)[field_ currentEditor] hasMarkedText];
685}
686
687void OmniboxViewMac::OnDidBeginEditing() {
688  // We should only arrive here when the field is focussed.
689  DCHECK([field_ currentEditor]);
690}
691
692void OmniboxViewMac::OnBeforeChange() {
693  // Capture the current state.
694  OnBeforePossibleChange();
695}
696
697void OmniboxViewMac::OnDidChange() {
698  // Figure out what changed and notify the model.
699  OnAfterPossibleChange();
700}
701
702void OmniboxViewMac::OnDidEndEditing() {
703  ClosePopup();
704}
705
706bool OmniboxViewMac::OnDoCommandBySelector(SEL cmd) {
707  if (cmd == @selector(deleteForward:))
708    delete_was_pressed_ = true;
709
710  if (cmd == @selector(moveDown:)) {
711    model()->OnUpOrDownKeyPressed(1);
712    return true;
713  }
714
715  if (cmd == @selector(moveUp:)) {
716    model()->OnUpOrDownKeyPressed(-1);
717    return true;
718  }
719
720  if (model()->popup_model()->IsOpen()) {
721    if (cmd == @selector(insertBacktab:)) {
722      if (model()->popup_model()->selected_line_state() ==
723            OmniboxPopupModel::KEYWORD) {
724        model()->ClearKeyword(GetText());
725        return true;
726      } else {
727        model()->OnUpOrDownKeyPressed(-1);
728        return true;
729      }
730    }
731
732    if ((cmd == @selector(insertTab:) ||
733        cmd == @selector(insertTabIgnoringFieldEditor:)) &&
734        !model()->is_keyword_hint()) {
735      model()->OnUpOrDownKeyPressed(1);
736      return true;
737    }
738  }
739
740  if (cmd == @selector(moveRight:)) {
741    // Only commit suggested text if the cursor is all the way to the right and
742    // there is no selection.
743    if (suggest_text_.length() > 0 && IsCaretAtEnd()) {
744      model()->CommitSuggestedText();
745      return true;
746    }
747  }
748
749  if (cmd == @selector(scrollPageDown:)) {
750    model()->OnUpOrDownKeyPressed(model()->result().size());
751    return true;
752  }
753
754  if (cmd == @selector(scrollPageUp:)) {
755    model()->OnUpOrDownKeyPressed(-model()->result().size());
756    return true;
757  }
758
759  if (cmd == @selector(cancelOperation:)) {
760    return model()->OnEscapeKeyPressed();
761  }
762
763  if ((cmd == @selector(insertTab:) ||
764      cmd == @selector(insertTabIgnoringFieldEditor:)) &&
765      model()->is_keyword_hint()) {
766    return model()->AcceptKeyword(ENTERED_KEYWORD_MODE_VIA_TAB);
767  }
768
769  // |-noop:| is sent when the user presses Cmd+Return. Override the no-op
770  // behavior with the proper WindowOpenDisposition.
771  NSEvent* event = [NSApp currentEvent];
772  if (cmd == @selector(insertNewline:) ||
773     (cmd == @selector(noop:) &&
774      ([event type] == NSKeyDown || [event type] == NSKeyUp) &&
775      [event keyCode] == kVK_Return)) {
776    WindowOpenDisposition disposition =
777        ui::WindowOpenDispositionFromNSEvent(event);
778    model()->AcceptInput(disposition, false);
779    // Opening a URL in a background tab should also revert the omnibox contents
780    // to their original state.  We cannot do a blanket revert in OpenURL()
781    // because middle-clicks also open in a new background tab, but those should
782    // not revert the omnibox text.
783    RevertAll();
784    return true;
785  }
786
787  // Option-Return
788  if (cmd == @selector(insertNewlineIgnoringFieldEditor:)) {
789    model()->AcceptInput(NEW_FOREGROUND_TAB, false);
790    return true;
791  }
792
793  // When the user does Control-Enter, the existing content has "www."
794  // prepended and ".com" appended.  model() should already have
795  // received notification when the Control key was depressed, but it
796  // is safe to tell it twice.
797  if (cmd == @selector(insertLineBreak:)) {
798    OnControlKeyChanged(true);
799    WindowOpenDisposition disposition =
800        ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
801    model()->AcceptInput(disposition, false);
802    return true;
803  }
804
805  if (cmd == @selector(deleteBackward:)) {
806    if (OnBackspacePressed()) {
807      return true;
808    }
809  }
810
811  if (cmd == @selector(deleteForward:)) {
812    const NSUInteger modifiers = [[NSApp currentEvent] modifierFlags];
813    if ((modifiers & NSShiftKeyMask) != 0) {
814      if (model()->popup_model()->IsOpen()) {
815        model()->popup_model()->TryDeletingCurrentItem();
816        return true;
817      }
818    }
819  }
820
821  return false;
822}
823
824void OmniboxViewMac::OnSetFocus(bool control_down) {
825  model()->OnSetFocus(control_down);
826  controller()->OnSetFocus();
827
828  HandleOriginChipMouseRelease();
829}
830
831void OmniboxViewMac::OnKillFocus() {
832  // Tell the model to reset itself.
833  model()->OnWillKillFocus(NULL);
834  model()->OnKillFocus();
835
836  OnDidKillFocus();
837}
838
839void OmniboxViewMac::OnMouseDown(NSInteger button_number) {
840  // Restore caret visibility whenever the user clicks in the the omnibox. This
841  // is not always covered by OnSetFocus() because when clicking while the
842  // omnibox has invisible focus does not trigger a new OnSetFocus() call.
843  if (button_number == 0 || button_number == 1)
844    model()->SetCaretVisibility(true);
845}
846
847bool OmniboxViewMac::ShouldSelectAllOnMouseDown() {
848  return !controller()->GetToolbarModel()->WouldPerformSearchTermReplacement(
849      false);
850}
851
852bool OmniboxViewMac::CanCopy() {
853  const NSRange selection = GetSelectedRange();
854  return selection.length > 0;
855}
856
857void OmniboxViewMac::CopyToPasteboard(NSPasteboard* pb) {
858  DCHECK(CanCopy());
859
860  const NSRange selection = GetSelectedRange();
861  base::string16 text = base::SysNSStringToUTF16(
862      [[field_ stringValue] substringWithRange:selection]);
863
864  // Copy the URL unless this is the search URL and it's being replaced by the
865  // Extended Instant API.
866  GURL url;
867  bool write_url = false;
868  if (!controller()->GetToolbarModel()->WouldPerformSearchTermReplacement(
869      false)) {
870    model()->AdjustTextForCopy(selection.location, IsSelectAll(), &text, &url,
871                               &write_url);
872  }
873
874  if (IsSelectAll())
875    UMA_HISTOGRAM_COUNTS(OmniboxEditModel::kCutOrCopyAllTextHistogram, 1);
876
877  NSString* nstext = base::SysUTF16ToNSString(text);
878  [pb declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
879  [pb setString:nstext forType:NSStringPboardType];
880
881  if (write_url) {
882    [pb declareURLPasteboardWithAdditionalTypes:[NSArray array] owner:nil];
883    [pb setDataForURL:base::SysUTF8ToNSString(url.spec()) title:nstext];
884  }
885}
886
887void OmniboxViewMac::ShowURL() {
888  DCHECK(ShouldEnableShowURL());
889  OmniboxView::ShowURL();
890}
891
892void OmniboxViewMac::OnPaste() {
893  // This code currently expects |field_| to be focussed.
894  DCHECK([field_ currentEditor]);
895
896  base::string16 text = GetClipboardText();
897  if (text.empty()) {
898    return;
899  }
900  NSString* s = base::SysUTF16ToNSString(text);
901
902  // -shouldChangeTextInRange:* and -didChangeText are documented in
903  // NSTextView as things you need to do if you write additional
904  // user-initiated editing functions.  They cause the appropriate
905  // delegate methods to be called.
906  // TODO(shess): It would be nice to separate the Cocoa-specific code
907  // from the Chrome-specific code.
908  NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]);
909  const NSRange selectedRange = GetSelectedRange();
910  if ([editor shouldChangeTextInRange:selectedRange replacementString:s]) {
911    // Record this paste, so we can do different behavior.
912    model()->OnPaste();
913
914    // Force a Paste operation to trigger the text_changed code in
915    // OnAfterPossibleChange(), even if identical contents are pasted
916    // into the text box.
917    text_before_change_.clear();
918
919    [editor replaceCharactersInRange:selectedRange withString:s];
920    [editor didChangeText];
921  }
922}
923
924// TODO(dominich): Move to OmniboxView base class? Currently this is defined on
925// the AutocompleteTextFieldObserver but the logic is shared between all
926// platforms. Some refactor might be necessary to simplify this. Or at least
927// this method could call the OmniboxView version.
928bool OmniboxViewMac::ShouldEnableShowURL() {
929  return controller()->GetToolbarModel()->WouldReplaceURL();
930}
931
932bool OmniboxViewMac::CanPasteAndGo() {
933  return model()->CanPasteAndGo(GetClipboardText());
934}
935
936int OmniboxViewMac::GetPasteActionStringId() {
937  base::string16 text(GetClipboardText());
938  DCHECK(model()->CanPasteAndGo(text));
939  return model()->IsPasteAndSearch(text) ?
940      IDS_PASTE_AND_SEARCH : IDS_PASTE_AND_GO;
941}
942
943void OmniboxViewMac::OnPasteAndGo() {
944  base::string16 text(GetClipboardText());
945  if (model()->CanPasteAndGo(text))
946    model()->PasteAndGo(text);
947}
948
949void OmniboxViewMac::OnFrameChanged() {
950  // TODO(shess): UpdatePopupAppearance() is called frequently, so it
951  // should be really cheap, but in this case we could probably make
952  // things even cheaper by refactoring between the popup-placement
953  // code and the matrix-population code.
954  popup_view_->UpdatePopupAppearance();
955
956  // Give controller a chance to rearrange decorations.
957  model()->OnChanged();
958}
959
960void OmniboxViewMac::ClosePopup() {
961  OmniboxView::CloseOmniboxPopup();
962}
963
964bool OmniboxViewMac::OnBackspacePressed() {
965  // Don't intercept if not in keyword search mode.
966  if (model()->is_keyword_hint() || model()->keyword().empty()) {
967    return false;
968  }
969
970  // Don't intercept if there is a selection, or the cursor isn't at
971  // the leftmost position.
972  const NSRange selection = GetSelectedRange();
973  if (selection.length > 0 || selection.location > 0) {
974    return false;
975  }
976
977  // We're showing a keyword and the user pressed backspace at the
978  // beginning of the text.  Delete the selected keyword.
979  model()->ClearKeyword(GetText());
980  return true;
981}
982
983NSRange OmniboxViewMac::SelectionRangeForProposedRange(NSRange proposed_range) {
984  return proposed_range;
985}
986
987void OmniboxViewMac::OnControlKeyChanged(bool pressed) {
988  model()->OnControlKeyChanged(pressed);
989}
990
991void OmniboxViewMac::FocusLocation(bool select_all) {
992  if ([field_ isEditable]) {
993    // If the text field has a field editor, it's the first responder, meaning
994    // that it's already focused. makeFirstResponder: will select all, so only
995    // call it if this behavior is desired.
996    if (select_all || ![field_ currentEditor])
997      [[field_ window] makeFirstResponder:field_];
998    DCHECK_EQ([field_ currentEditor], [[field_ window] firstResponder]);
999  }
1000}
1001
1002// static
1003NSFont* OmniboxViewMac::GetFieldFont(int style) {
1004  // This value should be kept in sync with InstantPage::InitializeFonts.
1005  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
1006  return rb.GetFontList(ui::ResourceBundle::BaseFont).Derive(1, style)
1007      .GetPrimaryFont().GetNativeFont();
1008}
1009
1010int OmniboxViewMac::GetOmniboxTextLength() const {
1011  return static_cast<int>(GetTextLength());
1012}
1013
1014NSUInteger OmniboxViewMac::GetTextLength() const {
1015  return [field_ currentEditor] ?  [[[field_ currentEditor] string] length] :
1016                                   [[field_ stringValue] length];
1017}
1018
1019bool OmniboxViewMac::IsCaretAtEnd() const {
1020  const NSRange selection = GetSelectedRange();
1021  return NSMaxRange(selection) == GetTextLength();
1022}
1023