// Copyright (c) 2009 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/cocoa/autocomplete_text_field.h" #include "base/logging.h" #import "chrome/browser/cocoa/autocomplete_text_field_cell.h" #import "chrome/browser/cocoa/browser_window_controller.h" #import "chrome/browser/cocoa/toolbar_controller.h" #import "chrome/browser/cocoa/url_drop_target.h" @implementation AutocompleteTextField @synthesize observer = observer_; + (Class)cellClass { return [AutocompleteTextFieldCell class]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; } - (void)awakeFromNib { DCHECK([[self cell] isKindOfClass:[AutocompleteTextFieldCell class]]); dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]); } - (void)flagsChanged:(NSEvent*)theEvent { if (observer_) { const bool controlFlag = ([theEvent modifierFlags]&NSControlKeyMask) != 0; observer_->OnControlKeyChanged(controlFlag); } } - (AutocompleteTextFieldCell*)autocompleteTextFieldCell { DCHECK([[self cell] isKindOfClass:[AutocompleteTextFieldCell class]]); return static_cast([self cell]); } // Reroute events for the decoration area to the field editor. This // will cause the cursor to be moved as close to the edge where the // event was seen as possible. // // The reason for this code's existence is subtle. NSTextField // implements text selection and editing in terms of a "field editor". // This is an NSTextView which is installed as a subview of the // control when the field becomes first responder. When the field // editor is installed, it will get -mouseDown: events and handle // them, rather than the text field - EXCEPT for the event which // caused the change in first responder, or events which fall in the // decorations outside the field editor's area. In that case, the // default NSTextField code will setup the field editor all over // again, which has the side effect of doing "select all" on the text. // This effect can be observed with a normal NSTextField if you click // in the narrow border area, and is only really a problem because in // our case the focus ring surrounds decorations which look clickable. // // When the user first clicks on the field, after installing the field // editor the default NSTextField code detects if the hit is in the // field editor area, and if so sets the selection to {0,0} to clear // the selection before forwarding the event to the field editor for // processing (it will set the cursor position). This also starts the // click-drag selection machinery. // // This code does the same thing for cases where the click was in the // decoration area. This allows the user to click-drag starting from // a decoration area and get the expected selection behaviour, // likewise for multiple clicks in those areas. - (void)mouseDown:(NSEvent*)theEvent { const NSPoint location = [self convertPoint:[theEvent locationInWindow] fromView:nil]; const NSRect bounds([self bounds]); AutocompleteTextFieldCell* cell = [self autocompleteTextFieldCell]; const NSRect textFrame([cell textFrameForFrame:bounds]); // A version of the textFrame which extends across the field's // entire width. const NSRect fullFrame(NSMakeRect(bounds.origin.x, textFrame.origin.y, bounds.size.width, textFrame.size.height)); // If the mouse is in the editing area, or above or below where the // editing area would be if we didn't add decorations, forward to // NSTextField -mouseDown: because it does the right thing. The // above/below test is needed because NSTextView treats mouse events // above/below as select-to-end-in-that-direction, which makes // things janky. BOOL flipped = [self isFlipped]; if (NSMouseInRect(location, textFrame, flipped) || !NSMouseInRect(location, fullFrame, flipped)) { [super mouseDown:theEvent]; // After the event has been handled, if the current event is a // mouse up and no selection was created (the mouse didn't move), // select the entire field. // NOTE(shess): This does not interfere with single-clicking to // place caret after a selection is made. An NSTextField only has // a selection when it has a field editor. The field editor is an // NSText subview, which will receive the -mouseDown: in that // case, and this code will never fire. NSText* editor = [self currentEditor]; if (editor) { NSEvent* currentEvent = [NSApp currentEvent]; if ([currentEvent type] == NSLeftMouseUp && ![editor selectedRange].length) { [editor selectAll:nil]; } } return; } // If the user clicked the security hint icon in the cell, display the page // info window. const NSRect hintIconFrame = [cell securityImageFrameForFrame:bounds]; if (NSMouseInRect(location, hintIconFrame, flipped)) { [cell onSecurityIconMousePressed]; return; } const BOOL ctrlKey = ([theEvent modifierFlags] & NSControlKeyMask) != 0; // If the user left-clicked a Page Action icon, execute its action. const size_t pageActionCount = [cell pageActionCount]; for (size_t i = 0; i < pageActionCount; ++i) { NSRect pageActionFrame = [cell pageActionFrameForIndex:i inFrame:bounds]; if (NSMouseInRect(location, pageActionFrame, flipped) && !ctrlKey) { [cell onPageActionMousePressedIn:pageActionFrame forIndex:i]; return; } } NSText* editor = [self currentEditor]; // We should only be here if we accepted first-responder status and // have a field editor. If one of these fires, it means some // assumptions are being broken. DCHECK(editor != nil); DCHECK([editor isDescendantOf:self]); // -becomeFirstResponder does a select-all, which we don't want // because it can lead to a dragged-text situation. Clear the // selection (any valid empty selection will do). [editor setSelectedRange:NSMakeRange(0, 0)]; // If the event is to the right of the editing area, scroll the // field editor to the end of the content so that the selection // doesn't initiate from somewhere in the middle of the text. if (location.x > NSMaxX(textFrame)) { [editor scrollRangeToVisible:NSMakeRange([[self stringValue] length], 0)]; } [editor mouseDown:theEvent]; } // Overriden to pass OnFrameChanged() notifications to |observer_|. // Additionally, cursor and tooltip rects need to be updated. - (void)setFrame:(NSRect)frameRect { [super setFrame:frameRect]; if (observer_) { observer_->OnFrameChanged(); } [self updateCursorAndToolTipRects]; } // Due to theming, parts of the field are transparent. - (BOOL)isOpaque { return NO; } - (void)setAttributedStringValue:(NSAttributedString*)aString { NSTextView* editor = static_cast([self currentEditor]); if (!editor) { [super setAttributedStringValue:aString]; } else { // -currentEditor is defined to return NSText*, make sure our // assumptions still hold, here. DCHECK([editor isKindOfClass:[NSTextView class]]); NSTextStorage* textStorage = [editor textStorage]; DCHECK(textStorage); [textStorage setAttributedString:aString]; } } - (NSUndoManager*)undoManagerForTextView:(NSTextView*)textView { if (!undoManager_.get()) undoManager_.reset([[NSUndoManager alloc] init]); return undoManager_.get(); } - (void)clearUndoChain { [undoManager_ removeAllActions]; } // Show the I-beam cursor unless the mouse is over an image within the field // (Page Actions or the security icon) in which case show the arrow cursor. - (void)resetCursorRects { NSRect fieldBounds = [self bounds]; [self addCursorRect:fieldBounds cursor:[NSCursor IBeamCursor]]; AutocompleteTextFieldCell* cell = [self autocompleteTextFieldCell]; NSRect iconRect = [cell securityImageFrameForFrame:fieldBounds]; [self addCursorRect:iconRect cursor:[NSCursor arrowCursor]]; const size_t pageActionCount = [cell pageActionCount]; for (size_t i = 0; i < pageActionCount; ++i) { iconRect = [cell pageActionFrameForIndex:i inFrame:fieldBounds]; [self addCursorRect:iconRect cursor:[NSCursor arrowCursor]]; } } - (void)updateCursorAndToolTipRects { // This will force |resetCursorRects| to be called, as it is not to be called // directly. [[self window] invalidateCursorRectsForView:self]; // |removeAllToolTips| only removes those set on the current NSView, not any // subviews. Unless more tooltips are added to this view, this should suffice // in place of managing a set of NSToolTipTag objects. [self removeAllToolTips]; AutocompleteTextFieldCell* cell = [self autocompleteTextFieldCell]; const size_t pageActionCount = [cell pageActionCount]; for (size_t i = 0; i < pageActionCount; ++i) { NSRect iconRect = [cell pageActionFrameForIndex:i inFrame:[self bounds]]; NSString* tooltip = [cell pageActionToolTipForIndex:i]; if (!tooltip) continue; [self addToolTipRect:iconRect owner:tooltip userData:nil]; } } // NOTE(shess): http://crbug.com/19116 describes a weird bug which // happens when the user runs a Print panel on Leopard. After that, // spurious -controlTextDidBeginEditing notifications are sent when an // NSTextField is firstResponder, even though -currentEditor on that // field returns nil. That notification caused significant problems // in AutocompleteEditViewMac. -textDidBeginEditing: was NOT being // sent in those cases, so this approach doesn't have the problem. - (void)textDidBeginEditing:(NSNotification*)aNotification { [super textDidBeginEditing:aNotification]; if (observer_) { observer_->OnDidBeginEditing(); } } - (void)textDidChange:(NSNotification *)aNotification { [super textDidChange:aNotification]; if (observer_) { observer_->OnDidChange(); } } - (void)textDidEndEditing:(NSNotification *)aNotification { [super textDidEndEditing:aNotification]; if (observer_) { observer_->OnDidEndEditing(); } } - (BOOL)textView:(NSTextView*)textView doCommandBySelector:(SEL)cmd { // TODO(shess): Review code for cases where we're fruitlessly attempting to // work in spite of not having an observer_. if (observer_ && observer_->OnDoCommandBySelector(cmd)) { return YES; } return NO; } - (void)windowDidResignKey:(NSNotification*)notification { DCHECK_EQ([self window], [notification object]); if (observer_) { observer_->OnDidResignKey(); } } - (void)viewWillMoveToWindow:(NSWindow*)newWindow { if ([self window]) { NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; [nc removeObserver:self name:NSWindowDidResignKeyNotification object:[self window]]; } } - (void)viewDidMoveToWindow { if ([self window]) { NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(windowDidResignKey:) name:NSWindowDidResignKeyNotification object:[self window]]; } } // (URLDropTarget protocol) - (id)urlDropController { BrowserWindowController* windowController = [[self window] windowController]; DCHECK([windowController isKindOfClass:[BrowserWindowController class]]); return [windowController toolbarController]; } // (URLDropTarget protocol) - (NSDragOperation)draggingEntered:(id)sender { // Make ourself the first responder, which will select the text to indicate // that our contents would be replaced by a drop. // TODO(viettrungluu): crbug.com/30809 -- this is a hack since it steals focus // and doesn't return it. [[self window] makeFirstResponder:self]; return [dropHandler_ draggingEntered:sender]; } // (URLDropTarget protocol) - (NSDragOperation)draggingUpdated:(id)sender { return [dropHandler_ draggingUpdated:sender]; } // (URLDropTarget protocol) - (void)draggingExited:(id)sender { return [dropHandler_ draggingExited:sender]; } // (URLDropTarget protocol) - (BOOL)performDragOperation:(id)sender { return [dropHandler_ performDragOperation:sender]; } @end