1// Copyright (c) 2011 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/autocomplete/autocomplete_edit_view_mac.h"
6
7#include <Carbon/Carbon.h>  // kVK_Return
8
9#include "app/mac/nsimage_cache.h"
10#include "base/string_util.h"
11#include "base/sys_string_conversions.h"
12#include "base/utf_string_conversions.h"
13#include "chrome/browser/autocomplete/autocomplete_edit.h"
14#include "chrome/browser/autocomplete/autocomplete_match.h"
15#include "chrome/browser/autocomplete/autocomplete_popup_model.h"
16#include "chrome/browser/autocomplete/autocomplete_popup_view_mac.h"
17#include "chrome/browser/browser_process.h"
18#include "chrome/browser/ui/cocoa/event_utils.h"
19#include "chrome/browser/ui/toolbar/toolbar_model.h"
20#include "content/browser/tab_contents/tab_contents.h"
21#include "grit/generated_resources.h"
22#include "grit/theme_resources.h"
23#include "net/base/escape.h"
24#import "third_party/mozilla/NSPasteboard+Utils.h"
25#include "ui/base/clipboard/clipboard.h"
26#include "ui/base/resource/resource_bundle.h"
27#include "ui/gfx/image.h"
28#include "ui/gfx/rect.h"
29
30// Focus-handling between |field_| and |model_| is a bit subtle.
31// Other platforms detect change of focus, which is inconvenient
32// without subclassing NSTextField (even with a subclass, the use of a
33// field editor may complicate things).
34//
35// |model_| doesn't actually do anything when it gains focus, it just
36// initializes.  Visible activity happens only after the user edits.
37// NSTextField delegate receives messages around starting and ending
38// edits, so that suffices to catch focus changes.  Since all calls
39// into |model_| start from AutocompleteEditViewMac, in the worst case
40// we can add code to sync up the sense of focus as needed.
41//
42// I've added DCHECK(IsFirstResponder()) in the places which I believe
43// should only be reachable when |field_| is being edited.  If these
44// fire, it probably means someone unexpected is calling into
45// |model_|.
46//
47// Other platforms don't appear to have the sense of "key window" that
48// Mac does (I believe their fields lose focus when the window loses
49// focus).  Rather than modifying focus outside the control's edit
50// scope, when the window resigns key the autocomplete popup is
51// closed.  |model_| still believes it has focus, and the popup will
52// be regenerated on the user's next edit.  That seems to match how
53// things work on other platforms.
54
55namespace {
56
57// TODO(shess): This is ugly, find a better way.  Using it right now
58// so that I can crib from gtk and still be able to see that I'm using
59// the same values easily.
60NSColor* ColorWithRGBBytes(int rr, int gg, int bb) {
61  DCHECK_LE(rr, 255);
62  DCHECK_LE(bb, 255);
63  DCHECK_LE(gg, 255);
64  return [NSColor colorWithCalibratedRed:static_cast<float>(rr)/255.0
65                                   green:static_cast<float>(gg)/255.0
66                                    blue:static_cast<float>(bb)/255.0
67                                   alpha:1.0];
68}
69
70NSColor* HostTextColor() {
71  return [NSColor blackColor];
72}
73NSColor* BaseTextColor() {
74  return [NSColor darkGrayColor];
75}
76NSColor* SuggestTextColor() {
77  return [NSColor grayColor];
78}
79NSColor* SecureSchemeColor() {
80  return ColorWithRGBBytes(0x07, 0x95, 0x00);
81}
82NSColor* SecurityErrorSchemeColor() {
83  return ColorWithRGBBytes(0xa2, 0x00, 0x00);
84}
85
86// Store's the model and view state across tab switches.
87struct AutocompleteEditViewMacState {
88  AutocompleteEditViewMacState(const AutocompleteEditModel::State model_state,
89                               const bool has_focus, const NSRange& selection)
90      : model_state(model_state),
91        has_focus(has_focus),
92        selection(selection) {
93  }
94
95  const AutocompleteEditModel::State model_state;
96  const bool has_focus;
97  const NSRange selection;
98};
99
100// Returns a lazily initialized property bag accessor for saving our
101// state in a TabContents.  When constructed |accessor| generates a
102// globally-unique id used to index into the per-tab PropertyBag used
103// to store the state data.
104PropertyAccessor<AutocompleteEditViewMacState>* GetStateAccessor() {
105  static PropertyAccessor<AutocompleteEditViewMacState> accessor;
106  return &accessor;
107}
108
109// Accessors for storing and getting the state from the tab.
110void StoreStateToTab(TabContents* tab,
111                     const AutocompleteEditViewMacState& state) {
112  GetStateAccessor()->SetProperty(tab->property_bag(), state);
113}
114const AutocompleteEditViewMacState* GetStateFromTab(const TabContents* tab) {
115  return GetStateAccessor()->GetProperty(tab->property_bag());
116}
117
118// Helper to make converting url_parse ranges to NSRange easier to
119// read.
120NSRange ComponentToNSRange(const url_parse::Component& component) {
121  return NSMakeRange(static_cast<NSInteger>(component.begin),
122                     static_cast<NSInteger>(component.len));
123}
124
125}  // namespace
126
127// static
128NSImage* AutocompleteEditViewMac::ImageForResource(int resource_id) {
129  NSString* image_name = nil;
130
131  switch(resource_id) {
132    // From the autocomplete popup, or the star icon at the RHS of the
133    // text field.
134    case IDR_STAR: image_name = @"star.pdf"; break;
135    case IDR_STAR_LIT: image_name = @"star_lit.pdf"; break;
136
137    // Values from |AutocompleteMatch::TypeToIcon()|.
138    case IDR_OMNIBOX_SEARCH:
139      image_name = @"omnibox_search.pdf"; break;
140    case IDR_OMNIBOX_HTTP:
141      image_name = @"omnibox_http.pdf"; break;
142    case IDR_OMNIBOX_HISTORY:
143      image_name = @"omnibox_history.pdf"; break;
144    case IDR_OMNIBOX_EXTENSION_APP:
145      image_name = @"omnibox_extension_app.pdf"; break;
146
147    // Values from |ToolbarModel::GetIcon()|.
148    case IDR_OMNIBOX_HTTPS_VALID:
149      image_name = @"omnibox_https_valid.pdf"; break;
150    case IDR_OMNIBOX_HTTPS_WARNING:
151      image_name = @"omnibox_https_warning.pdf"; break;
152    case IDR_OMNIBOX_HTTPS_INVALID:
153      image_name = @"omnibox_https_invalid.pdf"; break;
154  }
155
156  if (image_name) {
157    if (NSImage* image = app::mac::GetCachedImageWithName(image_name)) {
158      return image;
159    } else {
160      NOTREACHED()
161          << "Missing image for " << base::SysNSStringToUTF8(image_name);
162    }
163  }
164
165  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
166  return rb.GetNativeImageNamed(resource_id);
167}
168
169AutocompleteEditViewMac::AutocompleteEditViewMac(
170    AutocompleteEditController* controller,
171    ToolbarModel* toolbar_model,
172    Profile* profile,
173    CommandUpdater* command_updater,
174    AutocompleteTextField* field)
175    : model_(new AutocompleteEditModel(this, controller, profile)),
176      popup_view_(new AutocompletePopupViewMac(this, model_.get(), profile,
177                                               field)),
178      controller_(controller),
179      toolbar_model_(toolbar_model),
180      command_updater_(command_updater),
181      field_(field),
182      suggest_text_length_(0),
183      delete_was_pressed_(false),
184      delete_at_end_pressed_(false),
185      line_height_(0) {
186  DCHECK(controller);
187  DCHECK(toolbar_model);
188  DCHECK(profile);
189  DCHECK(command_updater);
190  DCHECK(field);
191  [field_ setObserver:this];
192
193  // Needed so that editing doesn't lose the styling.
194  [field_ setAllowsEditingTextAttributes:YES];
195
196  // Get the appropriate line height for the font that we use.
197  scoped_nsobject<NSLayoutManager>
198      layoutManager([[NSLayoutManager alloc] init]);
199  [layoutManager setUsesScreenFonts:YES];
200  line_height_ = [layoutManager defaultLineHeightForFont:GetFieldFont()];
201  DCHECK_GT(line_height_, 0);
202}
203
204AutocompleteEditViewMac::~AutocompleteEditViewMac() {
205  // Destroy popup view before this object in case it tries to call us
206  // back in the destructor.  Likewise for destroying the model before
207  // this object.
208  popup_view_.reset();
209  model_.reset();
210
211  // Disconnect from |field_|, it outlives this object.
212  [field_ setObserver:NULL];
213}
214
215AutocompleteEditModel* AutocompleteEditViewMac::model() {
216  return model_.get();
217}
218
219const AutocompleteEditModel* AutocompleteEditViewMac::model() const {
220  return model_.get();
221}
222
223void AutocompleteEditViewMac::SaveStateToTab(TabContents* tab) {
224  DCHECK(tab);
225
226  const bool hasFocus = [field_ currentEditor] ? true : false;
227
228  NSRange range;
229  if (hasFocus) {
230    range = GetSelectedRange();
231  } else {
232    // If we are not focussed, there is no selection.  Manufacture
233    // something reasonable in case it starts to matter in the future.
234    range = NSMakeRange(0, GetTextLength());
235  }
236
237  AutocompleteEditViewMacState state(model_->GetStateForTabSwitch(),
238                                     hasFocus, range);
239  StoreStateToTab(tab, state);
240}
241
242void AutocompleteEditViewMac::Update(
243    const TabContents* tab_for_state_restoring) {
244  // TODO(shess): It seems like if the tab is non-NULL, then this code
245  // shouldn't need to be called at all.  When coded that way, I find
246  // that the field isn't always updated correctly.  Figure out why
247  // this is.  Maybe this method should be refactored into more
248  // specific cases.
249  const bool user_visible =
250      model_->UpdatePermanentText(WideToUTF16Hack(toolbar_model_->GetText()));
251
252  if (tab_for_state_restoring) {
253    RevertAll();
254
255    const AutocompleteEditViewMacState* state =
256        GetStateFromTab(tab_for_state_restoring);
257    if (state) {
258      // Should restore the user's text via SetUserText().
259      model_->RestoreState(state->model_state);
260
261      // Restore focus and selection if they were present when the tab
262      // was switched away.
263      if (state->has_focus) {
264        // TODO(shess): Unfortunately, there is no safe way to update
265        // this because TabStripController -selectTabWithContents:* is
266        // also messing with focus.  Both parties need to agree to
267        // store existing state before anyone tries to setup the new
268        // state.  Anyhow, it would look something like this.
269#if 0
270        [[field_ window] makeFirstResponder:field_];
271        [[field_ currentEditor] setSelectedRange:state->selection];
272#endif
273      }
274    }
275  } else if (user_visible) {
276    // Restore everything to the baseline look.
277    RevertAll();
278    // TODO(shess): Figure out how this case is used, to make sure
279    // we're getting the selection and popup right.
280
281  } else {
282    // TODO(shess): This corresponds to _win and _gtk, except those
283    // guard it with a test for whether the security level changed.
284    // But AFAICT, that can only change if the text changed, and that
285    // code compares the toolbar_model_ security level with the local
286    // security level.  Dig in and figure out why this isn't a no-op
287    // that should go away.
288    EmphasizeURLComponents();
289  }
290}
291
292void AutocompleteEditViewMac::OpenURL(const GURL& url,
293                                      WindowOpenDisposition disposition,
294                                      PageTransition::Type transition,
295                                      const GURL& alternate_nav_url,
296                                      size_t selected_line,
297                                      const string16& keyword) {
298  // TODO(shess): Why is the caller passing an invalid url in the
299  // first place?  Make sure that case isn't being dropped on the
300  // floor.
301  if (!url.is_valid()) {
302    return;
303  }
304
305  model_->OpenURL(url, disposition, transition, alternate_nav_url,
306                  selected_line, keyword);
307}
308
309string16 AutocompleteEditViewMac::GetText() const {
310  return base::SysNSStringToUTF16(GetNonSuggestTextSubstring());
311}
312
313bool AutocompleteEditViewMac::IsEditingOrEmpty() const {
314  return model_->user_input_in_progress() || !GetTextLength();
315}
316
317int AutocompleteEditViewMac::GetIcon() const {
318  return IsEditingOrEmpty() ?
319      AutocompleteMatch::TypeToIcon(model_->CurrentTextType()) :
320      toolbar_model_->GetIcon();
321}
322
323void AutocompleteEditViewMac::SetUserText(const string16& text) {
324  SetUserText(text, text, true);
325}
326
327void AutocompleteEditViewMac::SetUserText(const string16& text,
328                                          const string16& display_text,
329                                          bool update_popup) {
330  model_->SetUserText(text);
331  // TODO(shess): TODO below from gtk.
332  // TODO(deanm): something about selection / focus change here.
333  SetText(display_text);
334  if (update_popup) {
335    UpdatePopup();
336  }
337  model_->OnChanged();
338}
339
340NSRange AutocompleteEditViewMac::GetSelectedRange() const {
341  return [[field_ currentEditor] selectedRange];
342}
343
344NSRange AutocompleteEditViewMac::GetMarkedRange() const {
345  DCHECK([field_ currentEditor]);
346  return [(NSTextView*)[field_ currentEditor] markedRange];
347}
348
349void AutocompleteEditViewMac::SetSelectedRange(const NSRange range) {
350  // This can be called when we don't have focus.  For instance, when
351  // the user clicks the "Go" button.
352  if (model_->has_focus()) {
353    // TODO(shess): If |model_| thinks we have focus, this should not
354    // be necessary.  Try to convert to DCHECK(IsFirstResponder()).
355    if (![field_ currentEditor]) {
356      [[field_ window] makeFirstResponder:field_];
357    }
358
359    // TODO(shess): What if it didn't get first responder, and there is
360    // no field editor?  This will do nothing.  Well, at least it won't
361    // crash.  Think of something more productive to do, or prove that
362    // it cannot occur and DCHECK appropriately.
363    [[field_ currentEditor] setSelectedRange:range];
364  }
365}
366
367void AutocompleteEditViewMac::SetWindowTextAndCaretPos(const string16& text,
368                                                       size_t caret_pos) {
369  DCHECK_LE(caret_pos, text.size());
370  SetTextAndSelectedRange(text, NSMakeRange(caret_pos, caret_pos));
371}
372
373void AutocompleteEditViewMac::SetForcedQuery() {
374  // We need to do this first, else |SetSelectedRange()| won't work.
375  FocusLocation(true);
376
377  const string16 current_text(GetText());
378  const size_t start = current_text.find_first_not_of(kWhitespaceUTF16);
379  if (start == string16::npos || (current_text[start] != '?')) {
380    SetUserText(ASCIIToUTF16("?"));
381  } else {
382    NSRange range = NSMakeRange(start + 1, current_text.size() - start - 1);
383    [[field_ currentEditor] setSelectedRange:range];
384  }
385}
386
387bool AutocompleteEditViewMac::IsSelectAll() {
388  if (![field_ currentEditor])
389    return true;
390  const NSRange all_range = NSMakeRange(0, GetTextLength());
391  return NSEqualRanges(all_range, GetSelectedRange());
392}
393
394bool AutocompleteEditViewMac::DeleteAtEndPressed() {
395  return delete_at_end_pressed_;
396}
397
398void AutocompleteEditViewMac::GetSelectionBounds(string16::size_type* start,
399                                                 string16::size_type* end) {
400  if (![field_ currentEditor]) {
401    *start = *end = 0;
402    return;
403  }
404
405  const NSRange selected_range = GetSelectedRange();
406  *start = static_cast<size_t>(selected_range.location);
407  *end = static_cast<size_t>(NSMaxRange(selected_range));
408}
409
410void AutocompleteEditViewMac::SelectAll(bool reversed) {
411  // TODO(shess): Figure out what |reversed| implies.  The gtk version
412  // has it imply inverting the selection front to back, but I don't
413  // even know if that makes sense for Mac.
414
415  // TODO(shess): Verify that we should be stealing focus at this
416  // point.
417  SetSelectedRange(NSMakeRange(0, GetTextLength()));
418}
419
420void AutocompleteEditViewMac::RevertAll() {
421  ClosePopup();
422  model_->Revert();
423  model_->OnChanged();
424  [field_ clearUndoChain];
425}
426
427void AutocompleteEditViewMac::UpdatePopup() {
428  model_->SetInputInProgress(true);
429  if (!model_->has_focus())
430    return;
431
432  // Comment copied from AutocompleteEditViewWin::UpdatePopup():
433  // Don't inline autocomplete when:
434  //   * The user is deleting text
435  //   * The caret/selection isn't at the end of the text
436  //   * The user has just pasted in something that replaced all the text
437  //   * The user is trying to compose something in an IME
438  bool prevent_inline_autocomplete = IsImeComposing();
439  NSTextView* editor = (NSTextView*)[field_ currentEditor];
440  if (editor) {
441    if (NSMaxRange([editor selectedRange]) <
442        [[editor textStorage] length] - suggest_text_length_)
443      prevent_inline_autocomplete = true;
444  }
445
446  model_->StartAutocomplete([editor selectedRange].length != 0,
447                            prevent_inline_autocomplete);
448}
449
450void AutocompleteEditViewMac::ClosePopup() {
451  model_->StopAutocomplete();
452}
453
454void AutocompleteEditViewMac::SetFocus() {
455}
456
457void AutocompleteEditViewMac::SetText(const string16& display_text) {
458  // If we are setting the text directly, there cannot be any suggest text.
459  suggest_text_length_ = 0;
460  SetTextInternal(display_text);
461}
462
463void AutocompleteEditViewMac::SetTextInternal(
464    const string16& display_text) {
465  NSString* ss = base::SysUTF16ToNSString(display_text);
466  NSMutableAttributedString* as =
467      [[[NSMutableAttributedString alloc] initWithString:ss] autorelease];
468
469  ApplyTextAttributes(display_text, as);
470
471  [field_ setAttributedStringValue:as];
472
473  // TODO(shess): This may be an appropriate place to call:
474  //   model_->OnChanged();
475  // In the current implementation, this tells LocationBarViewMac to
476  // mess around with |model_| and update |field_|.  Unfortunately,
477  // when I look at our peer implementations, it's not entirely clear
478  // to me if this is safe.  SetTextInternal() is sort of an utility method,
479  // and different callers sometimes have different needs.  Research
480  // this issue so that it can be added safely.
481
482  // TODO(shess): Also, consider whether this code couldn't just
483  // manage things directly.  Windows uses a series of overlaid view
484  // objects to accomplish the hinting stuff that OnChanged() does, so
485  // it makes sense to have it in the controller that lays those
486  // things out.  Mac instead pushes the support into a custom
487  // text-field implementation.
488}
489
490void AutocompleteEditViewMac::SetTextAndSelectedRange(
491    const string16& display_text, const NSRange range) {
492  SetText(display_text);
493  SetSelectedRange(range);
494}
495
496NSString* AutocompleteEditViewMac::GetNonSuggestTextSubstring() const {
497  NSString* text = [field_ stringValue];
498  if (suggest_text_length_ > 0) {
499    NSUInteger length = [text length];
500
501    DCHECK_LE(suggest_text_length_, length);
502    text = [text substringToIndex:(length - suggest_text_length_)];
503  }
504  return text;
505}
506
507NSString* AutocompleteEditViewMac::GetSuggestTextSubstring() const {
508  if (suggest_text_length_ == 0)
509    return nil;
510
511  NSString* text = [field_ stringValue];
512  NSUInteger length = [text length];
513  DCHECK_LE(suggest_text_length_, length);
514  return [text substringFromIndex:(length - suggest_text_length_)];
515}
516
517void AutocompleteEditViewMac::EmphasizeURLComponents() {
518  NSTextView* editor = (NSTextView*)[field_ currentEditor];
519  // If the autocomplete text field is in editing mode, then we can just change
520  // its attributes through its editor. Otherwise, we simply reset its content.
521  if (editor) {
522    NSTextStorage* storage = [editor textStorage];
523    [storage beginEditing];
524
525    // Clear the existing attributes from the text storage, then
526    // overlay the appropriate Omnibox attributes.
527    [storage setAttributes:[NSDictionary dictionary]
528                     range:NSMakeRange(0, [storage length])];
529    ApplyTextAttributes(GetText(), storage);
530
531    [storage endEditing];
532  } else {
533    SetText(GetText());
534  }
535}
536
537void AutocompleteEditViewMac::ApplyTextAttributes(
538    const string16& display_text, NSMutableAttributedString* as) {
539  [as addAttribute:NSFontAttributeName value:GetFieldFont()
540             range:NSMakeRange(0, [as length])];
541
542  // Make a paragraph style locking in the standard line height as the maximum,
543  // otherwise the baseline may shift "downwards".
544  scoped_nsobject<NSMutableParagraphStyle>
545      paragraph_style([[NSMutableParagraphStyle alloc] init]);
546  [paragraph_style setMaximumLineHeight:line_height_];
547  [as addAttribute:NSParagraphStyleAttributeName value:paragraph_style
548             range:NSMakeRange(0, [as length])];
549
550  // Grey out the suggest text.
551  [as addAttribute:NSForegroundColorAttributeName value:SuggestTextColor()
552             range:NSMakeRange([as length] - suggest_text_length_,
553                               suggest_text_length_)];
554
555  url_parse::Component scheme, host;
556  AutocompleteInput::ParseForEmphasizeComponents(
557      display_text, model_->GetDesiredTLD(), &scheme, &host);
558  const bool emphasize = model_->CurrentTextIsURL() && (host.len > 0);
559  if (emphasize) {
560    [as addAttribute:NSForegroundColorAttributeName value:BaseTextColor()
561               range:NSMakeRange(0, [as length])];
562
563    [as addAttribute:NSForegroundColorAttributeName value:HostTextColor()
564               range:ComponentToNSRange(host)];
565  }
566
567  // TODO(shess): GTK has this as a member var, figure out why.
568  // [Could it be to not change if no change?  If so, I'm guessing
569  // AppKit may already handle that.]
570  const ToolbarModel::SecurityLevel security_level =
571      toolbar_model_->GetSecurityLevel();
572
573  // Emphasize the scheme for security UI display purposes (if necessary).
574  if (!model_->user_input_in_progress() && scheme.is_nonempty() &&
575      (security_level != ToolbarModel::NONE)) {
576    NSColor* color;
577    if (security_level == ToolbarModel::EV_SECURE ||
578        security_level == ToolbarModel::SECURE) {
579      color = SecureSchemeColor();
580    } else if (security_level == ToolbarModel::SECURITY_ERROR) {
581      color = SecurityErrorSchemeColor();
582      // Add a strikethrough through the scheme.
583      [as addAttribute:NSStrikethroughStyleAttributeName
584                 value:[NSNumber numberWithInt:NSUnderlineStyleSingle]
585                 range:ComponentToNSRange(scheme)];
586    } else if (security_level == ToolbarModel::SECURITY_WARNING) {
587      color = BaseTextColor();
588    } else {
589      NOTREACHED();
590      color = BaseTextColor();
591    }
592    [as addAttribute:NSForegroundColorAttributeName value:color
593               range:ComponentToNSRange(scheme)];
594  }
595}
596
597void AutocompleteEditViewMac::OnTemporaryTextMaybeChanged(
598    const string16& display_text, bool save_original_selection) {
599  if (save_original_selection)
600    saved_temporary_selection_ = GetSelectedRange();
601
602  suggest_text_length_ = 0;
603  SetWindowTextAndCaretPos(display_text, display_text.size());
604  model_->OnChanged();
605  [field_ clearUndoChain];
606}
607
608void AutocompleteEditViewMac::OnStartingIME() {
609  // Reset the suggest text just before starting an IME composition session,
610  // otherwise the IME composition may be interrupted when the suggest text
611  // gets reset by the IME composition change.
612  SetInstantSuggestion(string16(), false);
613}
614
615bool AutocompleteEditViewMac::OnInlineAutocompleteTextMaybeChanged(
616    const string16& display_text, size_t user_text_length) {
617  // TODO(shess): Make sure that this actually works.  The round trip
618  // to native form and back may mean that it's the same but not the
619  // same.
620  if (display_text == GetText())
621    return false;
622
623  DCHECK_LE(user_text_length, display_text.size());
624  const NSRange range =
625      NSMakeRange(user_text_length, display_text.size() - user_text_length);
626  SetTextAndSelectedRange(display_text, range);
627  model_->OnChanged();
628  [field_ clearUndoChain];
629
630  return true;
631}
632
633void AutocompleteEditViewMac::OnRevertTemporaryText() {
634  SetSelectedRange(saved_temporary_selection_);
635}
636
637bool AutocompleteEditViewMac::IsFirstResponder() const {
638  return [field_ currentEditor] != nil ? true : false;
639}
640
641void AutocompleteEditViewMac::OnBeforePossibleChange() {
642  // We should only arrive here when the field is focussed.
643  DCHECK(IsFirstResponder());
644
645  selection_before_change_ = GetSelectedRange();
646  text_before_change_ = GetText();
647  marked_range_before_change_ = GetMarkedRange();
648}
649
650bool AutocompleteEditViewMac::OnAfterPossibleChange() {
651  // We should only arrive here when the field is focussed.
652  DCHECK(IsFirstResponder());
653
654  const NSRange new_selection(GetSelectedRange());
655  const string16 new_text(GetText());
656  const size_t length = new_text.length();
657
658  const bool selection_differs =
659      (new_selection.length || selection_before_change_.length) &&
660      !NSEqualRanges(new_selection, selection_before_change_);
661  const bool at_end_of_edit = (length == new_selection.location);
662  const bool text_differs = (new_text != text_before_change_) ||
663      !NSEqualRanges(marked_range_before_change_, GetMarkedRange());
664
665  // When the user has deleted text, we don't allow inline
666  // autocomplete.  This is assumed if the text has gotten shorter AND
667  // the selection has shifted towards the front of the text.  During
668  // normal typing the text will almost always be shorter (as the new
669  // input replaces the autocomplete suggestion), but in that case the
670  // selection point will have moved towards the end of the text.
671  // TODO(shess): In our implementation, we can catch -deleteBackward:
672  // and other methods to provide positive knowledge that a delete
673  // occured, rather than intuiting it from context.  Consider whether
674  // that would be a stronger approach.
675  const bool just_deleted_text =
676      (length < text_before_change_.length() &&
677       new_selection.location <= selection_before_change_.location);
678
679  delete_at_end_pressed_ = false;
680
681  const bool something_changed = model_->OnAfterPossibleChange(
682      new_text, new_selection.location, NSMaxRange(new_selection),
683      selection_differs, text_differs, just_deleted_text,
684      !IsImeComposing());
685
686  if (delete_was_pressed_ && at_end_of_edit)
687    delete_at_end_pressed_ = true;
688
689  // Restyle in case the user changed something.
690  // TODO(shess): I believe there are multiple-redraw cases, here.
691  // Linux watches for something_changed && text_differs, but that
692  // fails for us in case you copy the URL and paste the identical URL
693  // back (we'll lose the styling).
694  EmphasizeURLComponents();
695  model_->OnChanged();
696
697  delete_was_pressed_ = false;
698
699  return something_changed;
700}
701
702gfx::NativeView AutocompleteEditViewMac::GetNativeView() const {
703  return field_;
704}
705
706CommandUpdater* AutocompleteEditViewMac::GetCommandUpdater() {
707  return command_updater_;
708}
709
710void AutocompleteEditViewMac::SetInstantSuggestion(
711    const string16& suggest_text,
712    bool animate_to_complete) {
713  NSString* text = GetNonSuggestTextSubstring();
714  bool needs_update = (suggest_text_length_ > 0);
715
716  // Append the new suggest text.
717  suggest_text_length_ = suggest_text.length();
718  if (suggest_text_length_ > 0) {
719    text = [text stringByAppendingString:base::SysUTF16ToNSString(
720               suggest_text)];
721    needs_update = true;
722  }
723
724  if (needs_update) {
725    NSRange current_range = GetSelectedRange();
726    SetTextInternal(base::SysNSStringToUTF16(text));
727    if (NSMaxRange(current_range) <= [text length] - suggest_text_length_)
728      SetSelectedRange(current_range);
729    else
730      SetSelectedRange(NSMakeRange([text length] - suggest_text_length_, 0));
731  }
732}
733
734string16 AutocompleteEditViewMac::GetInstantSuggestion() const {
735  return suggest_text_length_ ?
736      base::SysNSStringToUTF16(GetSuggestTextSubstring()) : string16();
737}
738
739int AutocompleteEditViewMac::TextWidth() const {
740  // Not used on mac.
741  NOTREACHED();
742  return 0;
743}
744
745bool AutocompleteEditViewMac::IsImeComposing() const {
746  return [(NSTextView*)[field_ currentEditor] hasMarkedText];
747}
748
749void AutocompleteEditViewMac::OnDidBeginEditing() {
750  // We should only arrive here when the field is focussed.
751  DCHECK([field_ currentEditor]);
752}
753
754void AutocompleteEditViewMac::OnBeforeChange() {
755  // Capture the current state.
756  OnBeforePossibleChange();
757}
758
759void AutocompleteEditViewMac::OnDidChange() {
760  // Figure out what changed and notify the model_.
761  OnAfterPossibleChange();
762}
763
764void AutocompleteEditViewMac::OnDidEndEditing() {
765  ClosePopup();
766}
767
768bool AutocompleteEditViewMac::OnDoCommandBySelector(SEL cmd) {
769  // We should only arrive here when the field is focussed.
770  DCHECK(IsFirstResponder());
771
772  if (cmd != @selector(moveRight:) &&
773      cmd != @selector(insertTab:) &&
774      cmd != @selector(insertTabIgnoringFieldEditor:)) {
775    // Reset the suggest text for any change other than key right or tab.
776    // TODO(rohitrao): This is here to prevent complications when editing text.
777    // See if this can be removed.
778    SetInstantSuggestion(string16(), false);
779  }
780
781  if (cmd == @selector(deleteForward:))
782    delete_was_pressed_ = true;
783
784  // Don't intercept up/down-arrow if the popup isn't open.
785  if (popup_view_->IsOpen()) {
786    if (cmd == @selector(moveDown:)) {
787      model_->OnUpOrDownKeyPressed(1);
788      return true;
789    }
790
791    if (cmd == @selector(moveUp:)) {
792      model_->OnUpOrDownKeyPressed(-1);
793      return true;
794    }
795  }
796
797  if (cmd == @selector(moveRight:)) {
798    // Only commit suggested text if the cursor is all the way to the right and
799    // there is no selection.
800    if (suggest_text_length_ > 0 && IsCaretAtEnd()) {
801      model_->CommitSuggestedText(true);
802      return true;
803    }
804  }
805
806  if (cmd == @selector(scrollPageDown:)) {
807    model_->OnUpOrDownKeyPressed(model_->result().size());
808    return true;
809  }
810
811  if (cmd == @selector(scrollPageUp:)) {
812    model_->OnUpOrDownKeyPressed(-model_->result().size());
813    return true;
814  }
815
816  if (cmd == @selector(cancelOperation:)) {
817    return model_->OnEscapeKeyPressed();
818  }
819
820  if (cmd == @selector(insertTab:) ||
821      cmd == @selector(insertTabIgnoringFieldEditor:)) {
822    if (model_->is_keyword_hint())
823      return model_->AcceptKeyword();
824
825    if (suggest_text_length_ > 0) {
826      model_->CommitSuggestedText(true);
827      return true;
828    }
829
830    if (!IsCaretAtEnd()) {
831      PlaceCaretAt(GetTextLength());
832      // OnDidChange() will not be triggered when setting selected range in this
833      // method, so we need to call it explicitly.
834      OnDidChange();
835      return true;
836    }
837
838    if (model_->AcceptCurrentInstantPreview())
839      return true;
840  }
841
842  // |-noop:| is sent when the user presses Cmd+Return. Override the no-op
843  // behavior with the proper WindowOpenDisposition.
844  NSEvent* event = [NSApp currentEvent];
845  if (cmd == @selector(insertNewline:) ||
846     (cmd == @selector(noop:) && [event keyCode] == kVK_Return)) {
847    WindowOpenDisposition disposition =
848        event_utils::WindowOpenDispositionFromNSEvent(event);
849    model_->AcceptInput(disposition, false);
850    // Opening a URL in a background tab should also revert the omnibox contents
851    // to their original state.  We cannot do a blanket revert in OpenURL()
852    // because middle-clicks also open in a new background tab, but those should
853    // not revert the omnibox text.
854    RevertAll();
855    return true;
856  }
857
858  // Option-Return
859  if (cmd == @selector(insertNewlineIgnoringFieldEditor:)) {
860    model_->AcceptInput(NEW_FOREGROUND_TAB, false);
861    return true;
862  }
863
864  // When the user does Control-Enter, the existing content has "www."
865  // prepended and ".com" appended.  |model_| should already have
866  // received notification when the Control key was depressed, but it
867  // is safe to tell it twice.
868  if (cmd == @selector(insertLineBreak:)) {
869    OnControlKeyChanged(true);
870    WindowOpenDisposition disposition =
871        event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
872    model_->AcceptInput(disposition, false);
873    return true;
874  }
875
876  if (cmd == @selector(deleteBackward:)) {
877    if (OnBackspacePressed()) {
878      return true;
879    }
880  }
881
882  if (cmd == @selector(deleteForward:)) {
883    const NSUInteger modifiers = [[NSApp currentEvent] modifierFlags];
884    if ((modifiers & NSShiftKeyMask) != 0) {
885      if (model_->popup_model()->IsOpen()) {
886        model_->popup_model()->TryDeletingCurrentItem();
887        return true;
888      }
889    }
890  }
891
892  return false;
893}
894
895void AutocompleteEditViewMac::OnSetFocus(bool control_down) {
896  model_->OnSetFocus(control_down);
897  controller_->OnSetFocus();
898}
899
900void AutocompleteEditViewMac::OnKillFocus() {
901  // Tell the model to reset itself.
902  model_->OnWillKillFocus(NULL);
903  model_->OnKillFocus();
904  controller_->OnKillFocus();
905}
906
907bool AutocompleteEditViewMac::CanCopy() {
908  const NSRange selection = GetSelectedRange();
909  return selection.length > 0;
910}
911
912void AutocompleteEditViewMac::CopyToPasteboard(NSPasteboard* pb) {
913  DCHECK(CanCopy());
914
915  const NSRange selection = GetSelectedRange();
916  string16 text = base::SysNSStringToUTF16(
917      [[field_ stringValue] substringWithRange:selection]);
918
919  GURL url;
920  bool write_url = false;
921  model_->AdjustTextForCopy(selection.location, IsSelectAll(), &text, &url,
922                            &write_url);
923
924  NSString* nstext = base::SysUTF16ToNSString(text);
925  [pb declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
926  [pb setString:nstext forType:NSStringPboardType];
927
928  if (write_url) {
929    [pb declareURLPasteboardWithAdditionalTypes:[NSArray array] owner:nil];
930    [pb setDataForURL:base::SysUTF8ToNSString(url.spec()) title:nstext];
931  }
932}
933
934void AutocompleteEditViewMac::OnPaste() {
935  // This code currently expects |field_| to be focussed.
936  DCHECK([field_ currentEditor]);
937
938  string16 text = GetClipboardText(g_browser_process->clipboard());
939  if (text.empty()) {
940    return;
941  }
942  NSString* s = base::SysUTF16ToNSString(text);
943
944  // -shouldChangeTextInRange:* and -didChangeText are documented in
945  // NSTextView as things you need to do if you write additional
946  // user-initiated editing functions.  They cause the appropriate
947  // delegate methods to be called.
948  // TODO(shess): It would be nice to separate the Cocoa-specific code
949  // from the Chrome-specific code.
950  NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]);
951  const NSRange selectedRange = GetSelectedRange();
952  if ([editor shouldChangeTextInRange:selectedRange replacementString:s]) {
953    // Record this paste, so we can do different behavior.
954    model_->on_paste();
955
956    // Force a Paste operation to trigger the text_changed code in
957    // OnAfterPossibleChange(), even if identical contents are pasted
958    // into the text box.
959    text_before_change_.clear();
960
961    [editor replaceCharactersInRange:selectedRange withString:s];
962    [editor didChangeText];
963  }
964}
965
966bool AutocompleteEditViewMac::CanPasteAndGo() {
967  return
968    model_->CanPasteAndGo(GetClipboardText(g_browser_process->clipboard()));
969}
970
971int AutocompleteEditViewMac::GetPasteActionStringId() {
972  DCHECK(CanPasteAndGo());
973
974  // Use PASTE_AND_SEARCH as the default fallback (although the DCHECK above
975  // should never trigger).
976  if (!model_->is_paste_and_search())
977    return IDS_PASTE_AND_GO;
978  else
979    return IDS_PASTE_AND_SEARCH;
980}
981
982void AutocompleteEditViewMac::OnPasteAndGo() {
983  if (CanPasteAndGo())
984    model_->PasteAndGo();
985}
986
987void AutocompleteEditViewMac::OnFrameChanged() {
988  // TODO(shess): UpdatePopupAppearance() is called frequently, so it
989  // should be really cheap, but in this case we could probably make
990  // things even cheaper by refactoring between the popup-placement
991  // code and the matrix-population code.
992  popup_view_->UpdatePopupAppearance();
993  model_->PopupBoundsChangedTo(popup_view_->GetTargetBounds());
994
995  // Give controller a chance to rearrange decorations.
996  model_->OnChanged();
997}
998
999bool AutocompleteEditViewMac::OnBackspacePressed() {
1000  // Don't intercept if not in keyword search mode.
1001  if (model_->is_keyword_hint() || model_->keyword().empty()) {
1002    return false;
1003  }
1004
1005  // Don't intercept if there is a selection, or the cursor isn't at
1006  // the leftmost position.
1007  const NSRange selection = GetSelectedRange();
1008  if (selection.length > 0 || selection.location > 0) {
1009    return false;
1010  }
1011
1012  // We're showing a keyword and the user pressed backspace at the
1013  // beginning of the text.  Delete the selected keyword.
1014  model_->ClearKeyword(GetText());
1015  return true;
1016}
1017
1018NSRange AutocompleteEditViewMac::SelectionRangeForProposedRange(
1019    NSRange proposed_range) {
1020  // Should never call this function unless editing is in progress.
1021  DCHECK([field_ currentEditor]);
1022
1023  if (![field_ currentEditor])
1024    return proposed_range;
1025
1026  // Do not use [field_ stringValue] here, as that forces a sync between the
1027  // field and the editor.  This sync will end up setting the selection, which
1028  // in turn calls this method, leading to an infinite loop.  Instead, retrieve
1029  // the current string value directly from the editor.
1030  size_t text_length = [[[field_ currentEditor] string] length];
1031
1032  // Cannot select suggested text.
1033  size_t max = text_length - suggest_text_length_;
1034  NSUInteger start = proposed_range.location;
1035  NSUInteger end = proposed_range.location + proposed_range.length;
1036
1037  if (start > max)
1038    start = max;
1039
1040  if (end > max)
1041    end = max;
1042
1043  return NSMakeRange(start, end - start);
1044}
1045
1046void AutocompleteEditViewMac::OnControlKeyChanged(bool pressed) {
1047  model_->OnControlKeyChanged(pressed);
1048}
1049
1050void AutocompleteEditViewMac::FocusLocation(bool select_all) {
1051  if ([field_ isEditable]) {
1052    // If the text field has a field editor, it's the first responder, meaning
1053    // that it's already focused. makeFirstResponder: will select all, so only
1054    // call it if this behavior is desired.
1055    if (select_all || ![field_ currentEditor])
1056      [[field_ window] makeFirstResponder:field_];
1057    DCHECK_EQ([field_ currentEditor], [[field_ window] firstResponder]);
1058  }
1059}
1060
1061// TODO(shess): Copied from autocomplete_edit_view_win.cc.  Could this
1062// be pushed into the model?
1063string16 AutocompleteEditViewMac::GetClipboardText(
1064    ui::Clipboard* clipboard) {
1065  // autocomplete_edit_view_win.cc assumes this can never happen, we
1066  // will too.
1067  DCHECK(clipboard);
1068
1069  if (clipboard->IsFormatAvailable(ui::Clipboard::GetPlainTextWFormatType(),
1070                                   ui::Clipboard::BUFFER_STANDARD)) {
1071    string16 text16;
1072    clipboard->ReadText(ui::Clipboard::BUFFER_STANDARD, &text16);
1073
1074    // Note: Unlike in the find popup and textfield view, here we completely
1075    // remove whitespace strings containing newlines.  We assume users are
1076    // most likely pasting in URLs that may have been split into multiple
1077    // lines in terminals, email programs, etc., and so linebreaks indicate
1078    // completely bogus whitespace that would just cause the input to be
1079    // invalid.
1080    return CollapseWhitespace(text16, true);
1081  }
1082
1083  // Try bookmark format.
1084  //
1085  // It is tempting to try bookmark format first, but the URL we get out of a
1086  // bookmark has been cannonicalized via GURL.  This means if a user copies
1087  // and pastes from the URL bar to itself, the text will get fixed up and
1088  // cannonicalized, which is not what the user expects.  By pasting in this
1089  // order, we are sure to paste what the user copied.
1090  if (clipboard->IsFormatAvailable(ui::Clipboard::GetUrlWFormatType(),
1091                                   ui::Clipboard::BUFFER_STANDARD)) {
1092    std::string url_str;
1093    clipboard->ReadBookmark(NULL, &url_str);
1094    // pass resulting url string through GURL to normalize
1095    GURL url(url_str);
1096    if (url.is_valid()) {
1097      return UTF8ToUTF16(url.spec());
1098    }
1099  }
1100
1101  return string16();
1102}
1103
1104// static
1105NSFont* AutocompleteEditViewMac::GetFieldFont() {
1106  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
1107  return rb.GetFont(ResourceBundle::BaseFont).GetNativeFont();
1108}
1109
1110NSUInteger AutocompleteEditViewMac::GetTextLength() const {
1111  return ([field_ currentEditor] ?
1112          [[[field_ currentEditor] string] length] :
1113          [[field_ stringValue] length]) - suggest_text_length_;
1114}
1115
1116void AutocompleteEditViewMac::PlaceCaretAt(NSUInteger pos) {
1117  DCHECK(pos <= GetTextLength());
1118  SetSelectedRange(NSMakeRange(pos, pos));
1119}
1120
1121bool AutocompleteEditViewMac::IsCaretAtEnd() const {
1122  const NSRange selection = GetSelectedRange();
1123  return selection.length == 0 && selection.location == GetTextLength();
1124}
1125