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#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
6
7#include "base/logging.h"
8#import "chrome/browser/ui/cocoa/browser_window_controller.h"
9#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
10#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
11#import "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
12#import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
13#import "chrome/browser/ui/cocoa/url_drop_target.h"
14#import "chrome/browser/ui/cocoa/view_id_util.h"
15#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
16
17@implementation AutocompleteTextField
18
19@synthesize observer = observer_;
20
21+ (Class)cellClass {
22  return [AutocompleteTextFieldCell class];
23}
24
25- (void)dealloc {
26  [[NSNotificationCenter defaultCenter] removeObserver:self];
27  [super dealloc];
28}
29
30- (void)awakeFromNib {
31  DCHECK([[self cell] isKindOfClass:[AutocompleteTextFieldCell class]]);
32  [[self cell] setTruncatesLastVisibleLine:YES];
33  [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
34  currentToolTips_.reset([[NSMutableArray alloc] init]);
35}
36
37- (void)flagsChanged:(NSEvent*)theEvent {
38  if (observer_) {
39    const bool controlFlag = ([theEvent modifierFlags]&NSControlKeyMask) != 0;
40    observer_->OnControlKeyChanged(controlFlag);
41  }
42}
43
44- (AutocompleteTextFieldCell*)cell {
45  NSCell* cell = [super cell];
46  if (!cell)
47    return nil;
48
49  DCHECK([cell isKindOfClass:[AutocompleteTextFieldCell class]]);
50  return static_cast<AutocompleteTextFieldCell*>(cell);
51}
52
53// Reroute events for the decoration area to the field editor.  This
54// will cause the cursor to be moved as close to the edge where the
55// event was seen as possible.
56//
57// The reason for this code's existence is subtle.  NSTextField
58// implements text selection and editing in terms of a "field editor".
59// This is an NSTextView which is installed as a subview of the
60// control when the field becomes first responder.  When the field
61// editor is installed, it will get -mouseDown: events and handle
62// them, rather than the text field - EXCEPT for the event which
63// caused the change in first responder, or events which fall in the
64// decorations outside the field editor's area.  In that case, the
65// default NSTextField code will setup the field editor all over
66// again, which has the side effect of doing "select all" on the text.
67// This effect can be observed with a normal NSTextField if you click
68// in the narrow border area, and is only really a problem because in
69// our case the focus ring surrounds decorations which look clickable.
70//
71// When the user first clicks on the field, after installing the field
72// editor the default NSTextField code detects if the hit is in the
73// field editor area, and if so sets the selection to {0,0} to clear
74// the selection before forwarding the event to the field editor for
75// processing (it will set the cursor position).  This also starts the
76// click-drag selection machinery.
77//
78// This code does the same thing for cases where the click was in the
79// decoration area.  This allows the user to click-drag starting from
80// a decoration area and get the expected selection behaviour,
81// likewise for multiple clicks in those areas.
82- (void)mouseDown:(NSEvent*)theEvent {
83  // TODO(groby): Figure out if OnMouseDown needs to be postponed/skipped
84  // for button decorations.
85  if (observer_)
86    observer_->OnMouseDown([theEvent buttonNumber]);
87
88  // If the click was a Control-click, bring up the context menu.
89  // |NSTextField| handles these cases inconsistently if the field is
90  // not already first responder.
91  if (([theEvent modifierFlags] & NSControlKeyMask) != 0) {
92    NSText* editor = [self currentEditor];
93    NSMenu* menu = [editor menuForEvent:theEvent];
94    [NSMenu popUpContextMenu:menu withEvent:theEvent forView:editor];
95    return;
96  }
97
98  const NSPoint location =
99      [self convertPoint:[theEvent locationInWindow] fromView:nil];
100  const NSRect bounds([self bounds]);
101
102  AutocompleteTextFieldCell* cell = [self cell];
103  const NSRect textFrame([cell textFrameForFrame:bounds]);
104
105  // A version of the textFrame which extends across the field's
106  // entire width.
107
108  const NSRect fullFrame(NSMakeRect(bounds.origin.x, textFrame.origin.y,
109                                    bounds.size.width, textFrame.size.height));
110
111  // If the mouse is in the editing area, or above or below where the
112  // editing area would be if we didn't add decorations, forward to
113  // NSTextField -mouseDown: because it does the right thing.  The
114  // above/below test is needed because NSTextView treats mouse events
115  // above/below as select-to-end-in-that-direction, which makes
116  // things janky.
117  BOOL flipped = [self isFlipped];
118  if (NSMouseInRect(location, textFrame, flipped) ||
119      !NSMouseInRect(location, fullFrame, flipped)) {
120    [super mouseDown:theEvent];
121
122    // After the event has been handled, if the current event is a
123    // mouse up and no selection was created (the mouse didn't move),
124    // select the entire field.
125    // NOTE(shess): This does not interfere with single-clicking to
126    // place caret after a selection is made.  An NSTextField only has
127    // a selection when it has a field editor.  The field editor is an
128    // NSText subview, which will receive the -mouseDown: in that
129    // case, and this code will never fire.
130    NSText* editor = [self currentEditor];
131    if (editor) {
132      NSEvent* currentEvent = [NSApp currentEvent];
133      if ([currentEvent type] == NSLeftMouseUp &&
134          ![editor selectedRange].length &&
135          (!observer_ || observer_->ShouldSelectAllOnMouseDown())) {
136        [editor selectAll:nil];
137      }
138    }
139
140    return;
141  }
142
143  // Give the cell a chance to intercept clicks in page-actions and
144  // other decorative items.
145  if ([cell mouseDown:theEvent inRect:bounds ofView:self]) {
146    return;
147  }
148
149  NSText* editor = [self currentEditor];
150
151  // We should only be here if we accepted first-responder status and
152  // have a field editor.  If one of these fires, it means some
153  // assumptions are being broken.
154  DCHECK(editor != nil);
155  DCHECK([editor isDescendantOf:self]);
156
157  // -becomeFirstResponder does a select-all, which we don't want
158  // because it can lead to a dragged-text situation.  Clear the
159  // selection (any valid empty selection will do).
160  [editor setSelectedRange:NSMakeRange(0, 0)];
161
162  // If the event is to the right of the editing area, scroll the
163  // field editor to the end of the content so that the selection
164  // doesn't initiate from somewhere in the middle of the text.
165  if (location.x > NSMaxX(textFrame)) {
166    [editor scrollRangeToVisible:NSMakeRange([[self stringValue] length], 0)];
167  }
168
169  [editor mouseDown:theEvent];
170}
171
172- (void)rightMouseDown:(NSEvent*)event {
173  if (observer_)
174    observer_->OnMouseDown([event buttonNumber]);
175  [super rightMouseDown:event];
176}
177
178- (void)otherMouseDown:(NSEvent *)event {
179  if (observer_)
180    observer_->OnMouseDown([event buttonNumber]);
181  [super otherMouseDown:event];
182}
183
184// Received from tracking areas. Pass it down to the cell, and add the field.
185- (void)mouseEntered:(NSEvent*)theEvent {
186  [[self cell] mouseEntered:theEvent inView:self];
187}
188
189// Received from tracking areas. Pass it down to the cell, and add the field.
190- (void)mouseExited:(NSEvent*)theEvent {
191  [[self cell] mouseExited:theEvent inView:self];
192}
193
194// Overridden so that cursor and tooltip rects can be updated.
195- (void)setFrame:(NSRect)frameRect {
196  [super setFrame:frameRect];
197  if (observer_) {
198    observer_->OnFrameChanged();
199  }
200  [self updateMouseTracking];
201}
202
203- (void)setAttributedStringValue:(NSAttributedString*)aString {
204  AutocompleteTextFieldEditor* editor =
205      static_cast<AutocompleteTextFieldEditor*>([self currentEditor]);
206
207  if (!editor) {
208    [super setAttributedStringValue:aString];
209  } else {
210    // The type of the field editor must be AutocompleteTextFieldEditor,
211    // otherwise things won't work.
212    DCHECK([editor isKindOfClass:[AutocompleteTextFieldEditor class]]);
213
214    [editor setAttributedString:aString];
215  }
216}
217
218- (NSUndoManager*)undoManagerForTextView:(NSTextView*)textView {
219  if (!undoManager_.get())
220    undoManager_.reset([[NSUndoManager alloc] init]);
221  return undoManager_.get();
222}
223
224- (void)clearUndoChain {
225  [undoManager_ removeAllActions];
226}
227
228- (NSRange)textView:(NSTextView *)aTextView
229    willChangeSelectionFromCharacterRange:(NSRange)oldRange
230    toCharacterRange:(NSRange)newRange {
231  if (observer_)
232    return observer_->SelectionRangeForProposedRange(newRange);
233  return newRange;
234}
235
236- (void)addToolTip:(NSString*)tooltip forRect:(NSRect)aRect {
237  [currentToolTips_ addObject:tooltip];
238  [self addToolTipRect:aRect owner:tooltip userData:nil];
239}
240
241- (void)setGrayTextAutocompletion:(NSString*)suggestText
242                        textColor:(NSColor*)suggestColor {
243  [self setNeedsDisplay:YES];
244  suggestText_.reset([suggestText retain]);
245  suggestColor_.reset([suggestColor retain]);
246}
247
248- (NSString*)suggestText {
249  return suggestText_;
250}
251
252- (NSColor*)suggestColor {
253  return suggestColor_;
254}
255
256- (NSPoint)bubblePointForDecoration:(LocationBarDecoration*)decoration {
257  const NSRect frame =
258      [[self cell] frameForDecoration:decoration inFrame:[self bounds]];
259  const NSPoint point = decoration->GetBubblePointInFrame(frame);
260  return [self convertPoint:point toView:nil];
261}
262
263// TODO(shess): -resetFieldEditorFrameIfNeeded is the place where
264// changes to the cell layout should be flushed.  LocationBarViewMac
265// and ToolbarController are calling this routine directly, and I
266// think they are probably wrong.
267// http://crbug.com/40053
268- (void)updateMouseTracking {
269  // This will force |resetCursorRects| to be called, as it is not to be called
270  // directly.
271  [[self window] invalidateCursorRectsForView:self];
272
273  // |removeAllToolTips| only removes those set on the current NSView, not any
274  // subviews. Unless more tooltips are added to this view, this should suffice
275  // in place of managing a set of NSToolTipTag objects.
276  [self removeAllToolTips];
277
278  // Reload the decoration tooltips.
279  [currentToolTips_ removeAllObjects];
280  [[self cell] updateToolTipsInRect:[self bounds] ofView:self];
281
282  // Setup/update the tracking areas for the decorations.
283  [[self cell] setUpTrackingAreasInRect:[self bounds] ofView:self];
284}
285
286// NOTE(shess): http://crbug.com/19116 describes a weird bug which
287// happens when the user runs a Print panel on Leopard.  After that,
288// spurious -controlTextDidBeginEditing notifications are sent when an
289// NSTextField is firstResponder, even though -currentEditor on that
290// field returns nil.  That notification caused significant problems
291// in OmniboxViewMac.  -textDidBeginEditing: was NOT being
292// sent in those cases, so this approach doesn't have the problem.
293- (void)textDidBeginEditing:(NSNotification*)aNotification {
294  [super textDidBeginEditing:aNotification];
295  if (observer_) {
296    observer_->OnDidBeginEditing();
297  }
298}
299
300- (void)textDidEndEditing:(NSNotification *)aNotification {
301  [super textDidEndEditing:aNotification];
302  if (observer_) {
303    observer_->OnDidEndEditing();
304  }
305}
306
307// When the window resigns, make sure the autocomplete popup is no
308// longer visible, since the user's focus is elsewhere.
309- (void)windowDidResignKey:(NSNotification*)notification {
310  DCHECK_EQ([self window], [notification object]);
311  if (observer_)
312    observer_->ClosePopup();
313}
314
315- (void)windowDidResize:(NSNotification*)notification {
316  DCHECK_EQ([self window], [notification object]);
317  if (observer_)
318    observer_->OnFrameChanged();
319}
320
321- (void)viewWillMoveToWindow:(NSWindow*)newWindow {
322  if ([self window]) {
323    NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
324    [nc removeObserver:self
325                  name:NSWindowDidResignKeyNotification
326                object:[self window]];
327    [nc removeObserver:self
328                  name:NSWindowDidResizeNotification
329                object:[self window]];
330  }
331}
332
333- (void)viewDidMoveToWindow {
334  if ([self window]) {
335    NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
336    [nc addObserver:self
337           selector:@selector(windowDidResignKey:)
338               name:NSWindowDidResignKeyNotification
339             object:[self window]];
340    [nc addObserver:self
341           selector:@selector(windowDidResize:)
342               name:NSWindowDidResizeNotification
343             object:[self window]];
344    // Only register for drops if not in a popup window. Lazily create the
345    // drop handler when the type of window is known.
346    BrowserWindowController* windowController =
347        [BrowserWindowController browserWindowControllerForView:self];
348    if ([windowController isTabbedWindow])
349      dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
350  }
351}
352
353// NSTextField becomes first responder by installing a "field editor"
354// subview.  Clicks outside the field editor (such as a decoration)
355// will attempt to make the field the first-responder again, which
356// causes a select-all, even if the decoration handles the click.  If
357// the field editor is already in place, don't accept first responder
358// again.  This allows the selection to be unmodified if the click is
359// handled by a decoration or context menu (|-mouseDown:| will still
360// change it if appropriate).
361- (BOOL)acceptsFirstResponder {
362  if ([self currentEditor]) {
363    DCHECK_EQ([self currentEditor], [[self window] firstResponder]);
364    return NO;
365  }
366  return [super acceptsFirstResponder];
367}
368
369// (Overridden from NSResponder)
370- (BOOL)becomeFirstResponder {
371  BOOL doAccept = [super becomeFirstResponder];
372  if (doAccept) {
373    [[BrowserWindowController browserWindowControllerForView:self]
374        lockBarVisibilityForOwner:self withAnimation:YES delay:NO];
375
376    // Tells the observer that we get the focus.
377    // But we can't call observer_->OnKillFocus() in resignFirstResponder:,
378    // because the first responder will be immediately set to the field editor
379    // when calling [super becomeFirstResponder], thus we won't receive
380    // resignFirstResponder: anymore when losing focus.
381    [[self cell] handleFocusEvent:[NSApp currentEvent] ofView:self];
382  }
383  return doAccept;
384}
385
386// (Overridden from NSResponder)
387- (BOOL)resignFirstResponder {
388  BOOL doResign = [super resignFirstResponder];
389  if (doResign) {
390    [[BrowserWindowController browserWindowControllerForView:self]
391        releaseBarVisibilityForOwner:self withAnimation:YES delay:YES];
392  }
393  return doResign;
394}
395
396- (void)drawRect:(NSRect)rect {
397  [super drawRect:rect];
398  autocomplete_text_field::DrawGrayTextAutocompletion(
399      [self attributedStringValue],
400      suggestText_,
401      suggestColor_,
402      self,
403      [[self cell] drawingRectForBounds:[self bounds]]);
404}
405
406// (URLDropTarget protocol)
407- (id<URLDropTargetController>)urlDropController {
408  BrowserWindowController* windowController =
409      [BrowserWindowController browserWindowControllerForView:self];
410  return [windowController toolbarController];
411}
412
413// (URLDropTarget protocol)
414- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
415  // Make ourself the first responder, which will select the text to indicate
416  // that our contents would be replaced by a drop.
417  // TODO(viettrungluu): crbug.com/30809 -- this is a hack since it steals focus
418  // and doesn't return it.
419  [[self window] makeFirstResponder:self];
420  return [dropHandler_ draggingEntered:sender];
421}
422
423// (URLDropTarget protocol)
424- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
425  return [dropHandler_ draggingUpdated:sender];
426}
427
428// (URLDropTarget protocol)
429- (void)draggingExited:(id<NSDraggingInfo>)sender {
430  return [dropHandler_ draggingExited:sender];
431}
432
433// (URLDropTarget protocol)
434- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
435  return [dropHandler_ performDragOperation:sender];
436}
437
438- (NSMenu*)decorationMenuForEvent:(NSEvent*)event {
439  AutocompleteTextFieldCell* cell = [self cell];
440  return [cell decorationMenuForEvent:event inRect:[self bounds] ofView:self];
441}
442
443- (ViewID)viewID {
444  return VIEW_ID_OMNIBOX;
445}
446
447@end
448
449namespace autocomplete_text_field {
450
451void DrawGrayTextAutocompletion(NSAttributedString* mainText,
452                                NSString* suggestText,
453                                NSColor* suggestColor,
454                                NSView* controlView,
455                                NSRect frame) {
456  if (![suggestText length])
457    return;
458
459  base::scoped_nsobject<NSTextFieldCell> cell(
460      [[NSTextFieldCell alloc] initTextCell:@""]);
461  [cell setBordered:NO];
462  [cell setDrawsBackground:NO];
463  [cell setEditable:NO];
464
465  base::scoped_nsobject<NSMutableAttributedString> combinedText(
466      [[NSMutableAttributedString alloc] initWithAttributedString:mainText]);
467  NSRange range = NSMakeRange([combinedText length], 0);
468  [combinedText replaceCharactersInRange:range withString:suggestText];
469  [combinedText addAttribute:NSForegroundColorAttributeName
470                       value:suggestColor
471                       range:NSMakeRange(range.location, [suggestText length])];
472  [cell setAttributedStringValue:combinedText];
473
474  CGFloat mainTextWidth = [mainText size].width;
475  CGFloat suggestWidth = NSWidth(frame) - mainTextWidth;
476  NSRect suggestRect = NSMakeRect(NSMinX(frame) + mainTextWidth,
477                                  NSMinY(frame),
478                                  suggestWidth,
479                                  NSHeight(frame));
480
481  gfx::ScopedNSGraphicsContextSaveGState saveGState;
482  NSRectClip(suggestRect);
483  [cell drawInteriorWithFrame:frame inView:controlView];
484}
485
486}  // namespace autocomplete_text_field
487