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