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