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