blob: 674346121064759f015ae19b9fdaea635204ae85 [file] [log] [blame]
// 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]);
currentToolTips_.reset([[NSMutableArray alloc] init]);
}
- (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<AutocompleteTextFieldCell*>([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<NSTextView*>([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];
[currentToolTips_ removeAllObjects];
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;
// -[NSView addToolTipRect:owner:userData] does _not_ retain the owner!
// Put the string in a collection so it can't be dealloced while in use.
[currentToolTips_ addObject:tooltip];
[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<URLDropTargetController>)urlDropController {
BrowserWindowController* windowController = [[self window] windowController];
DCHECK([windowController isKindOfClass:[BrowserWindowController class]]);
return [windowController toolbarController];
}
// (URLDropTarget protocol)
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)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<NSDraggingInfo>)sender {
return [dropHandler_ draggingUpdated:sender];
}
// (URLDropTarget protocol)
- (void)draggingExited:(id<NSDraggingInfo>)sender {
return [dropHandler_ draggingExited:sender];
}
// (URLDropTarget protocol)
- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
return [dropHandler_ performDragOperation:sender];
}
@end