Home | History | Annotate | Download | only in location_bar
      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