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