summaryrefslogtreecommitdiffstats
path: root/chrome/browser/ui/cocoa/location_bar
diff options
context:
space:
mode:
authorben@chromium.org <ben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-12-01 16:34:49 +0000
committerben@chromium.org <ben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-12-01 16:34:49 +0000
commit7d791652c7ede4209a2014d885148e2713f49bce (patch)
treec26baf12593bed381c631b81c736106809d46b44 /chrome/browser/ui/cocoa/location_bar
parent3b94427c99bdf12836fd455eeb1499fdde511e26 (diff)
downloadchromium_src-7d791652c7ede4209a2014d885148e2713f49bce.zip
chromium_src-7d791652c7ede4209a2014d885148e2713f49bce.tar.gz
chromium_src-7d791652c7ede4209a2014d885148e2713f49bce.tar.bz2
Move browser/cocoa to browser/ui/cocoa
BUG=none TEST=none TBR=brettw git-svn-id: svn://svn.chromium.org/chrome/trunk/src@67854 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/ui/cocoa/location_bar')
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h144
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.mm385
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h76
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.mm402
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell_unittest.mm300
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h56
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm371
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor_unittest.mm297
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest.mm792
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h58
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.mm29
-rw-r--r--chrome/browser/ui/cocoa/location_bar/bubble_decoration.h67
-rw-r--r--chrome/browser/ui/cocoa/location_bar/bubble_decoration.mm158
-rw-r--r--chrome/browser/ui/cocoa/location_bar/content_setting_decoration.h55
-rw-r--r--chrome/browser/ui/cocoa/location_bar/content_setting_decoration.mm109
-rw-r--r--chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h59
-rw-r--r--chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.mm117
-rw-r--r--chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration_unittest.mm55
-rw-r--r--chrome/browser/ui/cocoa/location_bar/image_decoration.h36
-rw-r--r--chrome/browser/ui/cocoa/location_bar/image_decoration.mm54
-rw-r--r--chrome/browser/ui/cocoa/location_bar/image_decoration_unittest.mm55
-rw-r--r--chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.h43
-rw-r--r--chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.mm31
-rw-r--r--chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller_unittest.mm62
-rw-r--r--chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h16
-rw-r--r--chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.mm54
-rw-r--r--chrome/browser/ui/cocoa/location_bar/instant_opt_in_view_unittest.mm26
-rw-r--r--chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h47
-rw-r--r--chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.mm160
-rw-r--r--chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration_unittest.mm57
-rw-r--r--chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h89
-rw-r--r--chrome/browser/ui/cocoa/location_bar/location_bar_decoration.mm18
-rw-r--r--chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h237
-rw-r--r--chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.mm690
-rw-r--r--chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h46
-rw-r--r--chrome/browser/ui/cocoa/location_bar/location_icon_decoration.mm72
-rw-r--r--chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.h17
-rw-r--r--chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.mm43
-rw-r--r--chrome/browser/ui/cocoa/location_bar/omnibox_popup_view_unittest.mm68
-rw-r--r--chrome/browser/ui/cocoa/location_bar/page_action_decoration.h119
-rw-r--r--chrome/browser/ui/cocoa/location_bar/page_action_decoration.mm251
-rw-r--r--chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h42
-rw-r--r--chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.mm73
-rw-r--r--chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration_unittest.mm64
-rw-r--r--chrome/browser/ui/cocoa/location_bar/star_decoration.h44
-rw-r--r--chrome/browser/ui/cocoa/location_bar/star_decoration.mm53
46 files changed, 6097 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h
new file mode 100644
index 0000000..e731c2c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h
@@ -0,0 +1,144 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_AUTOCOMPLETE_TEXT_FIELD_H_
+#define CHROME_BROWSER_UI_COCOA_AUTOCOMPLETE_TEXT_FIELD_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/styled_text_field.h"
+#import "chrome/browser/ui/cocoa/url_drop_target.h"
+
+@class AutocompleteTextFieldCell;
+
+// AutocompleteTextField intercepts UI actions for forwarding to
+// AutocompleteEditViewMac (*), and provides a custom look. It works
+// together with AutocompleteTextFieldEditor (mostly for intercepting
+// user actions) and AutocompleteTextFieldCell (mostly for custom
+// drawing).
+//
+// For historical reasons, chrome/browser/autocomplete is the core
+// implementation of the Omnibox. Chrome code seems to vary between
+// autocomplete and Omnibox in describing this.
+//
+// (*) AutocompleteEditViewMac is a view in the MVC sense for the
+// Chrome internals, though it's really more of a mish-mash of model,
+// view, and controller.
+
+// Provides a hook so that we can call directly down to
+// AutocompleteEditViewMac rather than traversing the delegate chain.
+class AutocompleteTextFieldObserver {
+ public:
+ // Called before changing the selected range of the field.
+ virtual NSRange SelectionRangeForProposedRange(NSRange proposed_range) = 0;
+
+ // Called when the control-key state changes while the field is
+ // first responder.
+ virtual void OnControlKeyChanged(bool pressed) = 0;
+
+ // Called when the user pastes into the field.
+ virtual void OnPaste() = 0;
+
+ // Return |true| if there is a selection to copy.
+ virtual bool CanCopy() = 0;
+
+ // Clears the |pboard| and adds the field's current selection.
+ // Called when the user does a copy or drag.
+ virtual void CopyToPasteboard(NSPasteboard* pboard) = 0;
+
+ // Returns true if the current clipboard text supports paste and go
+ // (or paste and search).
+ virtual bool CanPasteAndGo() = 0;
+
+ // Returns the appropriate "Paste and Go" or "Paste and Search"
+ // context menu string, depending on what is currently in the
+ // clipboard. Must not be called unless CanPasteAndGo() returns
+ // true.
+ virtual int GetPasteActionStringId() = 0;
+
+ // Called when the user initiates a "paste and go" or "paste and
+ // search" into the field.
+ virtual void OnPasteAndGo() = 0;
+
+ // Called when the field's frame changes.
+ virtual void OnFrameChanged() = 0;
+
+ // Called when the popup is no longer appropriate, such as when the
+ // field's window loses focus or a page action is clicked.
+ virtual void ClosePopup() = 0;
+
+ // Called when the user begins editing the field, for every edit,
+ // and when the user is done editing the field.
+ virtual void OnDidBeginEditing() = 0;
+ virtual void OnDidChange() = 0;
+ virtual void OnDidEndEditing() = 0;
+
+ // NSResponder translates certain keyboard actions into selectors
+ // passed to -doCommandBySelector:. The selector is forwarded here,
+ // return true if |cmd| is handled, false if the caller should
+ // handle it.
+ // TODO(shess): For now, I think having the code which makes these
+ // decisions closer to the other autocomplete code is worthwhile,
+ // since it calls a wide variety of methods which otherwise aren't
+ // clearly relevent to expose here. But consider pulling more of
+ // the AutocompleteEditViewMac calls up to here.
+ virtual bool OnDoCommandBySelector(SEL cmd) = 0;
+
+ // Called whenever the autocomplete text field gets focused.
+ virtual void OnSetFocus(bool control_down) = 0;
+
+ // Called whenever the autocomplete text field is losing focus.
+ virtual void OnKillFocus() = 0;
+
+ protected:
+ virtual ~AutocompleteTextFieldObserver() {}
+};
+
+@interface AutocompleteTextField : StyledTextField<NSTextViewDelegate,
+ URLDropTarget> {
+ @private
+ // Undo manager for this text field. We use a specific instance rather than
+ // the standard undo manager in order to let us clear the undo stack at will.
+ scoped_nsobject<NSUndoManager> undoManager_;
+
+ AutocompleteTextFieldObserver* observer_; // weak, owned by location bar.
+
+ // Handles being a drag-and-drop target.
+ scoped_nsobject<URLDropTargetHandler> dropHandler_;
+
+ // Holds current tooltip strings, to keep them from being dealloced.
+ scoped_nsobject<NSMutableArray> currentToolTips_;
+}
+
+@property (nonatomic) AutocompleteTextFieldObserver* observer;
+
+// Convenience method to return the cell, casted appropriately.
+- (AutocompleteTextFieldCell*)cell;
+
+// Superclass aborts editing before changing the string, which causes
+// problems for undo. This version modifies the field editor's
+// contents if the control is already being edited.
+- (void)setAttributedStringValue:(NSAttributedString*)aString;
+
+// Clears the undo chain for this text field.
+- (void)clearUndoChain;
+
+// Updates cursor and tooltip rects depending on the contents of the text field
+// e.g. the security icon should have a default pointer shown on hover instead
+// of an I-beam.
+- (void)updateCursorAndToolTipRects;
+
+// Return the appropriate menu for any decoration under |event|.
+- (NSMenu*)decorationMenuForEvent:(NSEvent*)event;
+
+// Retains |tooltip| (in |currentToolTips_|) and adds this tooltip
+// via -[NSView addToolTipRect:owner:userData:].
+- (void)addToolTip:(NSString*)tooltip forRect:(NSRect)aRect;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_AUTOCOMPLETE_TEXT_FIELD_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.mm
new file mode 100644
index 0000000..33c34cf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.mm
@@ -0,0 +1,385 @@
+// Copyright (c) 2010 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/ui/cocoa/location_bar/autocomplete_text_field.h"
+
+#include "base/logging.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+#import "chrome/browser/ui/cocoa/url_drop_target.h"
+#import "chrome/browser/ui/cocoa/view_id_util.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]]);
+ [[self cell] setTruncatesLastVisibleLine:YES];
+ [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
+ currentToolTips_.reset([[NSMutableArray alloc] init]);
+}
+
+- (void)flagsChanged:(NSEvent*)theEvent {
+ if (observer_) {
+ const bool controlFlag = ([theEvent modifierFlags]&NSControlKeyMask) != 0;
+ observer_->OnControlKeyChanged(controlFlag);
+ }
+}
+
+- (AutocompleteTextFieldCell*)cell {
+ NSCell* cell = [super cell];
+ if (!cell)
+ return nil;
+
+ DCHECK([cell isKindOfClass:[AutocompleteTextFieldCell class]]);
+ return static_cast<AutocompleteTextFieldCell*>(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 {
+ // Close the popup before processing the event. This prevents the
+ // popup from being visible while a right-click context menu or
+ // page-action menu is visible. Also, it matches other platforms.
+ if (observer_)
+ observer_->ClosePopup();
+
+ // If the click was a Control-click, bring up the context menu.
+ // |NSTextField| handles these cases inconsistently if the field is
+ // not already first responder.
+ if (([theEvent modifierFlags] & NSControlKeyMask) != 0) {
+ NSText* editor = [self currentEditor];
+ NSMenu* menu = [editor menuForEvent:theEvent];
+ [NSMenu popUpContextMenu:menu withEvent:theEvent forView:editor];
+ return;
+ }
+
+ const NSPoint location =
+ [self convertPoint:[theEvent locationInWindow] fromView:nil];
+ const NSRect bounds([self bounds]);
+
+ AutocompleteTextFieldCell* cell = [self cell];
+ 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;
+ }
+
+ // Give the cell a chance to intercept clicks in page-actions and
+ // other decorative items.
+ if ([cell mouseDown:theEvent inRect:bounds ofView:self]) {
+ 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];
+}
+
+// Overridden 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 {
+ AutocompleteTextFieldEditor* editor =
+ static_cast<AutocompleteTextFieldEditor*>([self currentEditor]);
+
+ if (!editor) {
+ [super setAttributedStringValue:aString];
+ } else {
+ // The type of the field editor must be AutocompleteTextFieldEditor,
+ // otherwise things won't work.
+ DCHECK([editor isKindOfClass:[AutocompleteTextFieldEditor class]]);
+
+ [editor setAttributedString:aString];
+ }
+}
+
+- (NSUndoManager*)undoManagerForTextView:(NSTextView*)textView {
+ if (!undoManager_.get())
+ undoManager_.reset([[NSUndoManager alloc] init]);
+ return undoManager_.get();
+}
+
+- (void)clearUndoChain {
+ [undoManager_ removeAllActions];
+}
+
+- (NSRange)textView:(NSTextView *)aTextView
+ willChangeSelectionFromCharacterRange:(NSRange)oldRange
+ toCharacterRange:(NSRange)newRange {
+ if (observer_)
+ return observer_->SelectionRangeForProposedRange(newRange);
+ return newRange;
+}
+
+- (void)addToolTip:(NSString*)tooltip forRect:(NSRect)aRect {
+ [currentToolTips_ addObject:tooltip];
+ [self addToolTipRect:aRect owner:tooltip userData:nil];
+}
+
+// TODO(shess): -resetFieldEditorFrameIfNeeded is the place where
+// changes to the cell layout should be flushed. LocationBarViewMac
+// and ToolbarController are calling this routine directly, and I
+// think they are probably wrong.
+// http://crbug.com/40053
+- (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];
+
+ // Reload the decoration tooltips.
+ [currentToolTips_ removeAllObjects];
+ [[self cell] updateToolTipsInRect:[self bounds] ofView:self];
+}
+
+// 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)textDidEndEditing:(NSNotification *)aNotification {
+ [super textDidEndEditing:aNotification];
+ if (observer_) {
+ observer_->OnDidEndEditing();
+ }
+}
+
+// When the window resigns, make sure the autocomplete popup is no
+// longer visible, since the user's focus is elsewhere.
+- (void)windowDidResignKey:(NSNotification*)notification {
+ DCHECK_EQ([self window], [notification object]);
+ if (observer_)
+ observer_->ClosePopup();
+}
+
+- (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]];
+ // Only register for drops if not in a popup window. Lazily create the
+ // drop handler when the type of window is known.
+ BrowserWindowController* windowController =
+ [BrowserWindowController browserWindowControllerForView:self];
+ if ([windowController isNormalWindow])
+ dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
+ }
+}
+
+// NSTextField becomes first responder by installing a "field editor"
+// subview. Clicks outside the field editor (such as a decoration)
+// will attempt to make the field the first-responder again, which
+// causes a select-all, even if the decoration handles the click. If
+// the field editor is already in place, don't accept first responder
+// again. This allows the selection to be unmodified if the click is
+// handled by a decoration or context menu (|-mouseDown:| will still
+// change it if appropriate).
+- (BOOL)acceptsFirstResponder {
+ if ([self currentEditor]) {
+ DCHECK_EQ([self currentEditor], [[self window] firstResponder]);
+ return NO;
+ }
+ return [super acceptsFirstResponder];
+}
+
+// (Overridden from NSResponder)
+- (BOOL)becomeFirstResponder {
+ BOOL doAccept = [super becomeFirstResponder];
+ if (doAccept) {
+ [[BrowserWindowController browserWindowControllerForView:self]
+ lockBarVisibilityForOwner:self withAnimation:YES delay:NO];
+
+ // Tells the observer that we get the focus.
+ // But we can't call observer_->OnKillFocus() in resignFirstResponder:,
+ // because the first responder will be immediately set to the field editor
+ // when calling [super becomeFirstResponder], thus we won't receive
+ // resignFirstResponder: anymore when losing focus.
+ if (observer_) {
+ NSEvent* theEvent = [NSApp currentEvent];
+ const bool controlDown = ([theEvent modifierFlags]&NSControlKeyMask) != 0;
+ observer_->OnSetFocus(controlDown);
+ }
+ }
+ return doAccept;
+}
+
+// (Overridden from NSResponder)
+- (BOOL)resignFirstResponder {
+ BOOL doResign = [super resignFirstResponder];
+ if (doResign) {
+ [[BrowserWindowController browserWindowControllerForView:self]
+ releaseBarVisibilityForOwner:self withAnimation:YES delay:YES];
+ }
+ return doResign;
+}
+
+// (URLDropTarget protocol)
+- (id<URLDropTargetController>)urlDropController {
+ BrowserWindowController* windowController =
+ [BrowserWindowController browserWindowControllerForView:self];
+ 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];
+}
+
+- (NSMenu*)decorationMenuForEvent:(NSEvent*)event {
+ AutocompleteTextFieldCell* cell = [self cell];
+ return [cell decorationMenuForEvent:event inRect:[self bounds] ofView:self];
+}
+
+- (ViewID)viewID {
+ return VIEW_ID_LOCATION_BAR;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h
new file mode 100644
index 0000000..1306253
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h
@@ -0,0 +1,76 @@
+// Copyright (c) 2010 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.
+
+#include <vector>
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/styled_text_field_cell.h"
+
+@class AutocompleteTextField;
+class LocationBarDecoration;
+
+// AutocompleteTextFieldCell extends StyledTextFieldCell to provide support for
+// certain decorations to be applied to the field. These are the search hint
+// ("Type to search" on the right-hand side), the keyword hint ("Press [Tab] to
+// search Engine" on the right-hand side), and keyword mode ("Search Engine:" in
+// a button-like token on the left-hand side).
+@interface AutocompleteTextFieldCell : StyledTextFieldCell {
+ @private
+ // Decorations which live to the left and right of the text, ordered
+ // from outside in. Decorations are owned by |LocationBarViewMac|.
+ std::vector<LocationBarDecoration*> leftDecorations_;
+ std::vector<LocationBarDecoration*> rightDecorations_;
+}
+
+// Clear |leftDecorations_| and |rightDecorations_|.
+- (void)clearDecorations;
+
+// Add a new left-side decoration to the right of the existing
+// left-side decorations.
+- (void)addLeftDecoration:(LocationBarDecoration*)decoration;
+
+// Add a new right-side decoration to the left of the existing
+// right-side decorations.
+- (void)addRightDecoration:(LocationBarDecoration*)decoration;
+
+// The width available after accounting for decorations.
+- (CGFloat)availableWidthInFrame:(const NSRect)frame;
+
+// Return the frame for |aDecoration| if the cell is in |cellFrame|.
+// Returns |NSZeroRect| for decorations which are not currently
+// visible.
+- (NSRect)frameForDecoration:(const LocationBarDecoration*)aDecoration
+ inFrame:(NSRect)cellFrame;
+
+// Find the decoration under the event. |NULL| if |theEvent| is not
+// over anything.
+- (LocationBarDecoration*)decorationForEvent:(NSEvent*)theEvent
+ inRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)field;
+
+// Return the appropriate menu for any decorations under event.
+// Returns nil if no menu is present for the decoration, or if the
+// event is not over a decoration.
+- (NSMenu*)decorationMenuForEvent:(NSEvent*)theEvent
+ inRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView;
+
+// Called by |AutocompleteTextField| to let page actions intercept
+// clicks. Returns |YES| if the click has been intercepted.
+- (BOOL)mouseDown:(NSEvent*)theEvent
+ inRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView;
+
+// Overridden from StyledTextFieldCell to include decorations adjacent
+// to the text area which don't handle mouse clicks themselves.
+// Keyword-search bubble, for instance.
+- (NSRect)textCursorFrameForFrame:(NSRect)cellFrame;
+
+// Setup decoration tooltips on |controlView| by calling
+// |-addToolTip:forRect:|.
+- (void)updateToolTipsInRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView;
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.mm
new file mode 100644
index 0000000..02c8a667
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.mm
@@ -0,0 +1,402 @@
+// Copyright (c) 2010 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/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
+
+#include "base/logging.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
+
+namespace {
+
+const CGFloat kBaselineAdjust = 3.0;
+
+// Matches the clipping radius of |GradientButtonCell|.
+const CGFloat kCornerRadius = 4.0;
+
+// How far to inset the left-hand decorations from the field's bounds.
+const CGFloat kLeftDecorationXOffset = 5.0;
+
+// How far to inset the right-hand decorations from the field's bounds.
+// TODO(shess): Why is this different from |kLeftDecorationXOffset|?
+// |kDecorationOuterXOffset|?
+const CGFloat kRightDecorationXOffset = 5.0;
+
+// The amount of padding on either side reserved for drawing
+// decorations. [Views has |kItemPadding| == 3.]
+const CGFloat kDecorationHorizontalPad = 3.0;
+
+// How long to wait for mouse-up on the location icon before assuming
+// that the user wants to drag.
+const NSTimeInterval kLocationIconDragTimeout = 0.25;
+
+// Calculate the positions for a set of decorations. |frame| is the
+// overall frame to do layout in, |remaining_frame| will get the
+// left-over space. |all_decorations| is the set of decorations to
+// lay out, |decorations| will be set to the decorations which are
+// visible and which fit, in the same order as |all_decorations|,
+// while |decoration_frames| will be the corresponding frames.
+// |x_edge| describes the edge to layout the decorations against
+// (|NSMinXEdge| or |NSMaxXEdge|). |initial_padding| is the padding
+// from the edge of |cell_frame| (|kDecorationHorizontalPad| is used
+// between decorations).
+void CalculatePositionsHelper(
+ NSRect frame,
+ const std::vector<LocationBarDecoration*>& all_decorations,
+ NSRectEdge x_edge,
+ CGFloat initial_padding,
+ std::vector<LocationBarDecoration*>* decorations,
+ std::vector<NSRect>* decoration_frames,
+ NSRect* remaining_frame) {
+ DCHECK(x_edge == NSMinXEdge || x_edge == NSMaxXEdge);
+ DCHECK_EQ(decorations->size(), decoration_frames->size());
+
+ // The outer-most decoration will be inset a bit further from the
+ // edge.
+ CGFloat padding = initial_padding;
+
+ for (size_t i = 0; i < all_decorations.size(); ++i) {
+ if (all_decorations[i]->IsVisible()) {
+ NSRect padding_rect, available;
+
+ // Peel off the outside padding.
+ NSDivideRect(frame, &padding_rect, &available, padding, x_edge);
+
+ // Find out how large the decoration will be in the remaining
+ // space.
+ const CGFloat used_width =
+ all_decorations[i]->GetWidthForSpace(NSWidth(available));
+
+ if (used_width != LocationBarDecoration::kOmittedWidth) {
+ DCHECK_GT(used_width, 0.0);
+ NSRect decoration_frame;
+
+ // Peel off the desired width, leaving the remainder in
+ // |frame|.
+ NSDivideRect(available, &decoration_frame, &frame,
+ used_width, x_edge);
+
+ decorations->push_back(all_decorations[i]);
+ decoration_frames->push_back(decoration_frame);
+ DCHECK_EQ(decorations->size(), decoration_frames->size());
+
+ // Adjust padding for between decorations.
+ padding = kDecorationHorizontalPad;
+ }
+ }
+ }
+
+ DCHECK_EQ(decorations->size(), decoration_frames->size());
+ *remaining_frame = frame;
+}
+
+// Helper function for calculating placement of decorations w/in the
+// cell. |frame| is the cell's boundary rectangle, |remaining_frame|
+// will get any space left after decorations are laid out (for text).
+// |left_decorations| is a set of decorations for the left-hand side
+// of the cell, |right_decorations| for the right-hand side.
+// |decorations| will contain the resulting visible decorations, and
+// |decoration_frames| will contain their frames in the same
+// coordinates as |frame|. Decorations will be ordered left to right.
+// As a convenience returns the index of the first right-hand
+// decoration.
+size_t CalculatePositionsInFrame(
+ NSRect frame,
+ const std::vector<LocationBarDecoration*>& left_decorations,
+ const std::vector<LocationBarDecoration*>& right_decorations,
+ std::vector<LocationBarDecoration*>* decorations,
+ std::vector<NSRect>* decoration_frames,
+ NSRect* remaining_frame) {
+ decorations->clear();
+ decoration_frames->clear();
+
+ // Layout |left_decorations| against the LHS.
+ CalculatePositionsHelper(frame, left_decorations,
+ NSMinXEdge, kLeftDecorationXOffset,
+ decorations, decoration_frames, &frame);
+ DCHECK_EQ(decorations->size(), decoration_frames->size());
+
+ // Capture the number of visible left-hand decorations.
+ const size_t left_count = decorations->size();
+
+ // Layout |right_decorations| against the RHS.
+ CalculatePositionsHelper(frame, right_decorations,
+ NSMaxXEdge, kRightDecorationXOffset,
+ decorations, decoration_frames, &frame);
+ DCHECK_EQ(decorations->size(), decoration_frames->size());
+
+ // Reverse the right-hand decorations so that overall everything is
+ // sorted left to right.
+ std::reverse(decorations->begin() + left_count, decorations->end());
+ std::reverse(decoration_frames->begin() + left_count,
+ decoration_frames->end());
+
+ *remaining_frame = frame;
+ return left_count;
+}
+
+} // namespace
+
+@implementation AutocompleteTextFieldCell
+
+- (CGFloat)baselineAdjust {
+ return kBaselineAdjust;
+}
+
+- (CGFloat)cornerRadius {
+ return kCornerRadius;
+}
+
+- (BOOL)shouldDrawBezel {
+ return YES;
+}
+
+- (void)clearDecorations {
+ leftDecorations_.clear();
+ rightDecorations_.clear();
+}
+
+- (void)addLeftDecoration:(LocationBarDecoration*)decoration {
+ leftDecorations_.push_back(decoration);
+}
+
+- (void)addRightDecoration:(LocationBarDecoration*)decoration {
+ rightDecorations_.push_back(decoration);
+}
+
+- (CGFloat)availableWidthInFrame:(const NSRect)frame {
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect textFrame;
+ CalculatePositionsInFrame(frame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &textFrame);
+
+ return NSWidth(textFrame);
+}
+
+- (NSRect)frameForDecoration:(const LocationBarDecoration*)aDecoration
+ inFrame:(NSRect)cellFrame {
+ // Short-circuit if the decoration is known to be not visible.
+ if (aDecoration && !aDecoration->IsVisible())
+ return NSZeroRect;
+
+ // Layout the decorations.
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect textFrame;
+ CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &textFrame);
+
+ // Find our decoration and return the corresponding frame.
+ std::vector<LocationBarDecoration*>::const_iterator iter =
+ std::find(decorations.begin(), decorations.end(), aDecoration);
+ if (iter != decorations.end()) {
+ const size_t index = iter - decorations.begin();
+ return decorationFrames[index];
+ }
+
+ // Decorations which are not visible should have been filtered out
+ // at the top, but return |NSZeroRect| rather than a 0-width rect
+ // for consistency.
+ NOTREACHED();
+ return NSZeroRect;
+}
+
+// Overriden to account for the decorations.
+- (NSRect)textFrameForFrame:(NSRect)cellFrame {
+ // Get the frame adjusted for decorations.
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect textFrame = [super textFrameForFrame:cellFrame];
+ CalculatePositionsInFrame(textFrame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &textFrame);
+
+ // NOTE: This function must closely match the logic in
+ // |-drawInteriorWithFrame:inView:|.
+
+ return textFrame;
+}
+
+- (NSRect)textCursorFrameForFrame:(NSRect)cellFrame {
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect textFrame;
+ size_t left_count =
+ CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &textFrame);
+
+ // Determine the left-most extent for the i-beam cursor.
+ CGFloat minX = NSMinX(textFrame);
+ for (size_t index = left_count; index--; ) {
+ if (decorations[index]->AcceptsMousePress())
+ break;
+
+ // If at leftmost decoration, expand to edge of cell.
+ if (!index) {
+ minX = NSMinX(cellFrame);
+ } else {
+ minX = NSMinX(decorationFrames[index]) - kDecorationHorizontalPad;
+ }
+ }
+
+ // Determine the right-most extent for the i-beam cursor.
+ CGFloat maxX = NSMaxX(textFrame);
+ for (size_t index = left_count; index < decorations.size(); ++index) {
+ if (decorations[index]->AcceptsMousePress())
+ break;
+
+ // If at rightmost decoration, expand to edge of cell.
+ if (index == decorations.size() - 1) {
+ maxX = NSMaxX(cellFrame);
+ } else {
+ maxX = NSMaxX(decorationFrames[index]) + kDecorationHorizontalPad;
+ }
+ }
+
+ // I-beam cursor covers left-most to right-most.
+ return NSMakeRect(minX, NSMinY(textFrame), maxX - minX, NSHeight(textFrame));
+}
+
+- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect workingFrame;
+ CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &workingFrame);
+
+ // Draw the decorations.
+ for (size_t i = 0; i < decorations.size(); ++i) {
+ if (decorations[i])
+ decorations[i]->DrawInFrame(decorationFrames[i], controlView);
+ }
+
+ // NOTE: This function must closely match the logic in
+ // |-textFrameForFrame:|.
+
+ // Superclass draws text portion WRT original |cellFrame|.
+ [super drawInteriorWithFrame:cellFrame inView:controlView];
+}
+
+- (LocationBarDecoration*)decorationForEvent:(NSEvent*)theEvent
+ inRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView
+{
+ const BOOL flipped = [controlView isFlipped];
+ const NSPoint location =
+ [controlView convertPoint:[theEvent locationInWindow] fromView:nil];
+
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect textFrame;
+ CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &textFrame);
+
+ for (size_t i = 0; i < decorations.size(); ++i) {
+ if (NSMouseInRect(location, decorationFrames[i], flipped))
+ return decorations[i];
+ }
+
+ return NULL;
+}
+
+- (NSMenu*)decorationMenuForEvent:(NSEvent*)theEvent
+ inRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView {
+ LocationBarDecoration* decoration =
+ [self decorationForEvent:theEvent inRect:cellFrame ofView:controlView];
+ if (decoration)
+ return decoration->GetMenu();
+ return nil;
+}
+
+- (BOOL)mouseDown:(NSEvent*)theEvent
+ inRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView {
+ LocationBarDecoration* decoration =
+ [self decorationForEvent:theEvent inRect:cellFrame ofView:controlView];
+ if (!decoration || !decoration->AcceptsMousePress())
+ return NO;
+
+ NSRect decorationRect =
+ [self frameForDecoration:decoration inFrame:cellFrame];
+
+ // If the decoration is draggable, then initiate a drag if the user
+ // drags or holds the mouse down for awhile.
+ if (decoration->IsDraggable()) {
+ NSDate* timeout =
+ [NSDate dateWithTimeIntervalSinceNow:kLocationIconDragTimeout];
+ NSEvent* event = [NSApp nextEventMatchingMask:(NSLeftMouseDraggedMask |
+ NSLeftMouseUpMask)
+ untilDate:timeout
+ inMode:NSEventTrackingRunLoopMode
+ dequeue:YES];
+ if (!event || [event type] == NSLeftMouseDragged) {
+ NSPasteboard* pboard = decoration->GetDragPasteboard();
+ DCHECK(pboard);
+
+ NSImage* image = decoration->GetDragImage();
+ DCHECK(image);
+
+ NSRect dragImageRect = decoration->GetDragImageFrame(decorationRect);
+
+ // If the original click is not within |dragImageRect|, then
+ // center the image under the mouse. Otherwise, will drag from
+ // where the click was on the image.
+ const NSPoint mousePoint =
+ [controlView convertPoint:[theEvent locationInWindow] fromView:nil];
+ if (!NSMouseInRect(mousePoint, dragImageRect, [controlView isFlipped])) {
+ dragImageRect.origin =
+ NSMakePoint(mousePoint.x - NSWidth(dragImageRect) / 2.0,
+ mousePoint.y - NSHeight(dragImageRect) / 2.0);
+ }
+
+ // -[NSView dragImage:at:*] wants the images lower-left point,
+ // regardless of -isFlipped. Converting the rect to window base
+ // coordinates doesn't require any special-casing. Note that
+ // -[NSView dragFile:fromRect:*] takes a rect rather than a
+ // point, likely for this exact reason.
+ const NSPoint dragPoint =
+ [controlView convertRect:dragImageRect toView:nil].origin;
+ [[controlView window] dragImage:image
+ at:dragPoint
+ offset:NSZeroSize
+ event:theEvent
+ pasteboard:pboard
+ source:self
+ slideBack:YES];
+
+ return YES;
+ }
+
+ // On mouse-up fall through to mouse-pressed case.
+ DCHECK_EQ([event type], NSLeftMouseUp);
+ }
+
+ if (!decoration->OnMousePressed(decorationRect))
+ return NO;
+
+ return YES;
+}
+
+- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
+ return NSDragOperationCopy;
+}
+
+- (void)updateToolTipsInRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView {
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect textFrame;
+ CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &textFrame);
+
+ for (size_t i = 0; i < decorations.size(); ++i) {
+ NSString* tooltip = decorations[i]->GetToolTip();
+ if ([tooltip length] > 0)
+ [controlView addToolTip:tooltip forRect:decorationFrames[i]];
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell_unittest.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell_unittest.mm
new file mode 100644
index 0000000..1598cad
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell_unittest.mm
@@ -0,0 +1,300 @@
+// Copyright (c) 2010 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 <Cocoa/Cocoa.h>
+
+#include "app/resource_bundle.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/star_decoration.h"
+#include "grit/theme_resources.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+using ::testing::Return;
+using ::testing::StrictMock;
+using ::testing::_;
+
+namespace {
+
+// Width of the field so that we don't have to ask |field_| for it all
+// the time.
+const CGFloat kWidth(300.0);
+
+// A narrow width for tests which test things that don't fit.
+const CGFloat kNarrowWidth(5.0);
+
+class MockDecoration : public LocationBarDecoration {
+ public:
+ virtual CGFloat GetWidthForSpace(CGFloat width) { return 20.0; }
+
+ MOCK_METHOD2(DrawInFrame, void(NSRect frame, NSView* control_view));
+ MOCK_METHOD0(GetToolTip, NSString*());
+};
+
+class AutocompleteTextFieldCellTest : public CocoaTest {
+ public:
+ AutocompleteTextFieldCellTest() {
+ // Make sure this is wide enough to play games with the cell
+ // decorations.
+ const NSRect frame = NSMakeRect(0, 0, kWidth, 30);
+
+ scoped_nsobject<NSTextField> view(
+ [[NSTextField alloc] initWithFrame:frame]);
+ view_ = view.get();
+
+ scoped_nsobject<AutocompleteTextFieldCell> cell(
+ [[AutocompleteTextFieldCell alloc] initTextCell:@"Testing"]);
+ [cell setEditable:YES];
+ [cell setBordered:YES];
+
+ [cell clearDecorations];
+ mock_left_decoration_.SetVisible(false);
+ [cell addLeftDecoration:&mock_left_decoration_];
+ mock_right_decoration0_.SetVisible(false);
+ mock_right_decoration1_.SetVisible(false);
+ [cell addRightDecoration:&mock_right_decoration0_];
+ [cell addRightDecoration:&mock_right_decoration1_];
+
+ [view_ setCell:cell.get()];
+
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ NSTextField* view_;
+ MockDecoration mock_left_decoration_;
+ MockDecoration mock_right_decoration0_;
+ MockDecoration mock_right_decoration1_;
+};
+
+// Basic view tests (AddRemove, Display).
+TEST_VIEW(AutocompleteTextFieldCellTest, view_);
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+// Flaky, disabled. Bug http://crbug.com/49522
+TEST_F(AutocompleteTextFieldCellTest, DISABLED_FocusedDisplay) {
+ [view_ display];
+
+ // Test focused drawing.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:view_];
+ [view_ display];
+ [test_window() clearPretendKeyWindowAndFirstResponder];
+
+ // Test display of various cell configurations.
+ AutocompleteTextFieldCell* cell =
+ static_cast<AutocompleteTextFieldCell*>([view_ cell]);
+
+ // Load available decorations and try drawing. To make sure that
+ // they are actually drawn, check that |GetWidthForSpace()| doesn't
+ // indicate that they should be omitted.
+ const CGFloat kVeryWide = 1000.0;
+
+ SelectedKeywordDecoration selected_keyword_decoration([view_ font]);
+ selected_keyword_decoration.SetVisible(true);
+ selected_keyword_decoration.SetKeyword(std::wstring(L"Google"), false);
+ [cell addLeftDecoration:&selected_keyword_decoration];
+ EXPECT_NE(selected_keyword_decoration.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ // TODO(shess): This really wants a |LocationBarViewMac|, but only a
+ // few methods reference it, so this works well enough. But
+ // something better would be nice.
+ LocationIconDecoration location_icon_decoration(NULL);
+ location_icon_decoration.SetVisible(true);
+ location_icon_decoration.SetImage([NSImage imageNamed:@"NSApplicationIcon"]);
+ [cell addLeftDecoration:&location_icon_decoration];
+ EXPECT_NE(location_icon_decoration.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ EVBubbleDecoration ev_bubble_decoration(&location_icon_decoration,
+ [view_ font]);
+ ev_bubble_decoration.SetVisible(true);
+ ev_bubble_decoration.SetImage([NSImage imageNamed:@"NSApplicationIcon"]);
+ ev_bubble_decoration.SetLabel(@"Application");
+ [cell addLeftDecoration:&ev_bubble_decoration];
+ EXPECT_NE(ev_bubble_decoration.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ StarDecoration star_decoration(NULL);
+ star_decoration.SetVisible(true);
+ [cell addRightDecoration:&star_decoration];
+ EXPECT_NE(star_decoration.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ KeywordHintDecoration keyword_hint_decoration([view_ font]);
+ keyword_hint_decoration.SetVisible(true);
+ keyword_hint_decoration.SetKeyword(std::wstring(L"google"), false);
+ [cell addRightDecoration:&keyword_hint_decoration];
+ EXPECT_NE(keyword_hint_decoration.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ // Make sure we're actually calling |DrawInFrame()|.
+ StrictMock<MockDecoration> mock_decoration;
+ mock_decoration.SetVisible(true);
+ [cell addLeftDecoration:&mock_decoration];
+ EXPECT_CALL(mock_decoration, DrawInFrame(_, _));
+ EXPECT_NE(mock_decoration.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ [view_ display];
+
+ [cell clearDecorations];
+}
+
+TEST_F(AutocompleteTextFieldCellTest, TextFrame) {
+ AutocompleteTextFieldCell* cell =
+ static_cast<AutocompleteTextFieldCell*>([view_ cell]);
+ const NSRect bounds([view_ bounds]);
+ NSRect textFrame;
+
+ // The cursor frame should stay the same throughout.
+ const NSRect cursorFrame([cell textCursorFrameForFrame:bounds]);
+ EXPECT_TRUE(NSEqualRects(cursorFrame, bounds));
+
+ // At default settings, everything goes to the text area.
+ textFrame = [cell textFrameForFrame:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(textFrame));
+ EXPECT_TRUE(NSContainsRect(bounds, textFrame));
+ EXPECT_EQ(NSMinX(bounds), NSMinX(textFrame));
+ EXPECT_EQ(NSMaxX(bounds), NSMaxX(textFrame));
+ EXPECT_TRUE(NSContainsRect(cursorFrame, textFrame));
+
+ // Decoration on the left takes up space.
+ mock_left_decoration_.SetVisible(true);
+ textFrame = [cell textFrameForFrame:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(textFrame));
+ EXPECT_TRUE(NSContainsRect(bounds, textFrame));
+ EXPECT_GT(NSMinX(textFrame), NSMinX(bounds));
+ EXPECT_TRUE(NSContainsRect(cursorFrame, textFrame));
+}
+
+// The editor frame should be slightly inset from the text frame.
+TEST_F(AutocompleteTextFieldCellTest, DrawingRectForBounds) {
+ AutocompleteTextFieldCell* cell =
+ static_cast<AutocompleteTextFieldCell*>([view_ cell]);
+ const NSRect bounds([view_ bounds]);
+ NSRect textFrame, drawingRect;
+
+ textFrame = [cell textFrameForFrame:bounds];
+ drawingRect = [cell drawingRectForBounds:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSContainsRect(textFrame, NSInsetRect(drawingRect, 1, 1)));
+
+ // Save the starting frame for after clear.
+ const NSRect originalDrawingRect = drawingRect;
+
+ mock_left_decoration_.SetVisible(true);
+ textFrame = [cell textFrameForFrame:bounds];
+ drawingRect = [cell drawingRectForBounds:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSContainsRect(NSInsetRect(textFrame, 1, 1), drawingRect));
+
+ mock_right_decoration0_.SetVisible(true);
+ textFrame = [cell textFrameForFrame:bounds];
+ drawingRect = [cell drawingRectForBounds:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSContainsRect(NSInsetRect(textFrame, 1, 1), drawingRect));
+
+ mock_left_decoration_.SetVisible(false);
+ mock_right_decoration0_.SetVisible(false);
+ drawingRect = [cell drawingRectForBounds:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSEqualRects(drawingRect, originalDrawingRect));
+}
+
+// Test that left decorations are at the correct edge of the cell.
+TEST_F(AutocompleteTextFieldCellTest, LeftDecorationFrame) {
+ AutocompleteTextFieldCell* cell =
+ static_cast<AutocompleteTextFieldCell*>([view_ cell]);
+ const NSRect bounds = [view_ bounds];
+
+ mock_left_decoration_.SetVisible(true);
+ const NSRect decorationRect =
+ [cell frameForDecoration:&mock_left_decoration_ inFrame:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(decorationRect));
+ EXPECT_TRUE(NSContainsRect(bounds, decorationRect));
+
+ // Decoration should be left of |drawingRect|.
+ const NSRect drawingRect = [cell drawingRectForBounds:bounds];
+ EXPECT_GT(NSMinX(drawingRect), NSMinX(decorationRect));
+
+ // Decoration should be left of |textFrame|.
+ const NSRect textFrame = [cell textFrameForFrame:bounds];
+ EXPECT_GT(NSMinX(textFrame), NSMinX(decorationRect));
+}
+
+// Test that right decorations are at the correct edge of the cell.
+TEST_F(AutocompleteTextFieldCellTest, RightDecorationFrame) {
+ AutocompleteTextFieldCell* cell =
+ static_cast<AutocompleteTextFieldCell*>([view_ cell]);
+ const NSRect bounds = [view_ bounds];
+
+ mock_right_decoration0_.SetVisible(true);
+ mock_right_decoration1_.SetVisible(true);
+
+ const NSRect decoration0Rect =
+ [cell frameForDecoration:&mock_right_decoration0_ inFrame:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(decoration0Rect));
+ EXPECT_TRUE(NSContainsRect(bounds, decoration0Rect));
+
+ // Right-side decorations are ordered from rightmost to leftmost.
+ // Outer decoration (0) to right of inner decoration (1).
+ const NSRect decoration1Rect =
+ [cell frameForDecoration:&mock_right_decoration1_ inFrame:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(decoration1Rect));
+ EXPECT_TRUE(NSContainsRect(bounds, decoration1Rect));
+ EXPECT_LT(NSMinX(decoration1Rect), NSMinX(decoration0Rect));
+
+ // Decoration should be right of |drawingRect|.
+ const NSRect drawingRect = [cell drawingRectForBounds:bounds];
+ EXPECT_LT(NSMinX(drawingRect), NSMinX(decoration1Rect));
+
+ // Decoration should be right of |textFrame|.
+ const NSRect textFrame = [cell textFrameForFrame:bounds];
+ EXPECT_LT(NSMinX(textFrame), NSMinX(decoration1Rect));
+}
+
+// Verify -[AutocompleteTextFieldCell updateToolTipsInRect:ofView:].
+TEST_F(AutocompleteTextFieldCellTest, UpdateToolTips) {
+ NSString* tooltip = @"tooltip";
+
+ // Left decoration returns a tooltip, make sure it is called at
+ // least once.
+ mock_left_decoration_.SetVisible(true);
+ EXPECT_CALL(mock_left_decoration_, GetToolTip())
+ .WillOnce(Return(tooltip))
+ .WillRepeatedly(Return(tooltip));
+
+ // Right decoration returns no tooltip, make sure it is called at
+ // least once.
+ mock_right_decoration0_.SetVisible(true);
+ EXPECT_CALL(mock_right_decoration0_, GetToolTip())
+ .WillOnce(Return((NSString*)nil))
+ .WillRepeatedly(Return((NSString*)nil));
+
+ AutocompleteTextFieldCell* cell =
+ static_cast<AutocompleteTextFieldCell*>([view_ cell]);
+ const NSRect bounds = [view_ bounds];
+ const NSRect leftDecorationRect =
+ [cell frameForDecoration:&mock_left_decoration_ inFrame:bounds];
+
+ // |controlView| gets the tooltip for the left decoration.
+ id controlView = [OCMockObject mockForClass:[AutocompleteTextField class]];
+ [[controlView expect] addToolTip:tooltip forRect:leftDecorationRect];
+
+ [cell updateToolTipsInRect:bounds ofView:controlView];
+
+ [controlView verify];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h
new file mode 100644
index 0000000..905bc84
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h
@@ -0,0 +1,56 @@
+// Copyright (c) 2010 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 <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/url_drop_target.h"
+
+@class AutocompleteTextField;
+class AutocompleteTextFieldObserver;
+
+// AutocompleteTextFieldEditor customized the AutocompletTextField
+// field editor (helper text-view used in editing). It intercepts UI
+// events for forwarding to the core Omnibox code. It also undoes
+// some of the effects of using styled text in the Omnibox (the text
+// is styled but should not appear that way when copied to the
+// pasteboard).
+
+// Field editor used for the autocomplete field.
+@interface AutocompleteTextFieldEditor : NSTextView<URLDropTarget> {
+ // Handles being a drag-and-drop target. We handle DnD directly instead
+ // allowing the |AutocompletTextField| to handle it (by making an empty
+ // |-updateDragTypeRegistration|), since the latter results in a weird
+ // start-up time regression.
+ scoped_nsobject<URLDropTargetHandler> dropHandler_;
+
+ scoped_nsobject<NSCharacterSet> forbiddenCharacters_;
+
+ // Indicates if the field editor's interpretKeyEvents: method is being called.
+ // If it's YES, then we should postpone the call to the observer's
+ // OnDidChange() method after the field editor's interpretKeyEvents: method
+ // is finished, rather than calling it in textDidChange: method. Because the
+ // input method may update the marked text after inserting some text, but we
+ // need the observer be aware of the marked text as well.
+ BOOL interpretingKeyEvents_;
+
+ // Indicates if the text has been changed by key events.
+ BOOL textChangedByKeyEvents_;
+}
+
+// The delegate is always an AutocompleteTextField*. Override the superclass
+// implementations to allow for proper typing.
+- (AutocompleteTextField*)delegate;
+- (void)setDelegate:(AutocompleteTextField*)delegate;
+
+// Sets attributed string programatically through the field editor's text
+// storage object.
+- (void)setAttributedString:(NSAttributedString*)aString;
+
+@end
+
+@interface AutocompleteTextFieldEditor(PrivateTestMethods)
+- (AutocompleteTextFieldObserver*)observer;
+- (void)pasteAndGo:sender;
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm
new file mode 100644
index 0000000..1b52e70
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm
@@ -0,0 +1,371 @@
+// Copyright (c) 2010 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/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/string_util.h"
+#include "grit/generated_resources.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h" // IDC_*
+#include "chrome/browser/browser_list.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+
+@implementation AutocompleteTextFieldEditor
+
+- (id)initWithFrame:(NSRect)frameRect {
+ if ((self = [super initWithFrame:frameRect])) {
+ dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
+
+ forbiddenCharacters_.reset([[NSCharacterSet controlCharacterSet] retain]);
+ }
+ return self;
+}
+
+// If the entire field is selected, drag the same data as would be
+// dragged from the field's location icon. In some cases the textual
+// contents will not contain relevant data (for instance, "http://" is
+// stripped from URLs).
+- (BOOL)dragSelectionWithEvent:(NSEvent *)event
+ offset:(NSSize)mouseOffset
+ slideBack:(BOOL)slideBack {
+ AutocompleteTextFieldObserver* observer = [self observer];
+ DCHECK(observer);
+ if (observer && observer->CanCopy()) {
+ NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
+ observer->CopyToPasteboard(pboard);
+
+ NSPoint p;
+ NSImage* image = [self dragImageForSelectionWithEvent:event origin:&p];
+
+ [self dragImage:image
+ at:p
+ offset:mouseOffset
+ event:event
+ pasteboard:pboard
+ source:self
+ slideBack:slideBack];
+ return YES;
+ }
+ return [super dragSelectionWithEvent:event
+ offset:mouseOffset
+ slideBack:slideBack];
+}
+
+- (void)copy:(id)sender {
+ AutocompleteTextFieldObserver* observer = [self observer];
+ DCHECK(observer);
+ if (observer && observer->CanCopy())
+ observer->CopyToPasteboard([NSPasteboard generalPasteboard]);
+}
+
+- (void)cut:(id)sender {
+ [self copy:sender];
+ [self delete:nil];
+}
+
+// This class assumes that the delegate is an AutocompleteTextField.
+// Enforce that assumption.
+- (AutocompleteTextField*)delegate {
+ AutocompleteTextField* delegate =
+ static_cast<AutocompleteTextField*>([super delegate]);
+ DCHECK(delegate == nil ||
+ [delegate isKindOfClass:[AutocompleteTextField class]]);
+ return delegate;
+}
+
+- (void)setDelegate:(AutocompleteTextField*)delegate {
+ DCHECK(delegate == nil ||
+ [delegate isKindOfClass:[AutocompleteTextField class]]);
+ [super setDelegate:delegate];
+}
+
+// Convenience method for retrieving the observer from the delegate.
+- (AutocompleteTextFieldObserver*)observer {
+ return [[self delegate] observer];
+}
+
+- (void)paste:(id)sender {
+ AutocompleteTextFieldObserver* observer = [self observer];
+ DCHECK(observer);
+ if (observer) {
+ observer->OnPaste();
+ }
+}
+
+- (void)pasteAndGo:sender {
+ AutocompleteTextFieldObserver* observer = [self observer];
+ DCHECK(observer);
+ if (observer) {
+ observer->OnPasteAndGo();
+ }
+}
+
+// We have rich text, but it shouldn't be modified by the user, so
+// don't update the font panel. In theory, -setUsesFontPanel: should
+// accomplish this, but that gets called frequently with YES when
+// NSTextField and NSTextView synchronize their contents. That is
+// probably unavoidable because in most cases having rich text in the
+// field you probably would expect it to update the font panel.
+- (void)updateFontPanel {}
+
+// No ruler bar, so don't update any of that state, either.
+- (void)updateRuler {}
+
+- (NSMenu*)menuForEvent:(NSEvent*)event {
+ // Give the control a chance to provide page-action menus.
+ // NOTE: Note that page actions aren't even in the editor's
+ // boundaries! The Cocoa control implementation seems to do a
+ // blanket forward to here if nothing more specific is returned from
+ // the control and cell calls.
+ // TODO(shess): Determine if the page-action part of this can be
+ // moved to the cell.
+ NSMenu* actionMenu = [[self delegate] decorationMenuForEvent:event];
+ if (actionMenu)
+ return actionMenu;
+
+ NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"TITLE"] autorelease];
+ [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CUT)
+ action:@selector(cut:)
+ keyEquivalent:@""];
+ [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_COPY)
+ action:@selector(copy:)
+ keyEquivalent:@""];
+ [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_PASTE)
+ action:@selector(paste:)
+ keyEquivalent:@""];
+
+ // TODO(shess): If the control is not editable, should we show a
+ // greyed-out "Paste and Go"?
+ if ([self isEditable]) {
+ // Paste and go/search.
+ AutocompleteTextFieldObserver* observer = [self observer];
+ DCHECK(observer);
+ if (observer && observer->CanPasteAndGo()) {
+ const int string_id = observer->GetPasteActionStringId();
+ NSString* label = l10n_util::GetNSStringWithFixup(string_id);
+
+ // TODO(rohitrao): If the clipboard is empty, should we show a
+ // greyed-out "Paste and Go" or nothing at all?
+ if ([label length]) {
+ [menu addItemWithTitle:label
+ action:@selector(pasteAndGo:)
+ keyEquivalent:@""];
+ }
+ }
+
+ NSString* label = l10n_util::GetNSStringWithFixup(IDS_EDIT_SEARCH_ENGINES);
+ DCHECK([label length]);
+ if ([label length]) {
+ [menu addItem:[NSMenuItem separatorItem]];
+ NSMenuItem* item = [menu addItemWithTitle:label
+ action:@selector(commandDispatch:)
+ keyEquivalent:@""];
+ [item setTag:IDC_EDIT_SEARCH_ENGINES];
+ }
+ }
+
+ return menu;
+}
+
+// (Overridden from NSResponder)
+- (BOOL)becomeFirstResponder {
+ BOOL doAccept = [super becomeFirstResponder];
+ AutocompleteTextField* field = [self delegate];
+ // Only lock visibility if we've been set up with a delegate (the text field).
+ if (doAccept && field) {
+ // Give the text field ownership of the visibility lock. (The first
+ // responder dance between the field and the field editor is a little
+ // weird.)
+ [[BrowserWindowController browserWindowControllerForView:field]
+ lockBarVisibilityForOwner:field withAnimation:YES delay:NO];
+ }
+ return doAccept;
+}
+
+// (Overridden from NSResponder)
+- (BOOL)resignFirstResponder {
+ BOOL doResign = [super resignFirstResponder];
+ AutocompleteTextField* field = [self delegate];
+ // Only lock visibility if we've been set up with a delegate (the text field).
+ if (doResign && field) {
+ // Give the text field ownership of the visibility lock.
+ [[BrowserWindowController browserWindowControllerForView:field]
+ releaseBarVisibilityForOwner:field withAnimation:YES delay:YES];
+
+ AutocompleteTextFieldObserver* observer = [self observer];
+ if (observer)
+ observer->OnKillFocus();
+ }
+ return doResign;
+}
+
+// (URLDropTarget protocol)
+- (id<URLDropTargetController>)urlDropController {
+ BrowserWindowController* windowController =
+ [BrowserWindowController browserWindowControllerForView:self];
+ return [windowController toolbarController];
+}
+
+// (URLDropTarget protocol)
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
+ // Make ourself the first responder (even though we're presumably already the
+ // first responder), which will select the text to indicate that our contents
+ // would be replaced by a drop.
+ [[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];
+}
+
+// Prevent control characters from being entered into the Omnibox.
+// This is invoked for keyboard entry, not for pasting.
+- (void)insertText:(id)aString {
+ // This method is documented as received either |NSString| or
+ // |NSAttributedString|. The autocomplete code will restyle the
+ // results in any case, so simplify by always using |NSString|.
+ if ([aString isKindOfClass:[NSAttributedString class]])
+ aString = [aString string];
+
+ // Repeatedly remove control characters. The loop will only ever
+ // execute at allwhen the user enters control characters (using
+ // Ctrl-Alt- or Ctrl-Q). Making this generally efficient would
+ // probably be a loss, since the input always seems to be a single
+ // character.
+ NSRange range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
+ while (range.location != NSNotFound) {
+ aString = [aString stringByReplacingCharactersInRange:range withString:@""];
+ range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
+ }
+ DCHECK_EQ(range.length, 0U);
+
+ // NOTE: If |aString| is empty, this intentionally replaces the
+ // selection with empty. This seems consistent with the case where
+ // the input contained a mixture of characters and the string ended
+ // up not empty.
+ [super insertText:aString];
+}
+
+- (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange {
+ [super setMarkedText:aString selectedRange:selRange];
+
+ // Because the AutocompleteEditViewMac class treats marked text as content,
+ // we need to treat the change to marked text as content change as well.
+ [self didChangeText];
+}
+
+- (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange
+ granularity:(NSSelectionGranularity)granularity {
+ AutocompleteTextFieldObserver* observer = [self observer];
+ NSRange modifiedRange = [super selectionRangeForProposedRange:proposedSelRange
+ granularity:granularity];
+ if (observer)
+ return observer->SelectionRangeForProposedRange(modifiedRange);
+ return modifiedRange;
+}
+
+
+
+
+- (void)setSelectedRange:(NSRange)charRange
+ affinity:(NSSelectionAffinity)affinity
+ stillSelecting:(BOOL)flag {
+ [super setSelectedRange:charRange affinity:affinity stillSelecting:flag];
+
+ // We're only interested in selection changes directly caused by keyboard
+ // input from the user.
+ if (interpretingKeyEvents_)
+ textChangedByKeyEvents_ = YES;
+}
+
+- (void)interpretKeyEvents:(NSArray *)eventArray {
+ DCHECK(!interpretingKeyEvents_);
+ interpretingKeyEvents_ = YES;
+ textChangedByKeyEvents_ = NO;
+ [super interpretKeyEvents:eventArray];
+
+ AutocompleteTextFieldObserver* observer = [self observer];
+ if (textChangedByKeyEvents_ && observer)
+ observer->OnDidChange();
+
+ DCHECK(interpretingKeyEvents_);
+ interpretingKeyEvents_ = NO;
+}
+
+- (void)didChangeText {
+ [super didChangeText];
+
+ AutocompleteTextFieldObserver* observer = [self observer];
+ if (observer) {
+ if (!interpretingKeyEvents_)
+ observer->OnDidChange();
+ else
+ textChangedByKeyEvents_ = YES;
+ }
+}
+
+- (void)doCommandBySelector:(SEL)cmd {
+ // TODO(shess): Review code for cases where we're fruitlessly attempting to
+ // work in spite of not having an observer.
+ AutocompleteTextFieldObserver* observer = [self observer];
+
+ if (observer && observer->OnDoCommandBySelector(cmd)) {
+ // The observer should already be aware of any changes to the text, so
+ // setting |textChangedByKeyEvents_| to NO to prevent its OnDidChange()
+ // method from being called unnecessarily.
+ textChangedByKeyEvents_ = NO;
+ return;
+ }
+
+ // If the escape key was pressed and no revert happened and we're in
+ // fullscreen mode, make it resign key.
+ if (cmd == @selector(cancelOperation:)) {
+ BrowserWindowController* windowController =
+ [BrowserWindowController browserWindowControllerForView:self];
+ if ([windowController isFullscreen]) {
+ [windowController focusTabContents];
+ return;
+ }
+ }
+
+ [super doCommandBySelector:cmd];
+}
+
+- (void)setAttributedString:(NSAttributedString*)aString {
+ NSTextStorage* textStorage = [self textStorage];
+ DCHECK(textStorage);
+ [textStorage setAttributedString:aString];
+
+ // The text has been changed programmatically. The observer should know
+ // this change, so setting |textChangedByKeyEvents_| to NO to
+ // prevent its OnDidChange() method from being called unnecessarily.
+ textChangedByKeyEvents_ = NO;
+}
+
+- (void)mouseDown:(NSEvent*)theEvent {
+ // Close the popup before processing the event.
+ AutocompleteTextFieldObserver* observer = [self observer];
+ if (observer)
+ observer->ClosePopup();
+
+ [super mouseDown:theEvent];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor_unittest.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor_unittest.mm
new file mode 100644
index 0000000..e43b734
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor_unittest.mm
@@ -0,0 +1,297 @@
+// Copyright (c) 2010 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/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "base/string_util.h"
+#include "chrome/app/chrome_command_ids.h" // IDC_*
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h"
+#import "chrome/browser/ui/cocoa/test_event_utils.h"
+#include "grit/generated_resources.h"
+#include "testing/gmock/include/gmock/gmock-matchers.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+using ::testing::Return;
+using ::testing::ReturnArg;
+using ::testing::StrictMock;
+using ::testing::A;
+
+namespace {
+
+// TODO(shess): Very similar to AutocompleteTextFieldTest. Maybe
+// those can be shared.
+
+class AutocompleteTextFieldEditorTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ NSRect frame = NSMakeRect(0, 0, 50, 30);
+ scoped_nsobject<AutocompleteTextField> field(
+ [[AutocompleteTextField alloc] initWithFrame:frame]);
+ field_ = field.get();
+ [field_ setStringValue:@"Testing"];
+ [[test_window() contentView] addSubview:field_];
+
+ // Arrange for |field_| to get the right field editor.
+ window_delegate_.reset(
+ [[AutocompleteTextFieldWindowTestDelegate alloc] init]);
+ [test_window() setDelegate:window_delegate_.get()];
+
+ // Get the field editor setup.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ editor_ = static_cast<AutocompleteTextFieldEditor*>([field_ currentEditor]);
+
+ EXPECT_TRUE(editor_);
+ EXPECT_TRUE([editor_ isKindOfClass:[AutocompleteTextFieldEditor class]]);
+ }
+
+ AutocompleteTextFieldEditor* editor_;
+ AutocompleteTextField* field_;
+ scoped_nsobject<AutocompleteTextFieldWindowTestDelegate> window_delegate_;
+};
+
+// Disabled because it crashes sometimes. http://crbug.com/49522
+// Can't rename DISABLED_ because the TEST_VIEW macro prepends.
+// http://crbug.com/53621
+#if 0
+TEST_VIEW(AutocompleteTextFieldEditorTest, field_);
+#endif
+
+// Test that control characters are stripped from insertions.
+TEST_F(AutocompleteTextFieldEditorTest, InsertStripsControlChars) {
+ // Sets a string in the field.
+ NSString* test_string = @"astring";
+ [field_ setStringValue:test_string];
+ [editor_ selectAll:nil];
+
+ [editor_ insertText:@"t"];
+ EXPECT_NSEQ(@"t", [field_ stringValue]);
+
+ [editor_ insertText:@"h"];
+ EXPECT_NSEQ(@"th", [field_ stringValue]);
+
+ // TAB doesn't get inserted.
+ [editor_ insertText:[NSString stringWithFormat:@"%c", 7]];
+ EXPECT_NSEQ(@"th", [field_ stringValue]);
+
+ // Newline doesn't get inserted.
+ [editor_ insertText:[NSString stringWithFormat:@"%c", 12]];
+ EXPECT_NSEQ(@"th", [field_ stringValue]);
+
+ // Multi-character strings get through.
+ [editor_ insertText:[NSString stringWithFormat:@"i%cs%c", 8, 127]];
+ EXPECT_NSEQ(@"this", [field_ stringValue]);
+
+ // Attempting to insert newline when everything is selected clears
+ // the field.
+ [editor_ selectAll:nil];
+ [editor_ insertText:[NSString stringWithFormat:@"%c", 12]];
+ EXPECT_NSEQ(@"", [field_ stringValue]);
+}
+
+// Test that |delegate| can provide page action menus.
+TEST_F(AutocompleteTextFieldEditorTest, PageActionMenus) {
+ // The event just needs to be something the mock can recognize.
+ NSEvent* event =
+ test_event_utils::MouseEventAtPoint(NSZeroPoint, NSRightMouseDown, 0);
+
+ // Trivial menu which we can recognize and which doesn't look like
+ // the default editor context menu.
+ scoped_nsobject<id> menu([[NSMenu alloc] initWithTitle:@"Menu"]);
+ [menu addItemWithTitle:@"Go Fish"
+ action:@selector(goFish:)
+ keyEquivalent:@""];
+
+ // For OCMOCK_VALUE().
+ BOOL yes = YES;
+
+ // So that we don't have to mock the observer.
+ [editor_ setEditable:NO];
+
+ // The delegate's intercept point gets called, and results are
+ // propagated back.
+ {
+ id delegate = [OCMockObject mockForClass:[AutocompleteTextField class]];
+ [[[delegate stub] andReturnValue:OCMOCK_VALUE(yes)]
+ isKindOfClass:[AutocompleteTextField class]];
+ [[[delegate expect] andReturn:menu.get()] decorationMenuForEvent:event];
+ [editor_ setDelegate:delegate];
+ NSMenu* contextMenu = [editor_ menuForEvent:event];
+ [delegate verify];
+ [editor_ setDelegate:nil];
+
+ EXPECT_EQ(contextMenu, menu.get());
+ }
+
+ // If the delegate does not return any menu, the default menu is
+ // returned.
+ {
+ id delegate = [OCMockObject mockForClass:[AutocompleteTextField class]];
+ [[[delegate stub] andReturnValue:OCMOCK_VALUE(yes)]
+ isKindOfClass:[AutocompleteTextField class]];
+ [[[delegate expect] andReturn:nil] decorationMenuForEvent:event];
+ [editor_ setDelegate:delegate];
+ NSMenu* contextMenu = [editor_ menuForEvent:event];
+ [delegate verify];
+ [editor_ setDelegate:nil];
+
+ EXPECT_NE(contextMenu, menu.get());
+ NSArray* items = [contextMenu itemArray];
+ ASSERT_GT([items count], 0U);
+ EXPECT_EQ(@selector(cut:), [[items objectAtIndex:0] action])
+ << "action is: " << sel_getName([[items objectAtIndex:0] action]);
+ }
+}
+
+// Base class for testing AutocompleteTextFieldObserver messages.
+class AutocompleteTextFieldEditorObserverTest
+ : public AutocompleteTextFieldEditorTest {
+ public:
+ virtual void SetUp() {
+ AutocompleteTextFieldEditorTest::SetUp();
+ [field_ setObserver:&field_observer_];
+ }
+
+ virtual void TearDown() {
+ // Clear the observer so that we don't show output for
+ // uninteresting messages to the mock (for instance, if |field_| has
+ // focus at the end of the test).
+ [field_ setObserver:NULL];
+
+ AutocompleteTextFieldEditorTest::TearDown();
+ }
+
+ StrictMock<MockAutocompleteTextFieldObserver> field_observer_;
+};
+
+// Test that the field editor is linked in correctly.
+TEST_F(AutocompleteTextFieldEditorTest, FirstResponder) {
+ EXPECT_EQ(editor_, [field_ currentEditor]);
+ EXPECT_TRUE([editor_ isDescendantOf:field_]);
+ EXPECT_EQ([editor_ delegate], field_);
+ EXPECT_EQ([editor_ observer], [field_ observer]);
+}
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+TEST_F(AutocompleteTextFieldEditorTest, Display) {
+ [field_ display];
+ [editor_ display];
+}
+
+// Test that -paste: is correctly delegated to the observer.
+TEST_F(AutocompleteTextFieldEditorObserverTest, Paste) {
+ EXPECT_CALL(field_observer_, OnPaste());
+ [editor_ paste:nil];
+}
+
+// Test that -copy: is correctly delegated to the observer.
+TEST_F(AutocompleteTextFieldEditorObserverTest, Copy) {
+ EXPECT_CALL(field_observer_, CanCopy())
+ .WillOnce(Return(true));
+ EXPECT_CALL(field_observer_, CopyToPasteboard(A<NSPasteboard*>()))
+ .Times(1);
+ [editor_ copy:nil];
+}
+
+// Test that -cut: is correctly delegated to the observer and clears
+// the text field.
+TEST_F(AutocompleteTextFieldEditorObserverTest, Cut) {
+ // Sets a string in the field.
+ NSString* test_string = @"astring";
+ EXPECT_CALL(field_observer_, OnDidBeginEditing());
+ EXPECT_CALL(field_observer_, OnDidChange());
+ EXPECT_CALL(field_observer_, SelectionRangeForProposedRange(A<NSRange>()))
+ .WillRepeatedly(ReturnArg<0>());
+ [editor_ setString:test_string];
+ [editor_ selectAll:nil];
+
+ // Calls cut.
+ EXPECT_CALL(field_observer_, CanCopy())
+ .WillOnce(Return(true));
+ EXPECT_CALL(field_observer_, CopyToPasteboard(A<NSPasteboard*>()))
+ .Times(1);
+ [editor_ cut:nil];
+
+ // Check if the field is cleared.
+ ASSERT_EQ([[editor_ textStorage] length], 0U);
+}
+
+// Test that -pasteAndGo: is correctly delegated to the observer.
+TEST_F(AutocompleteTextFieldEditorObserverTest, PasteAndGo) {
+ EXPECT_CALL(field_observer_, OnPasteAndGo());
+ [editor_ pasteAndGo:nil];
+}
+
+// Test that the menu is constructed correctly when CanPasteAndGo().
+TEST_F(AutocompleteTextFieldEditorObserverTest, CanPasteAndGoMenu) {
+ EXPECT_CALL(field_observer_, CanPasteAndGo())
+ .WillOnce(Return(true));
+ EXPECT_CALL(field_observer_, GetPasteActionStringId())
+ .WillOnce(Return(IDS_PASTE_AND_GO));
+
+ NSMenu* menu = [editor_ menuForEvent:nil];
+ NSArray* items = [menu itemArray];
+ ASSERT_EQ([items count], 6U);
+ // TODO(shess): Check the titles, too?
+ NSUInteger i = 0; // Use an index to make future changes easier.
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(cut:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(copy:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(paste:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(pasteAndGo:));
+ EXPECT_TRUE([[items objectAtIndex:i++] isSeparatorItem]);
+
+ EXPECT_EQ([[items objectAtIndex:i] action], @selector(commandDispatch:));
+ EXPECT_EQ([[items objectAtIndex:i] tag], IDC_EDIT_SEARCH_ENGINES);
+ i++;
+}
+
+// Test that the menu is constructed correctly when !CanPasteAndGo().
+TEST_F(AutocompleteTextFieldEditorObserverTest, CannotPasteAndGoMenu) {
+ EXPECT_CALL(field_observer_, CanPasteAndGo())
+ .WillOnce(Return(false));
+
+ NSMenu* menu = [editor_ menuForEvent:nil];
+ NSArray* items = [menu itemArray];
+ ASSERT_EQ([items count], 5U);
+ // TODO(shess): Check the titles, too?
+ NSUInteger i = 0; // Use an index to make future changes easier.
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(cut:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(copy:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(paste:));
+ EXPECT_TRUE([[items objectAtIndex:i++] isSeparatorItem]);
+
+ EXPECT_EQ([[items objectAtIndex:i] action], @selector(commandDispatch:));
+ EXPECT_EQ([[items objectAtIndex:i] tag], IDC_EDIT_SEARCH_ENGINES);
+ i++;
+}
+
+// Test that the menu is constructed correctly when field isn't
+// editable.
+TEST_F(AutocompleteTextFieldEditorObserverTest, CanPasteAndGoMenuNotEditable) {
+ [field_ setEditable:NO];
+ [editor_ setEditable:NO];
+
+ // Never call these when not editable.
+ EXPECT_CALL(field_observer_, CanPasteAndGo())
+ .Times(0);
+ EXPECT_CALL(field_observer_, GetPasteActionStringId())
+ .Times(0);
+
+ NSMenu* menu = [editor_ menuForEvent:nil];
+ NSArray* items = [menu itemArray];
+ ASSERT_EQ([items count], 3U);
+ // TODO(shess): Check the titles, too?
+ NSUInteger i = 0; // Use an index to make future changes easier.
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(cut:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(copy:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(paste:));
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest.mm
new file mode 100644
index 0000000..14d5476
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest.mm
@@ -0,0 +1,792 @@
+// Copyright (c) 2010 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 <Cocoa/Cocoa.h>
+
+#include "app/resource_bundle.h"
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "grit/theme_resources.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+using ::testing::A;
+using ::testing::InSequence;
+using ::testing::Return;
+using ::testing::ReturnArg;
+using ::testing::StrictMock;
+using ::testing::_;
+
+namespace {
+
+class MockDecoration : public LocationBarDecoration {
+ public:
+ virtual CGFloat GetWidthForSpace(CGFloat width) { return 20.0; }
+
+ virtual void DrawInFrame(NSRect frame, NSView* control_view) { ; }
+ MOCK_METHOD0(AcceptsMousePress, bool());
+ MOCK_METHOD1(OnMousePressed, bool(NSRect frame));
+ MOCK_METHOD0(GetMenu, NSMenu*());
+};
+
+// Mock up an incrementing event number.
+NSUInteger eventNumber = 0;
+
+// Create an event of the indicated |type| at |point| within |view|.
+// TODO(shess): Would be nice to have a MockApplication which provided
+// nifty accessors to create these things and inject them. It could
+// even provide functions for "Click and drag mouse from point A to
+// point B".
+NSEvent* Event(NSView* view, const NSPoint point, const NSEventType type,
+ const NSUInteger clickCount) {
+ NSWindow* window([view window]);
+ const NSPoint locationInWindow([view convertPoint:point toView:nil]);
+ const NSPoint location([window convertBaseToScreen:locationInWindow]);
+ return [NSEvent mouseEventWithType:type
+ location:location
+ modifierFlags:0
+ timestamp:0
+ windowNumber:[window windowNumber]
+ context:nil
+ eventNumber:eventNumber++
+ clickCount:clickCount
+ pressure:0.0];
+}
+NSEvent* Event(NSView* view, const NSPoint point, const NSEventType type) {
+ return Event(view, point, type, 1);
+}
+
+// Width of the field so that we don't have to ask |field_| for it all
+// the time.
+static const CGFloat kWidth(300.0);
+
+class AutocompleteTextFieldTest : public CocoaTest {
+ public:
+ AutocompleteTextFieldTest() {
+ // Make sure this is wide enough to play games with the cell
+ // decorations.
+ NSRect frame = NSMakeRect(0, 0, kWidth, 30);
+ scoped_nsobject<AutocompleteTextField> field(
+ [[AutocompleteTextField alloc] initWithFrame:frame]);
+ field_ = field.get();
+ [field_ setStringValue:@"Test test"];
+ [[test_window() contentView] addSubview:field_];
+
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ [cell clearDecorations];
+
+ mock_left_decoration_.SetVisible(false);
+ [cell addLeftDecoration:&mock_left_decoration_];
+
+ mock_right_decoration_.SetVisible(false);
+ [cell addRightDecoration:&mock_right_decoration_];
+
+ window_delegate_.reset(
+ [[AutocompleteTextFieldWindowTestDelegate alloc] init]);
+ [test_window() setDelegate:window_delegate_.get()];
+ }
+
+ NSEvent* KeyDownEventWithFlags(NSUInteger flags) {
+ return [NSEvent keyEventWithType:NSKeyDown
+ location:NSZeroPoint
+ modifierFlags:flags
+ timestamp:0.0
+ windowNumber:[test_window() windowNumber]
+ context:nil
+ characters:@"a"
+ charactersIgnoringModifiers:@"a"
+ isARepeat:NO
+ keyCode:'a'];
+ }
+
+ // Helper to return the field-editor frame being used w/in |field_|.
+ NSRect EditorFrame() {
+ EXPECT_TRUE([field_ currentEditor]);
+ EXPECT_EQ([[field_ subviews] count], 1U);
+ if ([[field_ subviews] count] > 0) {
+ return [[[field_ subviews] objectAtIndex:0] frame];
+ } else {
+ // Return something which won't work so the caller can soldier
+ // on.
+ return NSZeroRect;
+ }
+ }
+
+ AutocompleteTextField* field_;
+ MockDecoration mock_left_decoration_;
+ MockDecoration mock_right_decoration_;
+ scoped_nsobject<AutocompleteTextFieldWindowTestDelegate> window_delegate_;
+};
+
+TEST_VIEW(AutocompleteTextFieldTest, field_);
+
+// Base class for testing AutocompleteTextFieldObserver messages.
+class AutocompleteTextFieldObserverTest : public AutocompleteTextFieldTest {
+ public:
+ virtual void SetUp() {
+ AutocompleteTextFieldTest::SetUp();
+ [field_ setObserver:&field_observer_];
+ }
+
+ virtual void TearDown() {
+ // Clear the observer so that we don't show output for
+ // uninteresting messages to the mock (for instance, if |field_| has
+ // focus at the end of the test).
+ [field_ setObserver:NULL];
+
+ AutocompleteTextFieldTest::TearDown();
+ }
+
+ StrictMock<MockAutocompleteTextFieldObserver> field_observer_;
+};
+
+// Test that we have the right cell class.
+TEST_F(AutocompleteTextFieldTest, CellClass) {
+ EXPECT_TRUE([[field_ cell] isKindOfClass:[AutocompleteTextFieldCell class]]);
+}
+
+// Test that becoming first responder sets things up correctly.
+TEST_F(AutocompleteTextFieldTest, FirstResponder) {
+ EXPECT_EQ(nil, [field_ currentEditor]);
+ EXPECT_EQ([[field_ subviews] count], 0U);
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_FALSE(nil == [field_ currentEditor]);
+ EXPECT_EQ([[field_ subviews] count], 1U);
+ EXPECT_TRUE([[field_ currentEditor] isDescendantOf:field_]);
+
+ // Check that the window delegate is providing the right editor.
+ Class c = [AutocompleteTextFieldEditor class];
+ EXPECT_TRUE([[field_ currentEditor] isKindOfClass:c]);
+}
+
+TEST_F(AutocompleteTextFieldTest, AvailableDecorationWidth) {
+ // A fudge factor to account for how much space the border takes up.
+ // The test shouldn't be too dependent on the field's internals, but
+ // it also shouldn't let deranged cases fall through the cracks
+ // (like nothing available with no text, or everything available
+ // with some text).
+ const CGFloat kBorderWidth = 20.0;
+
+ // With no contents, almost the entire width is available for
+ // decorations.
+ [field_ setStringValue:@""];
+ CGFloat availableWidth = [field_ availableDecorationWidth];
+ EXPECT_LE(availableWidth, kWidth);
+ EXPECT_GT(availableWidth, kWidth - kBorderWidth);
+
+ // With minor contents, most of the remaining width is available for
+ // decorations.
+ NSDictionary* attributes =
+ [NSDictionary dictionaryWithObject:[field_ font]
+ forKey:NSFontAttributeName];
+ NSString* string = @"Hello world";
+ const NSSize size([string sizeWithAttributes:attributes]);
+ [field_ setStringValue:string];
+ availableWidth = [field_ availableDecorationWidth];
+ EXPECT_LE(availableWidth, kWidth - size.width);
+ EXPECT_GT(availableWidth, kWidth - size.width - kBorderWidth);
+
+ // With huge contents, nothing at all is left for decorations.
+ string = @"A long string which is surely wider than field_ can hold.";
+ [field_ setStringValue:string];
+ availableWidth = [field_ availableDecorationWidth];
+ EXPECT_LT(availableWidth, 0.0);
+}
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+TEST_F(AutocompleteTextFieldTest, Display) {
+ [field_ display];
+
+ // Test focussed drawing.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ [field_ display];
+ [test_window() clearPretendKeyWindowAndFirstResponder];
+}
+
+TEST_F(AutocompleteTextFieldObserverTest, FlagsChanged) {
+ InSequence dummy; // Call mock in exactly the order specified.
+
+ // Test without Control key down, but some other modifier down.
+ EXPECT_CALL(field_observer_, OnControlKeyChanged(false));
+ [field_ flagsChanged:KeyDownEventWithFlags(NSShiftKeyMask)];
+
+ // Test with Control key down.
+ EXPECT_CALL(field_observer_, OnControlKeyChanged(true));
+ [field_ flagsChanged:KeyDownEventWithFlags(NSControlKeyMask)];
+}
+
+// This test is here rather than in the editor's tests because the
+// field catches -flagsChanged: because it's on the responder chain,
+// the field editor doesn't implement it.
+TEST_F(AutocompleteTextFieldObserverTest, FieldEditorFlagsChanged) {
+ // Many of these methods try to change the selection.
+ EXPECT_CALL(field_observer_, SelectionRangeForProposedRange(A<NSRange>()))
+ .WillRepeatedly(ReturnArg<0>());
+
+ InSequence dummy; // Call mock in exactly the order specified.
+ EXPECT_CALL(field_observer_, OnSetFocus(false));
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ NSResponder* firstResponder = [[field_ window] firstResponder];
+ EXPECT_EQ(firstResponder, [field_ currentEditor]);
+
+ // Test without Control key down, but some other modifier down.
+ EXPECT_CALL(field_observer_, OnControlKeyChanged(false));
+ [firstResponder flagsChanged:KeyDownEventWithFlags(NSShiftKeyMask)];
+
+ // Test with Control key down.
+ EXPECT_CALL(field_observer_, OnControlKeyChanged(true));
+ [firstResponder flagsChanged:KeyDownEventWithFlags(NSControlKeyMask)];
+}
+
+// Frame size changes are propagated to |observer_|.
+TEST_F(AutocompleteTextFieldObserverTest, FrameChanged) {
+ EXPECT_CALL(field_observer_, OnFrameChanged());
+ NSRect frame = [field_ frame];
+ frame.size.width += 10.0;
+ [field_ setFrame:frame];
+}
+
+// Test that the field editor gets the same bounds when focus is
+// delivered by the standard focusing machinery, or by
+// -resetFieldEditorFrameIfNeeded.
+TEST_F(AutocompleteTextFieldTest, ResetFieldEditorBase) {
+ // Capture the editor frame resulting from the standard focus
+ // machinery.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ const NSRect baseEditorFrame = EditorFrame();
+
+ // A decoration should result in a strictly smaller editor frame.
+ mock_left_decoration_.SetVisible(true);
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_FALSE(NSEqualRects(baseEditorFrame, EditorFrame()));
+ EXPECT_TRUE(NSContainsRect(baseEditorFrame, EditorFrame()));
+
+ // Removing the decoration and using -resetFieldEditorFrameIfNeeded
+ // should result in the same frame as the standard focus machinery.
+ mock_left_decoration_.SetVisible(false);
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_TRUE(NSEqualRects(baseEditorFrame, EditorFrame()));
+}
+
+// Test that the field editor gets the same bounds when focus is
+// delivered by the standard focusing machinery, or by
+// -resetFieldEditorFrameIfNeeded, this time with a decoration
+// pre-loaded.
+TEST_F(AutocompleteTextFieldTest, ResetFieldEditorWithDecoration) {
+ AutocompleteTextFieldCell* cell = [field_ cell];
+
+ // Make sure decoration isn't already visible, then make it visible.
+ EXPECT_TRUE(NSIsEmptyRect([cell frameForDecoration:&mock_left_decoration_
+ inFrame:[field_ bounds]]));
+ mock_left_decoration_.SetVisible(true);
+ EXPECT_FALSE(NSIsEmptyRect([cell frameForDecoration:&mock_left_decoration_
+ inFrame:[field_ bounds]]));
+
+ // Capture the editor frame resulting from the standard focus
+ // machinery.
+
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ const NSRect baseEditorFrame = EditorFrame();
+
+ // When the decoration is not visible the frame should be strictly larger.
+ mock_left_decoration_.SetVisible(false);
+ EXPECT_TRUE(NSIsEmptyRect([cell frameForDecoration:&mock_left_decoration_
+ inFrame:[field_ bounds]]));
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_FALSE(NSEqualRects(baseEditorFrame, EditorFrame()));
+ EXPECT_TRUE(NSContainsRect(EditorFrame(), baseEditorFrame));
+
+ // When the decoration is visible, -resetFieldEditorFrameIfNeeded
+ // should result in the same frame as the standard focus machinery.
+ mock_left_decoration_.SetVisible(true);
+ EXPECT_FALSE(NSIsEmptyRect([cell frameForDecoration:&mock_left_decoration_
+ inFrame:[field_ bounds]]));
+
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_TRUE(NSEqualRects(baseEditorFrame, EditorFrame()));
+}
+
+// Test that resetting the field editor bounds does not cause untoward
+// messages to the field's observer.
+TEST_F(AutocompleteTextFieldObserverTest, ResetFieldEditorContinuesEditing) {
+ // Many of these methods try to change the selection.
+ EXPECT_CALL(field_observer_, SelectionRangeForProposedRange(A<NSRange>()))
+ .WillRepeatedly(ReturnArg<0>());
+
+ EXPECT_CALL(field_observer_, OnSetFocus(false));
+ // Becoming first responder doesn't begin editing.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ const NSRect baseEditorFrame = EditorFrame();
+ NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]);
+ EXPECT_TRUE(nil != editor);
+
+ // This should begin editing and indicate a change.
+ EXPECT_CALL(field_observer_, OnDidBeginEditing());
+ EXPECT_CALL(field_observer_, OnDidChange());
+ [editor shouldChangeTextInRange:NSMakeRange(0, 0) replacementString:@""];
+ [editor didChangeText];
+
+ // No messages to |field_observer_| when the frame actually changes.
+ mock_left_decoration_.SetVisible(true);
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_FALSE(NSEqualRects(baseEditorFrame, EditorFrame()));
+}
+
+// Clicking in a right-hand decoration which does not handle the mouse
+// puts the caret rightmost.
+TEST_F(AutocompleteTextFieldTest, ClickRightDecorationPutsCaretRightmost) {
+ // Decoration does not handle the mouse event, so the cell should
+ // process it. Called at least once.
+ EXPECT_CALL(mock_right_decoration_, AcceptsMousePress())
+ .WillOnce(Return(false))
+ .WillRepeatedly(Return(false));
+
+ // Set the decoration before becoming responder.
+ EXPECT_FALSE([field_ currentEditor]);
+ mock_right_decoration_.SetVisible(true);
+
+ // Make first responder should select all.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_TRUE([field_ currentEditor]);
+ const NSRange allRange = NSMakeRange(0, [[field_ stringValue] length]);
+ EXPECT_TRUE(NSEqualRanges(allRange, [[field_ currentEditor] selectedRange]));
+
+ // Generate a click on the decoration.
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect bounds = [field_ bounds];
+ const NSRect iconFrame =
+ [cell frameForDecoration:&mock_right_decoration_ inFrame:bounds];
+ const NSPoint point = NSMakePoint(NSMidX(iconFrame), NSMidY(iconFrame));
+ NSEvent* downEvent = Event(field_, point, NSLeftMouseDown);
+ NSEvent* upEvent = Event(field_, point, NSLeftMouseUp);
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+
+ // Selection should be a right-hand-side caret.
+ EXPECT_TRUE(NSEqualRanges(NSMakeRange([[field_ stringValue] length], 0),
+ [[field_ currentEditor] selectedRange]));
+}
+
+// Clicking in a left-side decoration which doesn't handle the event
+// puts the selection in the leftmost position.
+TEST_F(AutocompleteTextFieldTest, ClickLeftDecorationPutsCaretLeftmost) {
+ // Decoration does not handle the mouse event, so the cell should
+ // process it. Called at least once.
+ EXPECT_CALL(mock_left_decoration_, AcceptsMousePress())
+ .WillOnce(Return(false))
+ .WillRepeatedly(Return(false));
+
+ // Set the decoration before becoming responder.
+ EXPECT_FALSE([field_ currentEditor]);
+ mock_left_decoration_.SetVisible(true);
+
+ // Make first responder should select all.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_TRUE([field_ currentEditor]);
+ const NSRange allRange = NSMakeRange(0, [[field_ stringValue] length]);
+ EXPECT_TRUE(NSEqualRanges(allRange, [[field_ currentEditor] selectedRange]));
+
+ // Generate a click on the decoration.
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect bounds = [field_ bounds];
+ const NSRect iconFrame =
+ [cell frameForDecoration:&mock_left_decoration_ inFrame:bounds];
+ const NSPoint point = NSMakePoint(NSMidX(iconFrame), NSMidY(iconFrame));
+ NSEvent* downEvent = Event(field_, point, NSLeftMouseDown);
+ NSEvent* upEvent = Event(field_, point, NSLeftMouseUp);
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+
+ // Selection should be a left-hand-side caret.
+ EXPECT_TRUE(NSEqualRanges(NSMakeRange(0, 0),
+ [[field_ currentEditor] selectedRange]));
+}
+
+// Clicks not in the text area or the cell's decorations fall through
+// to the editor.
+TEST_F(AutocompleteTextFieldTest, ClickBorderSelectsAll) {
+ // Can't rely on the window machinery to make us first responder,
+ // here.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_TRUE([field_ currentEditor]);
+
+ const NSPoint point(NSMakePoint(20.0, 1.0));
+ NSEvent* downEvent(Event(field_, point, NSLeftMouseDown));
+ NSEvent* upEvent(Event(field_, point, NSLeftMouseUp));
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+
+ // Clicking in the narrow border area around a Cocoa NSTextField
+ // does a select-all. Regardless of whether this is a good call, it
+ // works as a test that things get passed down to the editor.
+ const NSRange selectedRange([[field_ currentEditor] selectedRange]);
+ EXPECT_EQ(selectedRange.location, 0U);
+ EXPECT_EQ(selectedRange.length, [[field_ stringValue] length]);
+}
+
+// Single-click with no drag should setup a field editor and
+// select all.
+TEST_F(AutocompleteTextFieldTest, ClickSelectsAll) {
+ EXPECT_FALSE([field_ currentEditor]);
+
+ const NSPoint point = NSMakePoint(20.0, NSMidY([field_ bounds]));
+ NSEvent* downEvent(Event(field_, point, NSLeftMouseDown));
+ NSEvent* upEvent(Event(field_, point, NSLeftMouseUp));
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+ EXPECT_TRUE([field_ currentEditor]);
+ const NSRange selectedRange([[field_ currentEditor] selectedRange]);
+ EXPECT_EQ(selectedRange.location, 0U);
+ EXPECT_EQ(selectedRange.length, [[field_ stringValue] length]);
+}
+
+// Click-drag selects text, not select all.
+TEST_F(AutocompleteTextFieldTest, ClickDragSelectsText) {
+ EXPECT_FALSE([field_ currentEditor]);
+
+ NSEvent* downEvent(Event(field_, NSMakePoint(20.0, 5.0), NSLeftMouseDown));
+ NSEvent* upEvent(Event(field_, NSMakePoint(0.0, 5.0), NSLeftMouseUp));
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+ EXPECT_TRUE([field_ currentEditor]);
+
+ // Expect this to have selected a prefix of the content. Mostly
+ // just don't want the select-all behavior.
+ const NSRange selectedRange([[field_ currentEditor] selectedRange]);
+ EXPECT_EQ(selectedRange.location, 0U);
+ EXPECT_LT(selectedRange.length, [[field_ stringValue] length]);
+}
+
+// TODO(shess): Test that click/pause/click allows cursor placement.
+// In this case the first click goes to the field, but the second
+// click goes to the field editor, so the current testing pattern
+// can't work. What really needs to happen is to push through the
+// NSWindow event machinery so that we can say "two independent clicks
+// at the same location have the right effect". Once that is done, it
+// might make sense to revise the other tests to use the same
+// machinery.
+
+// Double-click selects word, not select all.
+TEST_F(AutocompleteTextFieldTest, DoubleClickSelectsWord) {
+ EXPECT_FALSE([field_ currentEditor]);
+
+ const NSPoint point = NSMakePoint(20.0, NSMidY([field_ bounds]));
+ NSEvent* downEvent(Event(field_, point, NSLeftMouseDown, 1));
+ NSEvent* upEvent(Event(field_, point, NSLeftMouseUp, 1));
+ NSEvent* downEvent2(Event(field_, point, NSLeftMouseDown, 2));
+ NSEvent* upEvent2(Event(field_, point, NSLeftMouseUp, 2));
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+ [NSApp postEvent:upEvent2 atStart:YES];
+ [field_ mouseDown:downEvent2];
+ EXPECT_TRUE([field_ currentEditor]);
+
+ // Selected the first word.
+ const NSRange selectedRange([[field_ currentEditor] selectedRange]);
+ const NSRange spaceRange([[field_ stringValue] rangeOfString:@" "]);
+ EXPECT_GT(spaceRange.location, 0U);
+ EXPECT_LT(spaceRange.length, [[field_ stringValue] length]);
+ EXPECT_EQ(selectedRange.location, 0U);
+ EXPECT_EQ(selectedRange.length, spaceRange.location);
+}
+
+TEST_F(AutocompleteTextFieldTest, TripleClickSelectsAll) {
+ EXPECT_FALSE([field_ currentEditor]);
+
+ const NSPoint point(NSMakePoint(20.0, 5.0));
+ NSEvent* downEvent(Event(field_, point, NSLeftMouseDown, 1));
+ NSEvent* upEvent(Event(field_, point, NSLeftMouseUp, 1));
+ NSEvent* downEvent2(Event(field_, point, NSLeftMouseDown, 2));
+ NSEvent* upEvent2(Event(field_, point, NSLeftMouseUp, 2));
+ NSEvent* downEvent3(Event(field_, point, NSLeftMouseDown, 3));
+ NSEvent* upEvent3(Event(field_, point, NSLeftMouseUp, 3));
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+ [NSApp postEvent:upEvent2 atStart:YES];
+ [field_ mouseDown:downEvent2];
+ [NSApp postEvent:upEvent3 atStart:YES];
+ [field_ mouseDown:downEvent3];
+ EXPECT_TRUE([field_ currentEditor]);
+
+ // Selected the first word.
+ const NSRange selectedRange([[field_ currentEditor] selectedRange]);
+ EXPECT_EQ(selectedRange.location, 0U);
+ EXPECT_EQ(selectedRange.length, [[field_ stringValue] length]);
+}
+
+// Clicking a decoration should call decoration's OnMousePressed.
+TEST_F(AutocompleteTextFieldTest, LeftDecorationMouseDown) {
+ // At this point, not focussed.
+ EXPECT_FALSE([field_ currentEditor]);
+
+ mock_left_decoration_.SetVisible(true);
+ EXPECT_CALL(mock_left_decoration_, AcceptsMousePress())
+ .WillRepeatedly(Return(true));
+
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect iconFrame =
+ [cell frameForDecoration:&mock_left_decoration_ inFrame:[field_ bounds]];
+ const NSPoint location = NSMakePoint(NSMidX(iconFrame), NSMidY(iconFrame));
+ NSEvent* downEvent = Event(field_, location, NSLeftMouseDown, 1);
+ NSEvent* upEvent = Event(field_, location, NSLeftMouseUp, 1);
+
+ // Since decorations can be dragged, the mouse-press is sent on
+ // mouse-up.
+ [NSApp postEvent:upEvent atStart:YES];
+
+ EXPECT_CALL(mock_left_decoration_, OnMousePressed(_))
+ .WillOnce(Return(true));
+ [field_ mouseDown:downEvent];
+
+ // Focus the field and test that handled clicks don't affect selection.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_TRUE([field_ currentEditor]);
+ const NSRange allRange = NSMakeRange(0, [[field_ stringValue] length]);
+ EXPECT_TRUE(NSEqualRanges(allRange, [[field_ currentEditor] selectedRange]));
+
+ // Generate another click on the decoration.
+ downEvent = Event(field_, location, NSLeftMouseDown, 1);
+ upEvent = Event(field_, location, NSLeftMouseUp, 1);
+ [NSApp postEvent:upEvent atStart:YES];
+ EXPECT_CALL(mock_left_decoration_, OnMousePressed(_))
+ .WillOnce(Return(true));
+ [field_ mouseDown:downEvent];
+
+ // The selection should not have changed.
+ EXPECT_TRUE(NSEqualRanges(allRange, [[field_ currentEditor] selectedRange]));
+
+ // TODO(shess): Test that mouse drags are initiated if the next
+ // event is a drag, or if the mouse-up takes too long to arrive.
+ // IDEA: mock decoration to return a pasteboard which a mock
+ // AutocompleteTextField notes in -dragImage:*.
+}
+
+// Clicking a decoration should call decoration's OnMousePressed.
+TEST_F(AutocompleteTextFieldTest, RightDecorationMouseDown) {
+ // At this point, not focussed.
+ EXPECT_FALSE([field_ currentEditor]);
+
+ mock_right_decoration_.SetVisible(true);
+ EXPECT_CALL(mock_right_decoration_, AcceptsMousePress())
+ .WillRepeatedly(Return(true));
+
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect bounds = [field_ bounds];
+ const NSRect iconFrame =
+ [cell frameForDecoration:&mock_right_decoration_ inFrame:bounds];
+ const NSPoint location = NSMakePoint(NSMidX(iconFrame), NSMidY(iconFrame));
+ NSEvent* downEvent = Event(field_, location, NSLeftMouseDown, 1);
+ NSEvent* upEvent = Event(field_, location, NSLeftMouseUp, 1);
+
+ // Since decorations can be dragged, the mouse-press is sent on
+ // mouse-up.
+ [NSApp postEvent:upEvent atStart:YES];
+
+ EXPECT_CALL(mock_right_decoration_, OnMousePressed(_))
+ .WillOnce(Return(true));
+ [field_ mouseDown:downEvent];
+}
+
+// Test that page action menus are properly returned.
+// TODO(shess): Really, this should test that things are forwarded to
+// the cell, and the cell tests should test that the right things are
+// selected. It's easier to mock the event here, though. This code's
+// event-mockers might be worth promoting to |test_event_utils.h| or
+// |cocoa_test_helper.h|.
+TEST_F(AutocompleteTextFieldTest, DecorationMenu) {
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect bounds([field_ bounds]);
+
+ const CGFloat edge = NSHeight(bounds) - 4.0;
+ const NSSize size = NSMakeSize(edge, edge);
+ scoped_nsobject<NSImage> image([[NSImage alloc] initWithSize:size]);
+
+ scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@"Menu"]);
+
+ mock_left_decoration_.SetVisible(true);
+ mock_right_decoration_.SetVisible(true);
+
+ // The item with a menu returns it.
+ NSRect actionFrame = [cell frameForDecoration:&mock_right_decoration_
+ inFrame:bounds];
+ NSPoint location = NSMakePoint(NSMidX(actionFrame), NSMidY(actionFrame));
+ NSEvent* event = Event(field_, location, NSRightMouseDown, 1);
+
+ // Check that the decoration is called, and the field returns the
+ // menu.
+ EXPECT_CALL(mock_right_decoration_, GetMenu())
+ .WillOnce(Return(menu.get()));
+ NSMenu *decorationMenu = [field_ decorationMenuForEvent:event];
+ EXPECT_EQ(decorationMenu, menu);
+
+ // The item without a menu returns nil.
+ EXPECT_CALL(mock_left_decoration_, GetMenu())
+ .WillOnce(Return(static_cast<NSMenu*>(nil)));
+ actionFrame = [cell frameForDecoration:&mock_left_decoration_
+ inFrame:bounds];
+ location = NSMakePoint(NSMidX(actionFrame), NSMidY(actionFrame));
+ event = Event(field_, location, NSRightMouseDown, 1);
+ EXPECT_FALSE([field_ decorationMenuForEvent:event]);
+
+ // Something not in an action returns nil.
+ location = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
+ event = Event(field_, location, NSRightMouseDown, 1);
+ EXPECT_FALSE([field_ decorationMenuForEvent:event]);
+}
+
+// Verify that -setAttributedStringValue: works as expected when
+// focussed or when not focussed. Our code mostly depends on about
+// whether -stringValue works right.
+TEST_F(AutocompleteTextFieldTest, SetAttributedStringBaseline) {
+ EXPECT_EQ(nil, [field_ currentEditor]);
+
+ // So that we can set rich text.
+ [field_ setAllowsEditingTextAttributes:YES];
+
+ // Set an attribute different from the field's default so we can
+ // tell we got the same string out as we put in.
+ NSFont* font = [NSFont fontWithDescriptor:[[field_ font] fontDescriptor]
+ size:[[field_ font] pointSize] + 2];
+ NSDictionary* attributes =
+ [NSDictionary dictionaryWithObject:font
+ forKey:NSFontAttributeName];
+ NSString* const kString = @"This is a test";
+ scoped_nsobject<NSAttributedString> attributedString(
+ [[NSAttributedString alloc] initWithString:kString
+ attributes:attributes]);
+
+ // Check that what we get back looks like what we put in.
+ EXPECT_NSNE(kString, [field_ stringValue]);
+ [field_ setAttributedStringValue:attributedString];
+ EXPECT_TRUE([[field_ attributedStringValue]
+ isEqualToAttributedString:attributedString]);
+ EXPECT_NSEQ(kString, [field_ stringValue]);
+
+ // Try that again with focus.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+
+ EXPECT_TRUE([field_ currentEditor]);
+
+ // Check that what we get back looks like what we put in.
+ [field_ setStringValue:@""];
+ EXPECT_NSNE(kString, [field_ stringValue]);
+ [field_ setAttributedStringValue:attributedString];
+ EXPECT_TRUE([[field_ attributedStringValue]
+ isEqualToAttributedString:attributedString]);
+ EXPECT_NSEQ(kString, [field_ stringValue]);
+}
+
+// -setAttributedStringValue: shouldn't reset the undo state if things
+// are being editted.
+TEST_F(AutocompleteTextFieldTest, SetAttributedStringUndo) {
+ NSColor* redColor = [NSColor redColor];
+ NSDictionary* attributes =
+ [NSDictionary dictionaryWithObject:redColor
+ forKey:NSForegroundColorAttributeName];
+ NSString* const kString = @"This is a test";
+ scoped_nsobject<NSAttributedString> attributedString(
+ [[NSAttributedString alloc] initWithString:kString
+ attributes:attributes]);
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_TRUE([field_ currentEditor]);
+ NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]);
+ NSUndoManager* undoManager = [editor undoManager];
+ EXPECT_TRUE(undoManager);
+
+ // Nothing to undo, yet.
+ EXPECT_FALSE([undoManager canUndo]);
+
+ // Starting an editing action creates an undoable item.
+ [editor shouldChangeTextInRange:NSMakeRange(0, 0) replacementString:@""];
+ [editor didChangeText];
+ EXPECT_TRUE([undoManager canUndo]);
+
+ // -setStringValue: resets the editor's undo chain.
+ [field_ setStringValue:kString];
+ EXPECT_FALSE([undoManager canUndo]);
+
+ // Verify that -setAttributedStringValue: does not reset the
+ // editor's undo chain.
+ [field_ setStringValue:@""];
+ [editor shouldChangeTextInRange:NSMakeRange(0, 0) replacementString:@""];
+ [editor didChangeText];
+ EXPECT_TRUE([undoManager canUndo]);
+ [field_ setAttributedStringValue:attributedString];
+ EXPECT_TRUE([undoManager canUndo]);
+
+ // Verify that calling -clearUndoChain clears the undo chain.
+ [field_ clearUndoChain];
+ EXPECT_FALSE([undoManager canUndo]);
+}
+
+TEST_F(AutocompleteTextFieldTest, EditorGetsCorrectUndoManager) {
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+
+ NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]);
+ EXPECT_TRUE(editor);
+ EXPECT_EQ([field_ undoManagerForTextView:editor], [editor undoManager]);
+}
+
+TEST_F(AutocompleteTextFieldObserverTest, SendsEditingMessages) {
+ // Many of these methods try to change the selection.
+ EXPECT_CALL(field_observer_, SelectionRangeForProposedRange(A<NSRange>()))
+ .WillRepeatedly(ReturnArg<0>());
+
+ EXPECT_CALL(field_observer_, OnSetFocus(false));
+ // Becoming first responder doesn't begin editing.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]);
+ EXPECT_TRUE(nil != editor);
+
+ // This should begin editing and indicate a change.
+ EXPECT_CALL(field_observer_, OnDidBeginEditing());
+ EXPECT_CALL(field_observer_, OnDidChange());
+ [editor shouldChangeTextInRange:NSMakeRange(0, 0) replacementString:@""];
+ [editor didChangeText];
+
+ // Further changes don't send the begin message.
+ EXPECT_CALL(field_observer_, OnDidChange());
+ [editor shouldChangeTextInRange:NSMakeRange(0, 0) replacementString:@""];
+ [editor didChangeText];
+
+ // -doCommandBySelector: should forward to observer via |field_|.
+ // TODO(shess): Test with a fake arrow-key event?
+ const SEL cmd = @selector(moveDown:);
+ EXPECT_CALL(field_observer_, OnDoCommandBySelector(cmd))
+ .WillOnce(Return(true));
+ [editor doCommandBySelector:cmd];
+
+ // Finished with the changes.
+ EXPECT_CALL(field_observer_, OnKillFocus());
+ EXPECT_CALL(field_observer_, OnDidEndEditing());
+ [test_window() clearPretendKeyWindowAndFirstResponder];
+}
+
+// Test that the resign-key notification is forwarded right, and that
+// the notification is registered and unregistered when the view moves
+// in and out of the window.
+// TODO(shess): Should this test the key window for realz? That would
+// be really annoying to whoever is running the tests.
+TEST_F(AutocompleteTextFieldObserverTest, ClosePopupOnResignKey) {
+ EXPECT_CALL(field_observer_, ClosePopup());
+ [test_window() resignKeyWindow];
+
+ scoped_nsobject<AutocompleteTextField> pin([field_ retain]);
+ [field_ removeFromSuperview];
+ [test_window() resignKeyWindow];
+
+ [[test_window() contentView] addSubview:field_];
+ EXPECT_CALL(field_observer_, ClosePopup());
+ [test_window() resignKeyWindow];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h
new file mode 100644
index 0000000..bccaae1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h
@@ -0,0 +1,58 @@
+// 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_AUTOCOMPLETE_TEXT_FIELD_UNITTEST_HELPER_H_
+#define CHROME_BROWSER_UI_COCOA_AUTOCOMPLETE_TEXT_FIELD_UNITTEST_HELPER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+@class AutocompleteTextFieldEditor;
+
+// Return the right field editor for AutocompleteTextField instance.
+
+@interface AutocompleteTextFieldWindowTestDelegate :
+ NSObject<NSWindowDelegate> {
+ scoped_nsobject<AutocompleteTextFieldEditor> editor_;
+}
+- (id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)anObject;
+@end
+
+namespace {
+
+// Allow monitoring calls into AutocompleteTextField's observer.
+// Being in a .h file with an anonymous namespace is strange, but this
+// is here so the mock interface doesn't have to change in multiple
+// places.
+
+// Any method you add here needs a unit test. You knew that.
+
+class MockAutocompleteTextFieldObserver : public AutocompleteTextFieldObserver {
+ public:
+ MOCK_METHOD1(SelectionRangeForProposedRange, NSRange(NSRange range));
+ MOCK_METHOD1(OnControlKeyChanged, void(bool pressed));
+ MOCK_METHOD0(CanCopy, bool());
+ MOCK_METHOD1(CopyToPasteboard, void(NSPasteboard* pboard));
+ MOCK_METHOD0(OnPaste, void());
+ MOCK_METHOD0(CanPasteAndGo, bool());
+ MOCK_METHOD0(GetPasteActionStringId, int());
+ MOCK_METHOD0(OnPasteAndGo, void());
+ MOCK_METHOD0(OnFrameChanged, void());
+ MOCK_METHOD0(ClosePopup, void());
+ MOCK_METHOD0(OnDidBeginEditing, void());
+ MOCK_METHOD0(OnDidChange, void());
+ MOCK_METHOD0(OnDidEndEditing, void());
+ MOCK_METHOD1(OnDoCommandBySelector, bool(SEL cmd));
+ MOCK_METHOD1(OnSetFocus, void(bool control_down));
+ MOCK_METHOD0(OnKillFocus, void());
+};
+
+} // namespace
+
+#endif // CHROME_BROWSER_UI_COCOA_AUTOCOMPLETE_TEXT_FIELD_UNITTEST_HELPER_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.mm
new file mode 100644
index 0000000..a2c5194
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.mm
@@ -0,0 +1,29 @@
+// 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/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h"
+
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+@implementation AutocompleteTextFieldWindowTestDelegate
+
+- (id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)anObject {
+ id editor = nil;
+ if ([anObject isKindOfClass:[AutocompleteTextField class]]) {
+ if (editor_ == nil) {
+ editor_.reset([[AutocompleteTextFieldEditor alloc] init]);
+ }
+ EXPECT_TRUE(editor_ != nil);
+
+ // This needs to be called every time, otherwise notifications
+ // aren't sent correctly.
+ [editor_ setFieldEditor:YES];
+ editor = editor_.get();
+ }
+ return editor;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/bubble_decoration.h b/chrome/browser/ui/cocoa/location_bar/bubble_decoration.h
new file mode 100644
index 0000000..234c254
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/bubble_decoration.h
@@ -0,0 +1,67 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_BUBBLE_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_BUBBLE_DECORATION_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/gtest_prod_util.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
+
+// Draws an outlined rounded rect, with an optional image to the left
+// and an optional text label to the right.
+
+class BubbleDecoration : public LocationBarDecoration {
+ public:
+ // |font| will be used when drawing the label, and cannot be |nil|.
+ BubbleDecoration(NSFont* font);
+ ~BubbleDecoration();
+
+ // Setup the drawing parameters.
+ NSImage* GetImage();
+ void SetImage(NSImage* image);
+ void SetLabel(NSString* label);
+ void SetColors(NSColor* border_color,
+ NSColor* background_color,
+ NSColor* text_color);
+
+ // Implement |LocationBarDecoration|.
+ virtual void DrawInFrame(NSRect frame, NSView* control_view);
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+
+ protected:
+ // Helper returning bubble width for the given |image| and |label|
+ // assuming |font_| (for sizing text). Arguments can be nil.
+ CGFloat GetWidthForImageAndLabel(NSImage* image, NSString* label);
+
+ // Helper to return where the image is drawn, for subclasses to drag
+ // from. |frame| is the decoration's frame in the containing cell.
+ NSRect GetImageRectInFrame(NSRect frame);
+
+ private:
+ friend class SelectedKeywordDecorationTest;
+ FRIEND_TEST_ALL_PREFIXES(SelectedKeywordDecorationTest,
+ UsesPartialKeywordIfNarrow);
+
+ // Contains font and color attribute for drawing |label_|.
+ scoped_nsobject<NSDictionary> attributes_;
+
+ // Image drawn in the left side of the bubble.
+ scoped_nsobject<NSImage> image_;
+
+ // Label to draw to right of image. Can be |nil|.
+ scoped_nsobject<NSString> label_;
+
+ // Colors used to draw the bubble, should be set by the subclass
+ // constructor.
+ scoped_nsobject<NSColor> background_color_;
+ scoped_nsobject<NSColor> border_color_;
+
+ DISALLOW_COPY_AND_ASSIGN(BubbleDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_BUBBLE_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/bubble_decoration.mm b/chrome/browser/ui/cocoa/location_bar/bubble_decoration.mm
new file mode 100644
index 0000000..b568639
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/bubble_decoration.mm
@@ -0,0 +1,158 @@
+// Copyright (c) 2010 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.
+
+#include <cmath>
+
+#import "chrome/browser/ui/cocoa/location_bar/bubble_decoration.h"
+
+#include "base/logging.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+
+namespace {
+
+// Padding between the icon/label and bubble edges.
+const CGFloat kBubblePadding = 3.0;
+
+// The image needs to be in the same position as for the location
+// icon, which implies that the bubble's padding in the Omnibox needs
+// to differ from the location icon's. Indeed, that's how the views
+// implementation handles the problem. This draws the bubble edge a
+// little bit further left, which is easier but no less hacky.
+const CGFloat kLeftSideOverdraw = 2.0;
+
+// Omnibox corner radius is |4.0|, this needs to look tight WRT that.
+const CGFloat kBubbleCornerRadius = 2.0;
+
+// How far to inset the bubble from the top and bottom of the drawing
+// frame.
+// TODO(shess): Would be nicer to have the drawing code factor out the
+// space outside the border, and perhaps the border. Then this could
+// reflect the single pixel space w/in that.
+const CGFloat kBubbleYInset = 4.0;
+
+} // namespace
+
+BubbleDecoration::BubbleDecoration(NSFont* font) {
+ DCHECK(font);
+ if (font) {
+ NSDictionary* attributes =
+ [NSDictionary dictionaryWithObject:font
+ forKey:NSFontAttributeName];
+ attributes_.reset([attributes retain]);
+ }
+}
+
+BubbleDecoration::~BubbleDecoration() {
+}
+
+CGFloat BubbleDecoration::GetWidthForImageAndLabel(NSImage* image,
+ NSString* label) {
+ if (!image && !label)
+ return kOmittedWidth;
+
+ const CGFloat image_width = image ? [image size].width : 0.0;
+ if (!label)
+ return kBubblePadding + image_width;
+
+ // The bubble needs to take up an integral number of pixels.
+ // Generally -sizeWithAttributes: seems to overestimate rather than
+ // underestimate, so floor() seems to work better.
+ const CGFloat label_width =
+ std::floor([label sizeWithAttributes:attributes_].width);
+ return kBubblePadding + image_width + label_width;
+}
+
+NSRect BubbleDecoration::GetImageRectInFrame(NSRect frame) {
+ NSRect imageRect = NSInsetRect(frame, 0.0, kBubbleYInset);
+ if (image_) {
+ // Center the image vertically.
+ const NSSize imageSize = [image_ size];
+ imageRect.origin.y +=
+ std::floor((NSHeight(frame) - imageSize.height) / 2.0);
+ imageRect.size = imageSize;
+ }
+ return imageRect;
+}
+
+CGFloat BubbleDecoration::GetWidthForSpace(CGFloat width) {
+ const CGFloat all_width = GetWidthForImageAndLabel(image_, label_);
+ if (all_width <= width)
+ return all_width;
+
+ const CGFloat image_width = GetWidthForImageAndLabel(image_, nil);
+ if (image_width <= width)
+ return image_width;
+
+ return kOmittedWidth;
+}
+
+void BubbleDecoration::DrawInFrame(NSRect frame, NSView* control_view) {
+ const NSRect decorationFrame = NSInsetRect(frame, 0.0, kBubbleYInset);
+
+ // The inset is to put the border down the middle of the pixel.
+ NSRect bubbleFrame = NSInsetRect(decorationFrame, 0.5, 0.5);
+ bubbleFrame.origin.x -= kLeftSideOverdraw;
+ bubbleFrame.size.width += kLeftSideOverdraw;
+ NSBezierPath* path =
+ [NSBezierPath bezierPathWithRoundedRect:bubbleFrame
+ xRadius:kBubbleCornerRadius
+ yRadius:kBubbleCornerRadius];
+
+ [background_color_ setFill];
+ [path fill];
+
+ [border_color_ setStroke];
+ [path setLineWidth:1.0];
+ [path stroke];
+
+ NSRect imageRect = decorationFrame;
+ if (image_) {
+ // Center the image vertically.
+ const NSSize imageSize = [image_ size];
+ imageRect.origin.y +=
+ std::floor((NSHeight(decorationFrame) - imageSize.height) / 2.0);
+ imageRect.size = imageSize;
+ [image_ drawInRect:imageRect
+ fromRect:NSZeroRect // Entire image
+ operation:NSCompositeSourceOver
+ fraction:1.0
+ neverFlipped:YES];
+ } else {
+ imageRect.size = NSZeroSize;
+ }
+
+ if (label_) {
+ NSRect textRect = decorationFrame;
+ textRect.origin.x = NSMaxX(imageRect);
+ textRect.size.width = NSMaxX(decorationFrame) - NSMinX(textRect);
+ [label_ drawInRect:textRect withAttributes:attributes_];
+ }
+}
+
+NSImage* BubbleDecoration::GetImage() {
+ return image_;
+}
+
+void BubbleDecoration::SetImage(NSImage* image) {
+ image_.reset([image retain]);
+}
+
+void BubbleDecoration::SetLabel(NSString* label) {
+ // If the initializer was called with |nil|, then the code cannot
+ // process a label.
+ DCHECK(attributes_);
+ if (attributes_)
+ label_.reset([label copy]);
+}
+
+void BubbleDecoration::SetColors(NSColor* border_color,
+ NSColor* background_color,
+ NSColor* text_color) {
+ border_color_.reset([border_color retain]);
+ background_color_.reset([background_color retain]);
+
+ scoped_nsobject<NSMutableDictionary> attributes([attributes_ mutableCopy]);
+ [attributes setObject:text_color forKey:NSForegroundColorAttributeName];
+ attributes_.reset([attributes copy]);
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/content_setting_decoration.h b/chrome/browser/ui/cocoa/location_bar/content_setting_decoration.h
new file mode 100644
index 0000000..07b1510
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/content_setting_decoration.h
@@ -0,0 +1,55 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_CONTENT_SETTING_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_CONTENT_SETTING_DECORATION_H_
+#pragma once
+
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/location_bar/image_decoration.h"
+#include "chrome/common/content_settings_types.h"
+
+// ContentSettingDecoration is used to display the content settings
+// images on the current page.
+
+class ContentSettingImageModel;
+class LocationBarViewMac;
+class Profile;
+class TabContents;
+
+class ContentSettingDecoration : public ImageDecoration {
+ public:
+ ContentSettingDecoration(ContentSettingsType settings_type,
+ LocationBarViewMac* owner,
+ Profile* profile);
+ virtual ~ContentSettingDecoration();
+
+ // Updates the image and visibility state based on the supplied TabContents.
+ // Returns true if the decoration's visible state changed.
+ bool UpdateFromTabContents(TabContents* tab_contents);
+
+ // Overridden from |LocationBarDecoration|
+ virtual bool AcceptsMousePress() { return true; }
+ virtual bool OnMousePressed(NSRect frame);
+ virtual NSString* GetToolTip();
+
+ private:
+ // Helper to get where the bubble point should land. Similar to
+ // |PageActionDecoration| or |StarDecoration| (|LocationBarViewMac|
+ // calls those).
+ NSPoint GetBubblePointInFrame(NSRect frame);
+
+ void SetToolTip(NSString* tooltip);
+
+ scoped_ptr<ContentSettingImageModel> content_setting_image_model_;
+
+ LocationBarViewMac* owner_; // weak
+ Profile* profile_; // weak
+
+ scoped_nsobject<NSString> tooltip_;
+
+ DISALLOW_COPY_AND_ASSIGN(ContentSettingDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_CONTENT_SETTING_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/content_setting_decoration.mm b/chrome/browser/ui/cocoa/location_bar/content_setting_decoration.mm
new file mode 100644
index 0000000..c803d13
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/content_setting_decoration.mm
@@ -0,0 +1,109 @@
+// Copyright (c) 2010 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/ui/cocoa/location_bar/content_setting_decoration.h"
+
+#include "app/resource_bundle.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/content_setting_image_model.h"
+#include "chrome/browser/content_setting_bubble_model.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/content_setting_bubble_cocoa.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+#include "chrome/common/pref_names.h"
+#include "net/base/net_util.h"
+
+namespace {
+
+// How far to offset up from the bottom of the view to get the top
+// border of the popup 2px below the bottom of the Omnibox.
+const CGFloat kPopupPointYOffset = 2.0;
+
+} // namespace
+
+ContentSettingDecoration::ContentSettingDecoration(
+ ContentSettingsType settings_type,
+ LocationBarViewMac* owner,
+ Profile* profile)
+ : content_setting_image_model_(
+ ContentSettingImageModel::CreateContentSettingImageModel(
+ settings_type)),
+ owner_(owner),
+ profile_(profile) {
+}
+
+ContentSettingDecoration::~ContentSettingDecoration() {
+}
+
+bool ContentSettingDecoration::UpdateFromTabContents(
+ TabContents* tab_contents) {
+ bool was_visible = IsVisible();
+ int old_icon = content_setting_image_model_->get_icon();
+ content_setting_image_model_->UpdateFromTabContents(tab_contents);
+ SetVisible(content_setting_image_model_->is_visible());
+ bool decoration_changed = was_visible != IsVisible() ||
+ old_icon != content_setting_image_model_->get_icon();
+ if (IsVisible()) {
+ // TODO(thakis): We should use pdfs for these icons on OSX.
+ // http://crbug.com/35847
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ SetImage(rb.GetNativeImageNamed(content_setting_image_model_->get_icon()));
+ SetToolTip(base::SysUTF8ToNSString(
+ content_setting_image_model_->get_tooltip()));
+ }
+ return decoration_changed;
+}
+
+NSPoint ContentSettingDecoration::GetBubblePointInFrame(NSRect frame) {
+ const NSRect draw_frame = GetDrawRectInFrame(frame);
+ return NSMakePoint(NSMidX(draw_frame),
+ NSMaxY(draw_frame) - kPopupPointYOffset);
+}
+
+bool ContentSettingDecoration::OnMousePressed(NSRect frame) {
+ // Get host. This should be shared on linux/win/osx medium-term.
+ TabContents* tabContents =
+ BrowserList::GetLastActive()->GetSelectedTabContents();
+ if (!tabContents)
+ return true;
+
+ GURL url = tabContents->GetURL();
+ std::wstring displayHost;
+ net::AppendFormattedHost(
+ url,
+ UTF8ToWide(profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)),
+ &displayHost, NULL, NULL);
+
+ // Find point for bubble's arrow in screen coordinates.
+ // TODO(shess): |owner_| is only being used to fetch |field|.
+ // Consider passing in |control_view|. Or refactoring to be
+ // consistent with other decorations (which don't currently bring up
+ // their bubble directly).
+ AutocompleteTextField* field = owner_->GetAutocompleteTextField();
+ NSPoint anchor = GetBubblePointInFrame(frame);
+ anchor = [field convertPoint:anchor toView:nil];
+ anchor = [[field window] convertBaseToScreen:anchor];
+
+ // Open bubble.
+ ContentSettingBubbleModel* model =
+ ContentSettingBubbleModel::CreateContentSettingBubbleModel(
+ tabContents, profile_,
+ content_setting_image_model_->get_content_settings_type());
+ [ContentSettingBubbleController showForModel:model
+ parentWindow:[field window]
+ anchoredAt:anchor];
+ return true;
+}
+
+NSString* ContentSettingDecoration::GetToolTip() {
+ return tooltip_.get();
+}
+
+void ContentSettingDecoration::SetToolTip(NSString* tooltip) {
+ tooltip_.reset([tooltip retain]);
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h b/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h
new file mode 100644
index 0000000..fb13de1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h
@@ -0,0 +1,59 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_EV_BUBBLE_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_EV_BUBBLE_DECORATION_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/ui/cocoa/location_bar/bubble_decoration.h"
+
+// Draws the "Extended Validation SSL" bubble. This will be a lock
+// icon plus a label from the certification, and will replace the
+// location icon for URLs which have an EV cert. The |location_icon|
+// is used to fulfill drag-related calls.
+
+// TODO(shess): Refactor to pull the |location_icon| functionality out
+// into a distinct class like views |ClickHandler|.
+// http://crbug.com/48866
+
+class LocationIconDecoration;
+
+class EVBubbleDecoration : public BubbleDecoration {
+ public:
+ EVBubbleDecoration(LocationIconDecoration* location_icon, NSFont* font);
+
+ // |GetWidthForSpace()| will set |full_label| as the label, if it
+ // fits, else it will set an elided version.
+ void SetFullLabel(NSString* full_label);
+
+ // Get the point where the page info bubble should point within the
+ // decoration's frame, in the cell's coordinates.
+ NSPoint GetBubblePointInFrame(NSRect frame);
+
+ // Implement |LocationBarDecoration|.
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+ virtual bool IsDraggable();
+ virtual NSPasteboard* GetDragPasteboard();
+ virtual NSImage* GetDragImage();
+ virtual NSRect GetDragImageFrame(NSRect frame) {
+ return GetImageRectInFrame(frame);
+ }
+ virtual bool OnMousePressed(NSRect frame);
+ virtual bool AcceptsMousePress() { return true; }
+
+ private:
+ // Keeps a reference to the font for use when eliding.
+ scoped_nsobject<NSFont> font_;
+
+ // The real label. BubbleDecoration's label may be elided.
+ scoped_nsobject<NSString> full_label_;
+
+ LocationIconDecoration* location_icon_; // weak, owned by location bar.
+
+ DISALLOW_COPY_AND_ASSIGN(EVBubbleDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_EV_BUBBLE_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.mm b/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.mm
new file mode 100644
index 0000000..ad384f9
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.mm
@@ -0,0 +1,117 @@
+// Copyright (c) 2010 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/ui/cocoa/location_bar/ev_bubble_decoration.h"
+
+#include "app/text_elider.h"
+#import "base/logging.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h"
+#include "gfx/font.h"
+
+namespace {
+
+// TODO(shess): In general, decorations that don't fit in the
+// available space are omitted. This one never goes to omitted, it
+// sticks at 150px, which AFAICT follows the Windows code. Since the
+// Layout() code doesn't take this into account, it's possible the
+// control could end up with display artifacts, though things still
+// work (and don't crash).
+// http://crbug.com/49822
+
+// Minimum acceptable width for the ev bubble.
+const CGFloat kMinElidedBubbleWidth = 150.0;
+
+// Maximum amount of available space to make the bubble, subject to
+// |kMinElidedBubbleWidth|.
+const float kMaxBubbleFraction = 0.5;
+
+// The info-bubble point should look like it points to the bottom of the lock
+// icon. Determined with Pixie.app.
+const CGFloat kPageInfoBubblePointYOffset = 6.0;
+
+// TODO(shess): This is ugly, find a better way. Using it right now
+// so that I can crib from gtk and still be able to see that I'm using
+// the same values easily.
+NSColor* ColorWithRGBBytes(int rr, int gg, int bb) {
+ DCHECK_LE(rr, 255);
+ DCHECK_LE(bb, 255);
+ DCHECK_LE(gg, 255);
+ return [NSColor colorWithCalibratedRed:static_cast<float>(rr)/255.0
+ green:static_cast<float>(gg)/255.0
+ blue:static_cast<float>(bb)/255.0
+ alpha:1.0];
+}
+
+} // namespace
+
+EVBubbleDecoration::EVBubbleDecoration(
+ LocationIconDecoration* location_icon,
+ NSFont* font)
+ : BubbleDecoration(font),
+ font_([font retain]),
+ location_icon_(location_icon) {
+ // Color tuples stolen from location_bar_view_gtk.cc.
+ NSColor* border_color = ColorWithRGBBytes(0x90, 0xc3, 0x90);
+ NSColor* background_color = ColorWithRGBBytes(0xef, 0xfc, 0xef);
+ NSColor* text_color = ColorWithRGBBytes(0x07, 0x95, 0x00);
+ SetColors(border_color, background_color, text_color);
+}
+
+void EVBubbleDecoration::SetFullLabel(NSString* label) {
+ full_label_.reset([label retain]);
+ SetLabel(full_label_);
+}
+
+NSPoint EVBubbleDecoration::GetBubblePointInFrame(NSRect frame) {
+ NSRect image_rect = GetImageRectInFrame(frame);
+ return NSMakePoint(NSMidX(image_rect),
+ NSMaxY(image_rect) - kPageInfoBubblePointYOffset);
+}
+
+CGFloat EVBubbleDecoration::GetWidthForSpace(CGFloat width) {
+ // Limit with to not take up too much of the available width, but
+ // also don't let it shrink too much.
+ width = std::max(width * kMaxBubbleFraction, kMinElidedBubbleWidth);
+
+ // Use the full label if it fits.
+ NSImage* image = GetImage();
+ const CGFloat all_width = GetWidthForImageAndLabel(image, full_label_);
+ if (all_width <= width) {
+ SetLabel(full_label_);
+ return all_width;
+ }
+
+ // Width left for laying out the label.
+ const CGFloat width_left = width - GetWidthForImageAndLabel(image, @"");
+
+ // Middle-elide the label to fit |width_left|. This leaves the
+ // prefix and the trailing country code in place.
+ gfx::Font font(base::SysNSStringToWide([font_ fontName]),
+ [font_ pointSize]);
+ NSString* elided_label = base::SysUTF16ToNSString(
+ ElideText(base::SysNSStringToUTF16(full_label_), font, width_left, true));
+
+ // Use the elided label.
+ SetLabel(elided_label);
+ return GetWidthForImageAndLabel(image, elided_label);
+}
+
+// Pass mouse operations through to location icon.
+bool EVBubbleDecoration::IsDraggable() {
+ return location_icon_->IsDraggable();
+}
+
+NSPasteboard* EVBubbleDecoration::GetDragPasteboard() {
+ return location_icon_->GetDragPasteboard();
+}
+
+NSImage* EVBubbleDecoration::GetDragImage() {
+ return location_icon_->GetDragImage();
+}
+
+bool EVBubbleDecoration::OnMousePressed(NSRect frame) {
+ return location_icon_->OnMousePressed(frame);
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration_unittest.mm b/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration_unittest.mm
new file mode 100644
index 0000000..0506a72
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration_unittest.mm
@@ -0,0 +1,55 @@
+// Copyright (c) 2010 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 <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h"
+
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class EVBubbleDecorationTest : public CocoaTest {
+ public:
+ EVBubbleDecorationTest()
+ : decoration_(NULL, [NSFont userFontOfSize:12]) {
+ }
+
+ EVBubbleDecoration decoration_;
+};
+
+// Test that the decoration gets smaller when there's not enough space
+// to fit, within bounds.
+TEST_F(EVBubbleDecorationTest, MiddleElide) {
+ NSString* kLongString = @"A very long string with spaces";
+ const CGFloat kWide = 1000.0; // Wide enough to fit everything.
+ const CGFloat kNarrow = 10.0; // Too narrow for anything.
+ const CGFloat kMinimumWidth = 100.0; // Never should get this small.
+
+ const NSSize kImageSize = NSMakeSize(20.0, 20.0);
+ scoped_nsobject<NSImage> image([[NSImage alloc] initWithSize:kImageSize]);
+
+ decoration_.SetImage(image);
+ decoration_.SetFullLabel(kLongString);
+
+ // Lots of space, decoration not omitted.
+ EXPECT_NE(decoration_.GetWidthForSpace(kWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ // If the available space is of the same magnitude as the required
+ // space, the decoration doesn't eat it all up.
+ const CGFloat long_width = decoration_.GetWidthForSpace(kWide);
+ EXPECT_NE(decoration_.GetWidthForSpace(long_width + 20.0),
+ LocationBarDecoration::kOmittedWidth);
+ EXPECT_LT(decoration_.GetWidthForSpace(long_width + 20.0), long_width);
+
+ // If there is very little space, the decoration is still relatively
+ // big.
+ EXPECT_NE(decoration_.GetWidthForSpace(kNarrow),
+ LocationBarDecoration::kOmittedWidth);
+ EXPECT_GT(decoration_.GetWidthForSpace(kNarrow), kMinimumWidth);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/image_decoration.h b/chrome/browser/ui/cocoa/location_bar/image_decoration.h
new file mode 100644
index 0000000..c0bcfbf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/image_decoration.h
@@ -0,0 +1,36 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_IMAGE_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_IMAGE_DECORATION_H_
+#pragma once
+
+#import "base/scoped_nsobject.h"
+#include "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
+
+// |LocationBarDecoration| which sizes and draws itself according to
+// an |NSImage|.
+
+class ImageDecoration : public LocationBarDecoration {
+ public:
+ ImageDecoration();
+ virtual ~ImageDecoration();
+
+ NSImage* GetImage();
+ void SetImage(NSImage* image);
+
+ // Returns the part of |frame| the image is drawn in.
+ NSRect GetDrawRectInFrame(NSRect frame);
+
+ // Implement |LocationBarDecoration|.
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+ virtual void DrawInFrame(NSRect frame, NSView* control_view);
+
+ private:
+ scoped_nsobject<NSImage> image_;
+
+ DISALLOW_COPY_AND_ASSIGN(ImageDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_IMAGE_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/image_decoration.mm b/chrome/browser/ui/cocoa/location_bar/image_decoration.mm
new file mode 100644
index 0000000..8056a4e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/image_decoration.mm
@@ -0,0 +1,54 @@
+// Copyright (c) 2010 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.
+
+#include <cmath>
+
+#import "chrome/browser/ui/cocoa/location_bar/image_decoration.h"
+
+#import "chrome/browser/ui/cocoa/image_utils.h"
+
+ImageDecoration::ImageDecoration() {
+}
+
+ImageDecoration::~ImageDecoration() {
+}
+
+NSImage* ImageDecoration::GetImage() {
+ return image_;
+}
+
+void ImageDecoration::SetImage(NSImage* image) {
+ image_.reset([image retain]);
+}
+
+NSRect ImageDecoration::GetDrawRectInFrame(NSRect frame) {
+ NSImage* image = GetImage();
+ if (!image)
+ return frame;
+
+ // Center the image within the frame.
+ const CGFloat delta_height = NSHeight(frame) - [image size].height;
+ const CGFloat y_inset = std::floor(delta_height / 2.0);
+ const CGFloat delta_width = NSWidth(frame) - [image size].width;
+ const CGFloat x_inset = std::floor(delta_width / 2.0);
+ return NSInsetRect(frame, x_inset, y_inset);
+}
+
+CGFloat ImageDecoration::GetWidthForSpace(CGFloat width) {
+ NSImage* image = GetImage();
+ if (image) {
+ const CGFloat image_width = [image size].width;
+ if (image_width <= width)
+ return image_width;
+ }
+ return kOmittedWidth;
+}
+
+void ImageDecoration::DrawInFrame(NSRect frame, NSView* control_view) {
+ [GetImage() drawInRect:GetDrawRectInFrame(frame)
+ fromRect:NSZeroRect // Entire image
+ operation:NSCompositeSourceOver
+ fraction:1.0
+ neverFlipped:YES];
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/image_decoration_unittest.mm b/chrome/browser/ui/cocoa/location_bar/image_decoration_unittest.mm
new file mode 100644
index 0000000..db69b0d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/image_decoration_unittest.mm
@@ -0,0 +1,55 @@
+// Copyright (c) 2010 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 <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/location_bar/image_decoration.h"
+
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class ImageDecorationTest : public CocoaTest {
+ public:
+ ImageDecoration decoration_;
+};
+
+TEST_F(ImageDecorationTest, SetGetImage) {
+ EXPECT_FALSE(decoration_.GetImage());
+
+ const NSSize kImageSize = NSMakeSize(20.0, 20.0);
+ scoped_nsobject<NSImage> image([[NSImage alloc] initWithSize:kImageSize]);
+
+ decoration_.SetImage(image);
+ EXPECT_EQ(decoration_.GetImage(), image);
+
+ decoration_.SetImage(nil);
+ EXPECT_FALSE(decoration_.GetImage());
+}
+
+TEST_F(ImageDecorationTest, GetWidthForSpace) {
+ const CGFloat kWide = 100.0;
+ const CGFloat kNarrow = 10.0;
+
+ // Decoration with no image is omitted.
+ EXPECT_EQ(decoration_.GetWidthForSpace(kWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ const NSSize kImageSize = NSMakeSize(20.0, 20.0);
+ scoped_nsobject<NSImage> image([[NSImage alloc] initWithSize:kImageSize]);
+
+ // Decoration takes up the space of the image.
+ decoration_.SetImage(image);
+ EXPECT_EQ(decoration_.GetWidthForSpace(kWide), kImageSize.width);
+
+ // If the image doesn't fit, decoration is omitted.
+ EXPECT_EQ(decoration_.GetWidthForSpace(kNarrow),
+ LocationBarDecoration::kOmittedWidth);
+}
+
+// TODO(shess): It would be nice to test mouse clicks and dragging,
+// but those are hard because they require a real |owner|.
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.h b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.h
new file mode 100644
index 0000000..97a6b3e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.h
@@ -0,0 +1,43 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_INSTANT_OPT_IN_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_INSTANT_OPT_IN_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+class Profile;
+
+// This delegate receives callbacks from the InstantOptInController when the OK
+// and Cancel buttons are pushed.
+class InstantOptInControllerDelegate {
+ public:
+ virtual void UserPressedOptIn(bool opt_in) = 0;
+
+ protected:
+ virtual ~InstantOptInControllerDelegate() {}
+};
+
+// Manages an instant opt-in view, which is part of the omnibox popup.
+@interface InstantOptInController : NSViewController {
+ @private
+ InstantOptInControllerDelegate* delegate_; // weak
+
+ // Needed in order to localize text and resize to fit.
+ IBOutlet NSButton* okButton_;
+ IBOutlet NSButton* cancelButton_;
+ IBOutlet NSTextField* label_;
+}
+
+// Designated initializer.
+- (id)initWithDelegate:(InstantOptInControllerDelegate*)delegate;
+
+// Button actions.
+- (IBAction)ok:(id)sender;
+- (IBAction)cancel:(id)sender;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_INSTANT_OPT_IN_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.mm b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.mm
new file mode 100644
index 0000000..17b0d15
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.mm
@@ -0,0 +1,31 @@
+// Copyright (c) 2010 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/ui/cocoa/location_bar/instant_opt_in_controller.h"
+
+#include "base/mac_util.h"
+
+@implementation InstantOptInController
+
+- (id)initWithDelegate:(InstantOptInControllerDelegate*)delegate {
+ if ((self = [super initWithNibName:@"InstantOptIn"
+ bundle:mac_util::MainAppBundle()])) {
+ delegate_ = delegate;
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ // TODO(rohitrao): Translate and resize strings.
+}
+
+- (IBAction)ok:(id)sender {
+ delegate_->UserPressedOptIn(true);
+}
+
+- (IBAction)cancel:(id)sender {
+ delegate_->UserPressedOptIn(false);
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller_unittest.mm b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller_unittest.mm
new file mode 100644
index 0000000..81ef513
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller_unittest.mm
@@ -0,0 +1,62 @@
+// Copyright (c) 2010 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.
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface InstantOptInController (ExposedForTesting)
+- (NSButton*)okButton;
+- (NSButton*)cancelButton;
+@end
+
+@implementation InstantOptInController (ExposedForTesting)
+- (NSButton*)okButton {
+ return okButton_;
+}
+
+- (NSButton*)cancelButton {
+ return cancelButton_;
+}
+@end
+
+
+namespace {
+
+class MockDelegate : public InstantOptInControllerDelegate {
+ public:
+ MOCK_METHOD1(UserPressedOptIn, void(bool opt_in));
+};
+
+class InstantOptInControllerTest : public CocoaTest {
+ public:
+ void SetUp() {
+ CocoaTest::SetUp();
+
+ controller_.reset(
+ [[InstantOptInController alloc] initWithDelegate:&delegate_]);
+
+ NSView* parent = [test_window() contentView];
+ [parent addSubview:[controller_ view]];
+ }
+
+ MockDelegate delegate_;
+ scoped_nsobject<InstantOptInController> controller_;
+};
+
+TEST_F(InstantOptInControllerTest, OkButtonCallback) {
+ EXPECT_CALL(delegate_, UserPressedOptIn(true));
+ [[controller_ okButton] performClick:nil];
+}
+
+TEST_F(InstantOptInControllerTest, CancelButtonCallback) {
+ EXPECT_CALL(delegate_, UserPressedOptIn(false));
+ [[controller_ cancelButton] performClick:nil];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h
new file mode 100644
index 0000000..b9ef6bd
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h
@@ -0,0 +1,16 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_INSTANT_OPT_IN_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_INSTANT_OPT_IN_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// The instant opt in view that is embedded in the omnibox. Draws rounded
+// bottom corners and a horizontal gray line at the top.
+@interface InstantOptInView : NSView
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_INSTANT_OPT_IN_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.mm b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.mm
new file mode 100644
index 0000000..06ca79d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.mm
@@ -0,0 +1,54 @@
+// Copyright (c) 2010 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.
+
+#include <algorithm>
+
+#import "chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h"
+#import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h"
+
+namespace {
+// How to round off the popup's corners. Goal is to match star and go
+// buttons.
+const CGFloat kPopupRoundingRadius = 3.5;
+
+// How far from the top of the view to place the horizontal line.
+const CGFloat kHorizontalLineTopOffset = 2;
+
+// How far from the sides to inset the horizontal line.
+const CGFloat kHorizontalLineInset = 2;
+}
+
+@implementation InstantOptInView
+
+- (void)drawRect:(NSRect)rect {
+ // Round off the bottom corners only.
+ NSBezierPath* path =
+ [NSBezierPath gtm_bezierPathWithRoundRect:[self bounds]
+ topLeftCornerRadius:0
+ topRightCornerRadius:0
+ bottomLeftCornerRadius:kPopupRoundingRadius
+ bottomRightCornerRadius:kPopupRoundingRadius];
+
+ [NSGraphicsContext saveGraphicsState];
+ [path addClip];
+
+ // Background is white.
+ [[NSColor whiteColor] set];
+ NSRectFill(rect);
+
+ // Draw a horizontal line 2 px down from the top of the view, inset at the
+ // sides by 2 px.
+ CGFloat lineY = NSMaxY([self bounds]) - kHorizontalLineTopOffset;
+ CGFloat minX = std::min(NSMinX([self bounds]) + kHorizontalLineInset,
+ NSMaxX([self bounds]));
+ CGFloat maxX = std::max(NSMaxX([self bounds]) - kHorizontalLineInset,
+ NSMinX([self bounds]));
+
+ [[NSColor lightGrayColor] set];
+ NSRectFill(NSMakeRect(minX, lineY, maxX - minX, 1));
+
+ [NSGraphicsContext restoreGraphicsState];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view_unittest.mm b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view_unittest.mm
new file mode 100644
index 0000000..ce5ff48
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view_unittest.mm
@@ -0,0 +1,26 @@
+// Copyright (c) 2010 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/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h"
+
+namespace {
+
+class InstantOptInViewTest : public CocoaTest {
+ public:
+ InstantOptInViewTest() {
+ NSRect content_frame = [[test_window() contentView] frame];
+ scoped_nsobject<InstantOptInView> view(
+ [[InstantOptInView alloc] initWithFrame:content_frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ InstantOptInView* view_; // Weak. Owned by the view hierarchy.
+};
+
+// Tests display, add/remove.
+TEST_VIEW(InstantOptInViewTest, view_);
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h b/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h
new file mode 100644
index 0000000..3b8c607
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h
@@ -0,0 +1,47 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_KEYWORD_HINT_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_KEYWORD_HINT_DECORATION_H_
+#pragma once
+
+#include <string>
+
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
+
+#import "base/scoped_nsobject.h"
+
+// Draws the keyword hint, "Press [tab] to search <site>".
+
+class KeywordHintDecoration : public LocationBarDecoration {
+ public:
+ KeywordHintDecoration(NSFont* font);
+ virtual ~KeywordHintDecoration();
+
+ // Calculates the message to display and where to place the [tab]
+ // image.
+ void SetKeyword(const std::wstring& keyword, bool is_extension_keyword);
+
+ // Implement |LocationBarDecoration|.
+ virtual void DrawInFrame(NSRect frame, NSView* control_view);
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+
+ private:
+ // Fetch and cache the [tab] image.
+ NSImage* GetHintImage();
+
+ // Attributes for drawing the hint string, such as font and color.
+ scoped_nsobject<NSDictionary> attributes_;
+
+ // Cache for the [tab] image.
+ scoped_nsobject<NSImage> hint_image_;
+
+ // The text to display to the left and right of the hint image.
+ scoped_nsobject<NSString> hint_prefix_;
+ scoped_nsobject<NSString> hint_suffix_;
+
+ DISALLOW_COPY_AND_ASSIGN(KeywordHintDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_KEYWORD_HINT_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.mm b/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.mm
new file mode 100644
index 0000000..0b080319
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.mm
@@ -0,0 +1,160 @@
+// Copyright (c) 2010 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.
+
+#include <cmath>
+
+#import "chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h"
+
+#include "app/l10n_util.h"
+#include "app/resource_bundle.h"
+#include "base/logging.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#include "grit/theme_resources.h"
+#include "grit/generated_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+
+namespace {
+
+// How far to inset the hint text area from sides.
+const CGFloat kHintTextYInset = 4.0;
+
+// How far to inset the hint image from sides. Lines baseline of text
+// in image with baseline of prefix and suffix.
+const CGFloat kHintImageYInset = 4.0;
+
+// Extra padding right and left of the image.
+const CGFloat kHintImagePadding = 1.0;
+
+// Maxmimum of the available space to allow the hint to take over.
+// Should leave enough so that the user has space to edit things.
+const CGFloat kHintAvailableRatio = 2.0 / 3.0;
+
+// Helper to convert |s| to an |NSString|, trimming whitespace at
+// ends.
+NSString* TrimAndConvert(const std::wstring& s) {
+ std::wstring output;
+ TrimWhitespace(s, TRIM_ALL, &output);
+ return base::SysWideToNSString(output);
+}
+
+} // namespace
+
+KeywordHintDecoration::KeywordHintDecoration(NSFont* font) {
+ NSColor* text_color = [NSColor lightGrayColor];
+ NSDictionary* attributes =
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ font, NSFontAttributeName,
+ text_color, NSForegroundColorAttributeName,
+ nil];
+ attributes_.reset([attributes retain]);
+}
+
+KeywordHintDecoration::~KeywordHintDecoration() {
+}
+
+NSImage* KeywordHintDecoration::GetHintImage() {
+ if (!hint_image_) {
+ SkBitmap* skiaBitmap = ResourceBundle::GetSharedInstance().
+ GetBitmapNamed(IDR_LOCATION_BAR_KEYWORD_HINT_TAB);
+ if (skiaBitmap)
+ hint_image_.reset([gfx::SkBitmapToNSImage(*skiaBitmap) retain]);
+ }
+ return hint_image_;
+}
+
+void KeywordHintDecoration::SetKeyword(const std::wstring& short_name,
+ bool is_extension_keyword) {
+ // KEYWORD_HINT is a message like "Press [tab] to search <site>".
+ // [tab] is a parameter to be replaced by an image. "<site>" is
+ // derived from |short_name|.
+ std::vector<size_t> content_param_offsets;
+ int message_id = is_extension_keyword ?
+ IDS_OMNIBOX_EXTENSION_KEYWORD_HINT : IDS_OMNIBOX_KEYWORD_HINT;
+ const std::wstring keyword_hint(
+ l10n_util::GetStringF(message_id,
+ std::wstring(), short_name,
+ &content_param_offsets));
+
+ // Should always be 2 offsets, see the comment in
+ // location_bar_view.cc after IDS_OMNIBOX_KEYWORD_HINT fetch.
+ DCHECK_EQ(content_param_offsets.size(), 2U);
+
+ // Where to put the [tab] image.
+ const size_t split = content_param_offsets.front();
+
+ // Trim the spaces from the edges (there is space in the image) and
+ // convert to |NSString|.
+ hint_prefix_.reset([TrimAndConvert(keyword_hint.substr(0, split)) retain]);
+ hint_suffix_.reset([TrimAndConvert(keyword_hint.substr(split)) retain]);
+}
+
+CGFloat KeywordHintDecoration::GetWidthForSpace(CGFloat width) {
+ NSImage* image = GetHintImage();
+ const CGFloat image_width = image ? [image size].width : 0.0;
+
+ // AFAICT, on Windows the choices are "everything" if it fits, then
+ // "image only" if it fits.
+
+ // Entirely too small to fit, omit.
+ if (width < image_width)
+ return kOmittedWidth;
+
+ // Show the full hint if it won't take up too much space. The image
+ // needs to be placed at a pixel boundary, round the text widths so
+ // that any partially-drawn pixels don't look too close (or too
+ // far).
+ CGFloat full_width =
+ std::floor([hint_prefix_ sizeWithAttributes:attributes_].width + 0.5) +
+ kHintImagePadding + image_width + kHintImagePadding +
+ std::floor([hint_suffix_ sizeWithAttributes:attributes_].width + 0.5);
+ if (full_width <= width * kHintAvailableRatio)
+ return full_width;
+
+ return image_width;
+}
+
+void KeywordHintDecoration::DrawInFrame(NSRect frame, NSView* control_view) {
+ NSImage* image = GetHintImage();
+ const CGFloat image_width = image ? [image size].width : 0.0;
+
+ const bool draw_full = NSWidth(frame) > image_width;
+
+ if (draw_full) {
+ NSRect prefix_rect = NSInsetRect(frame, 0.0, kHintTextYInset);
+ const CGFloat prefix_width =
+ [hint_prefix_ sizeWithAttributes:attributes_].width;
+ DCHECK_GE(NSWidth(prefix_rect), prefix_width);
+ [hint_prefix_ drawInRect:prefix_rect withAttributes:attributes_];
+
+ // The image should be drawn at a pixel boundary, round the prefix
+ // so that partial pixels aren't oddly close (or distant).
+ frame.origin.x += std::floor(prefix_width + 0.5) + kHintImagePadding;
+ frame.size.width -= std::floor(prefix_width + 0.5) + kHintImagePadding;
+ }
+
+ NSRect image_rect = NSInsetRect(frame, 0.0, kHintImageYInset);
+ image_rect.size = [image size];
+ [image drawInRect:image_rect
+ fromRect:NSZeroRect // Entire image
+ operation:NSCompositeSourceOver
+ fraction:1.0
+ neverFlipped:YES];
+ frame.origin.x += NSWidth(image_rect);
+ frame.size.width -= NSWidth(image_rect);
+
+ if (draw_full) {
+ NSRect suffix_rect = NSInsetRect(frame, 0.0, kHintTextYInset);
+ const CGFloat suffix_width =
+ [hint_suffix_ sizeWithAttributes:attributes_].width;
+
+ // Right-justify the text within the remaining space, so it
+ // doesn't get too close to the image relative to a following
+ // decoration.
+ suffix_rect.origin.x = NSMaxX(suffix_rect) - suffix_width;
+ DCHECK_GE(NSWidth(suffix_rect), suffix_width);
+ [hint_suffix_ drawInRect:suffix_rect withAttributes:attributes_];
+ }
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration_unittest.mm b/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration_unittest.mm
new file mode 100644
index 0000000..bfcf454
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration_unittest.mm
@@ -0,0 +1,57 @@
+// Copyright (c) 2010 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 <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h"
+
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class KeywordHintDecorationTest : public CocoaTest {
+ public:
+ KeywordHintDecorationTest()
+ : decoration_(NULL) {
+ }
+
+ KeywordHintDecoration decoration_;
+};
+
+TEST_F(KeywordHintDecorationTest, GetWidthForSpace) {
+ decoration_.SetVisible(true);
+ decoration_.SetKeyword(std::wstring(L"Google"), false);
+
+ const CGFloat kVeryWide = 1000.0;
+ const CGFloat kFairlyWide = 100.0; // Estimate for full hint space.
+ const CGFloat kEditingSpace = 50.0;
+
+ // Wider than the [tab] image when we have lots of space.
+ EXPECT_NE(decoration_.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+ EXPECT_GE(decoration_.GetWidthForSpace(kVeryWide), kFairlyWide);
+
+ // When there's not enough space for the text, trims to something
+ // narrower.
+ const CGFloat full_width = decoration_.GetWidthForSpace(kVeryWide);
+ const CGFloat not_wide_enough = full_width - 10.0;
+ EXPECT_NE(decoration_.GetWidthForSpace(not_wide_enough),
+ LocationBarDecoration::kOmittedWidth);
+ EXPECT_LT(decoration_.GetWidthForSpace(not_wide_enough), full_width);
+
+ // Even trims when there's enough space for everything, but it would
+ // eat "too much".
+ EXPECT_NE(decoration_.GetWidthForSpace(full_width + kEditingSpace),
+ LocationBarDecoration::kOmittedWidth);
+ EXPECT_LT(decoration_.GetWidthForSpace(full_width + kEditingSpace),
+ full_width);
+
+ // Omitted when not wide enough to fit even the image.
+ const CGFloat image_width = decoration_.GetWidthForSpace(not_wide_enough);
+ EXPECT_EQ(decoration_.GetWidthForSpace(image_width - 1.0),
+ LocationBarDecoration::kOmittedWidth);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h b/chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h
new file mode 100644
index 0000000..5947edc
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h
@@ -0,0 +1,89 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_LOCATION_BAR_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_LOCATION_BAR_DECORATION_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/basictypes.h"
+
+// Base class for decorations at the left and right of the location
+// bar. For instance, the location icon.
+
+// |LocationBarDecoration| and subclasses should approximately
+// parallel the classes provided under views/location_bar/. The term
+// "decoration" is used because "view" has strong connotations in
+// Cocoa, and while these are view-like, they aren't views at all.
+// Decorations are more like Cocoa cells, except implemented in C++ to
+// allow more similarity to the other platform implementations.
+
+class LocationBarDecoration {
+ public:
+ LocationBarDecoration()
+ : visible_(false) {
+ }
+ virtual ~LocationBarDecoration() {}
+
+ // Determines whether the decoration is visible.
+ virtual bool IsVisible() const {
+ return visible_;
+ }
+ virtual void SetVisible(bool visible) {
+ visible_ = visible;
+ }
+
+ // Decorations can change their size to fit the available space.
+ // Returns the width the decoration will use in the space allotted,
+ // or |kOmittedWidth| if it should be omitted.
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+
+ // Draw the decoration in the frame provided. The frame will be
+ // generated from an earlier call to |GetWidthForSpace()|.
+ virtual void DrawInFrame(NSRect frame, NSView* control_view);
+
+ // Returns the tooltip for this decoration, return |nil| for no tooltip.
+ virtual NSString* GetToolTip() { return nil; }
+
+ // Decorations which do not accept mouse events are treated like the
+ // field's background for purposes of selecting text. When such
+ // decorations are adjacent to the text area, they will show the
+ // I-beam cursor. Decorations which do accept mouse events will get
+ // an arrow cursor when the mouse is over them.
+ virtual bool AcceptsMousePress() { return false; }
+
+ // Determine if the item can act as a drag source.
+ virtual bool IsDraggable() { return false; }
+
+ // The image to drag.
+ virtual NSImage* GetDragImage() { return nil; }
+
+ // Return the place within the decoration's frame where the
+ // |GetDragImage()| comes from. This is used to make sure the image
+ // appears correctly under the mouse while dragging. |frame|
+ // matches the frame passed to |DrawInFrame()|.
+ virtual NSRect GetDragImageFrame(NSRect frame) { return NSZeroRect; }
+
+ // The pasteboard to drag.
+ virtual NSPasteboard* GetDragPasteboard() { return nil; }
+
+ // Called on mouse down. Return |false| to indicate that the press
+ // was not processed and should be handled by the cell.
+ virtual bool OnMousePressed(NSRect frame) { return false; }
+
+ // Called to get the right-click menu, return |nil| for no menu.
+ virtual NSMenu* GetMenu() { return nil; }
+
+ // Width returned by |GetWidthForSpace()| when the item should be
+ // omitted for this width;
+ static const CGFloat kOmittedWidth;
+
+ private:
+ bool visible_;
+
+ DISALLOW_COPY_AND_ASSIGN(LocationBarDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_LOCATION_BAR_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/location_bar_decoration.mm b/chrome/browser/ui/cocoa/location_bar/location_bar_decoration.mm
new file mode 100644
index 0000000..bbd9b0b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/location_bar_decoration.mm
@@ -0,0 +1,18 @@
+// Copyright (c) 2010 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/ui/cocoa/location_bar/location_bar_decoration.h"
+
+#include "base/logging.h"
+
+const CGFloat LocationBarDecoration::kOmittedWidth = 0.0;
+
+CGFloat LocationBarDecoration::GetWidthForSpace(CGFloat width) {
+ NOTREACHED();
+ return kOmittedWidth;
+}
+
+void LocationBarDecoration::DrawInFrame(NSRect frame, NSView* control_view) {
+ NOTREACHED();
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h b/chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h
new file mode 100644
index 0000000..7675165d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h
@@ -0,0 +1,237 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_VIEW_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_VIEW_MAC_H_
+#pragma once
+
+#include <string>
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "base/scoped_vector.h"
+#include "chrome/browser/autocomplete/autocomplete_edit.h"
+#include "chrome/browser/autocomplete/autocomplete_edit_view_mac.h"
+#include "chrome/browser/extensions/image_loading_tracker.h"
+#include "chrome/browser/first_run/first_run.h"
+#include "chrome/browser/location_bar.h"
+#include "chrome/browser/toolbar_model.h"
+#include "chrome/common/content_settings_types.h"
+
+@class AutocompleteTextField;
+class CommandUpdater;
+class ContentSettingDecoration;
+class ContentSettingImageModel;
+class EVBubbleDecoration;
+@class ExtensionPopupController;
+class KeywordHintDecoration;
+class LocationIconDecoration;
+class PageActionDecoration;
+class Profile;
+class SelectedKeywordDecoration;
+class SkBitmap;
+class StarDecoration;
+class ToolbarModel;
+
+// A C++ bridge class that represents the location bar UI element to
+// the portable code. Wires up an AutocompleteEditViewMac instance to
+// the location bar text field, which handles most of the work.
+
+class LocationBarViewMac : public AutocompleteEditController,
+ public LocationBar,
+ public LocationBarTesting,
+ public NotificationObserver {
+ public:
+ LocationBarViewMac(AutocompleteTextField* field,
+ CommandUpdater* command_updater,
+ ToolbarModel* toolbar_model,
+ Profile* profile,
+ Browser* browser);
+ virtual ~LocationBarViewMac();
+
+ // Overridden from LocationBar:
+ virtual void ShowFirstRunBubble(FirstRun::BubbleType bubble_type);
+ virtual void SetSuggestedText(const string16& text);
+ virtual std::wstring GetInputString() const;
+ virtual WindowOpenDisposition GetWindowOpenDisposition() const;
+ virtual PageTransition::Type GetPageTransition() const;
+ virtual void AcceptInput();
+ virtual void FocusLocation(bool select_all);
+ virtual void FocusSearch();
+ virtual void UpdateContentSettingsIcons();
+ virtual void UpdatePageActions();
+ virtual void InvalidatePageActions();
+ virtual void SaveStateToContents(TabContents* contents);
+ virtual void Revert();
+ virtual const AutocompleteEditView* location_entry() const {
+ return edit_view_.get();
+ }
+ virtual AutocompleteEditView* location_entry() {
+ return edit_view_.get();
+ }
+ virtual LocationBarTesting* GetLocationBarForTesting() { return this; }
+
+ // Overridden from LocationBarTesting:
+ virtual int PageActionCount();
+ virtual int PageActionVisibleCount();
+ virtual ExtensionAction* GetPageAction(size_t index);
+ virtual ExtensionAction* GetVisiblePageAction(size_t index);
+ virtual void TestPageActionPressed(size_t index);
+
+ // Set/Get the editable state of the field.
+ void SetEditable(bool editable);
+ bool IsEditable();
+
+ // Set the starred state of the bookmark star.
+ void SetStarred(bool starred);
+
+ // Get the point on the star for the bookmark bubble to aim at.
+ NSPoint GetBookmarkBubblePoint() const;
+
+ // Get the point in the security icon at which the page info bubble aims.
+ NSPoint GetPageInfoBubblePoint() const;
+
+ // Get the point in the omnibox at which the first run bubble aims.
+ NSPoint GetFirstRunBubblePoint() const;
+
+ // Updates the location bar. Resets the bar's permanent text and
+ // security style, and if |should_restore_state| is true, restores
+ // saved state from the tab (for tab switching).
+ void Update(const TabContents* tab, bool should_restore_state);
+
+ // Layout the various decorations which live in the field.
+ void Layout();
+
+ // Returns the current TabContents.
+ TabContents* GetTabContents() const;
+
+ // Sets preview_enabled_ for the PageActionImageView associated with this
+ // |page_action|. If |preview_enabled|, the location bar will display the
+ // PageAction icon even if it has not been activated by the extension.
+ // This is used by the ExtensionInstalledBubble to preview what the icon
+ // will look like for the user upon installation of the extension.
+ void SetPreviewEnabledPageAction(ExtensionAction* page_action,
+ bool preview_enabled);
+
+ // Return |page_action|'s info-bubble point in window coordinates.
+ // This function should always be called with a visible page action.
+ // If |page_action| is not a page action or not visible, NOTREACHED()
+ // is called and this function returns |NSZeroPoint|.
+ NSPoint GetPageActionBubblePoint(ExtensionAction* page_action);
+
+ // Get the blocked-popup content setting's frame in window
+ // coordinates. Used by the blocked-popup animation. Returns
+ // |NSZeroRect| if the relevant content setting decoration is not
+ // visible.
+ NSRect GetBlockedPopupRect() const;
+
+ // AutocompleteEditController implementation.
+ virtual void OnAutocompleteWillClosePopup();
+ virtual void OnAutocompleteLosingFocus(gfx::NativeView unused);
+ virtual void OnAutocompleteWillAccept();
+ virtual bool OnCommitSuggestedText(const std::wstring& typed_text);
+ virtual void OnSetSuggestedSearchText(const string16& suggested_text);
+ virtual void OnPopupBoundsChanged(const gfx::Rect& bounds);
+ virtual void OnAutocompleteAccept(const GURL& url,
+ WindowOpenDisposition disposition,
+ PageTransition::Type transition,
+ const GURL& alternate_nav_url);
+ virtual void OnChanged();
+ virtual void OnSelectionBoundsChanged();
+ virtual void OnInputInProgress(bool in_progress);
+ virtual void OnKillFocus();
+ virtual void OnSetFocus();
+ virtual SkBitmap GetFavIcon() const;
+ virtual std::wstring GetTitle() const;
+
+ NSImage* GetKeywordImage(const std::wstring& keyword);
+
+ AutocompleteTextField* GetAutocompleteTextField() { return field_; }
+
+
+ // Overridden from NotificationObserver.
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ private:
+ // Posts |notification| to the default notification center.
+ void PostNotification(NSString* notification);
+
+ // Return the decoration for |page_action|.
+ PageActionDecoration* GetPageActionDecoration(ExtensionAction* page_action);
+
+ // Clear the page-action decorations.
+ void DeletePageActionDecorations();
+
+ // Re-generate the page-action decorations from the profile's
+ // extension service.
+ void RefreshPageActionDecorations();
+
+ // Updates visibility of the content settings icons based on the current
+ // tab contents state.
+ bool RefreshContentSettingsDecorations();
+
+ void ShowFirstRunBubbleInternal(FirstRun::BubbleType bubble_type);
+
+ scoped_ptr<AutocompleteEditViewMac> edit_view_;
+
+ CommandUpdater* command_updater_; // Weak, owned by Browser.
+
+ AutocompleteTextField* field_; // owned by tab controller
+
+ // When we get an OnAutocompleteAccept notification from the autocomplete
+ // edit, we save the input string so we can give it back to the browser on
+ // the LocationBar interface via GetInputString().
+ std::wstring location_input_;
+
+ // The user's desired disposition for how their input should be opened.
+ WindowOpenDisposition disposition_;
+
+ // A decoration that shows an icon to the left of the address.
+ scoped_ptr<LocationIconDecoration> location_icon_decoration_;
+
+ // A decoration that shows the keyword-search bubble on the left.
+ scoped_ptr<SelectedKeywordDecoration> selected_keyword_decoration_;
+
+ // A decoration that shows a lock icon and ev-cert label in a bubble
+ // on the left.
+ scoped_ptr<EVBubbleDecoration> ev_bubble_decoration_;
+
+ // Bookmark star right of page actions.
+ scoped_ptr<StarDecoration> star_decoration_;
+
+ // Any installed Page Actions.
+ ScopedVector<PageActionDecoration> page_action_decorations_;
+
+ // The content blocked decorations.
+ ScopedVector<ContentSettingDecoration> content_setting_decorations_;
+
+ // Keyword hint decoration displayed on the right-hand side.
+ scoped_ptr<KeywordHintDecoration> keyword_hint_decoration_;
+
+ Profile* profile_;
+
+ Browser* browser_;
+
+ ToolbarModel* toolbar_model_; // Weak, owned by Browser.
+
+ // Whether or not to update the instant preview.
+ bool update_instant_;
+
+ // The transition type to use for the navigation.
+ PageTransition::Type transition_;
+
+ // Used to register for notifications received by NotificationObserver.
+ NotificationRegistrar registrar_;
+
+ // Used to schedule a task for the first run info bubble.
+ ScopedRunnableMethodFactory<LocationBarViewMac> first_run_bubble_;
+
+ DISALLOW_COPY_AND_ASSIGN(LocationBarViewMac);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_VIEW_MAC_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.mm b/chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.mm
new file mode 100644
index 0000000..6fbac24
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.mm
@@ -0,0 +1,690 @@
+// Copyright (c) 2010 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/ui/cocoa/location_bar/location_bar_view_mac.h"
+
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#include "base/nsimage_cache_mac.h"
+#include "base/stl_util-inl.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/alternate_nav_url_fetcher.h"
+#import "chrome/browser/app_controller_mac.h"
+#import "chrome/browser/autocomplete/autocomplete_edit_view_mac.h"
+#import "chrome/browser/autocomplete/autocomplete_popup_model.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/command_updater.h"
+#include "chrome/browser/content_setting_image_model.h"
+#include "chrome/browser/content_setting_bubble_model.h"
+#include "chrome/browser/defaults.h"
+#include "chrome/browser/extensions/extension_browser_event_router.h"
+#include "chrome/browser/extensions/extensions_service.h"
+#include "chrome/browser/extensions/extension_tabs_module.h"
+#include "chrome/browser/instant/instant_controller.h"
+#include "chrome/browser/location_bar_util.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/search_engines/template_url.h"
+#include "chrome/browser/search_engines/template_url_model.h"
+#include "chrome/browser/tab_contents/navigation_entry.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/content_setting_bubble_cocoa.h"
+#include "chrome/browser/ui/cocoa/event_utils.h"
+#import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h"
+#import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
+#import "chrome/browser/ui/cocoa/first_run_bubble_controller.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/location_bar/content_setting_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/page_action_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/star_decoration.h"
+#include "chrome/common/extensions/extension.h"
+#include "chrome/common/extensions/extension_action.h"
+#include "chrome/common/extensions/extension_resource.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/pref_names.h"
+#include "net/base/net_util.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+namespace {
+
+// Vertical space between the bottom edge of the location_bar and the first run
+// bubble arrow point.
+const static int kFirstRunBubbleYOffset = 1;
+
+}
+
+// TODO(shess): This code is mostly copied from the gtk
+// implementation. Make sure it's all appropriate and flesh it out.
+
+LocationBarViewMac::LocationBarViewMac(
+ AutocompleteTextField* field,
+ CommandUpdater* command_updater,
+ ToolbarModel* toolbar_model,
+ Profile* profile,
+ Browser* browser)
+ : edit_view_(new AutocompleteEditViewMac(this, toolbar_model, profile,
+ command_updater, field)),
+ command_updater_(command_updater),
+ field_(field),
+ disposition_(CURRENT_TAB),
+ location_icon_decoration_(new LocationIconDecoration(this)),
+ selected_keyword_decoration_(
+ new SelectedKeywordDecoration(
+ AutocompleteEditViewMac::GetFieldFont())),
+ ev_bubble_decoration_(
+ new EVBubbleDecoration(location_icon_decoration_.get(),
+ AutocompleteEditViewMac::GetFieldFont())),
+ star_decoration_(new StarDecoration(command_updater)),
+ keyword_hint_decoration_(
+ new KeywordHintDecoration(AutocompleteEditViewMac::GetFieldFont())),
+ profile_(profile),
+ browser_(browser),
+ toolbar_model_(toolbar_model),
+ update_instant_(true),
+ transition_(PageTransition::TYPED),
+ first_run_bubble_(this) {
+ for (size_t i = 0; i < CONTENT_SETTINGS_NUM_TYPES; ++i) {
+ DCHECK_EQ(i, content_setting_decorations_.size());
+ ContentSettingsType type = static_cast<ContentSettingsType>(i);
+ content_setting_decorations_.push_back(
+ new ContentSettingDecoration(type, this, profile_));
+ }
+
+ registrar_.Add(this,
+ NotificationType::EXTENSION_PAGE_ACTION_VISIBILITY_CHANGED,
+ NotificationService::AllSources());
+}
+
+LocationBarViewMac::~LocationBarViewMac() {
+ // Disconnect from cell in case it outlives us.
+ [[field_ cell] clearDecorations];
+}
+
+void LocationBarViewMac::ShowFirstRunBubble(FirstRun::BubbleType bubble_type) {
+ // We need the browser window to be shown before we can show the bubble, but
+ // we get called before that's happened.
+ Task* task = first_run_bubble_.NewRunnableMethod(
+ &LocationBarViewMac::ShowFirstRunBubbleInternal, bubble_type);
+ MessageLoop::current()->PostTask(FROM_HERE, task);
+}
+
+void LocationBarViewMac::ShowFirstRunBubbleInternal(
+ FirstRun::BubbleType bubble_type) {
+ if (!field_ || ![field_ window])
+ return;
+
+ // The first run bubble's left edge should line up with the left edge of the
+ // omnibox. This is different from other bubbles, which line up at a point
+ // set by their top arrow. Because the BaseBubbleController adjusts the
+ // window origin left to account for the arrow spacing, the first run bubble
+ // moves the window origin right by this spacing, so that the
+ // BaseBubbleController will move it back to the correct position.
+ const NSPoint kOffset = NSMakePoint(
+ info_bubble::kBubbleArrowXOffset + info_bubble::kBubbleArrowWidth/2.0,
+ kFirstRunBubbleYOffset);
+ [FirstRunBubbleController showForView:field_ offset:kOffset profile:profile_];
+}
+
+std::wstring LocationBarViewMac::GetInputString() const {
+ return location_input_;
+}
+
+void LocationBarViewMac::SetSuggestedText(const string16& text) {
+ edit_view_->SetSuggestText(
+ edit_view_->model()->UseVerbatimInstant() ? string16() : text);
+}
+
+WindowOpenDisposition LocationBarViewMac::GetWindowOpenDisposition() const {
+ return disposition_;
+}
+
+PageTransition::Type LocationBarViewMac::GetPageTransition() const {
+ return transition_;
+}
+
+void LocationBarViewMac::AcceptInput() {
+ WindowOpenDisposition disposition =
+ event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
+ edit_view_->model()->AcceptInput(disposition, false);
+}
+
+void LocationBarViewMac::FocusLocation(bool select_all) {
+ edit_view_->FocusLocation(select_all);
+}
+
+void LocationBarViewMac::FocusSearch() {
+ edit_view_->SetForcedQuery();
+}
+
+void LocationBarViewMac::UpdateContentSettingsIcons() {
+ if (RefreshContentSettingsDecorations()) {
+ [field_ updateCursorAndToolTipRects];
+ [field_ setNeedsDisplay:YES];
+ }
+}
+
+void LocationBarViewMac::UpdatePageActions() {
+ size_t count_before = page_action_decorations_.size();
+ RefreshPageActionDecorations();
+ Layout();
+ if (page_action_decorations_.size() != count_before) {
+ NotificationService::current()->Notify(
+ NotificationType::EXTENSION_PAGE_ACTION_COUNT_CHANGED,
+ Source<LocationBar>(this),
+ NotificationService::NoDetails());
+ }
+}
+
+void LocationBarViewMac::InvalidatePageActions() {
+ size_t count_before = page_action_decorations_.size();
+ DeletePageActionDecorations();
+ Layout();
+ if (page_action_decorations_.size() != count_before) {
+ NotificationService::current()->Notify(
+ NotificationType::EXTENSION_PAGE_ACTION_COUNT_CHANGED,
+ Source<LocationBar>(this),
+ NotificationService::NoDetails());
+ }
+}
+
+void LocationBarViewMac::SaveStateToContents(TabContents* contents) {
+ // TODO(shess): Why SaveStateToContents vs SaveStateToTab?
+ edit_view_->SaveStateToTab(contents);
+}
+
+void LocationBarViewMac::Update(const TabContents* contents,
+ bool should_restore_state) {
+ bool star_enabled = browser_defaults::bookmarks_enabled &&
+ [field_ isEditable] && !toolbar_model_->input_in_progress();
+ command_updater_->UpdateCommandEnabled(IDC_BOOKMARK_PAGE, star_enabled);
+ star_decoration_->SetVisible(star_enabled);
+ RefreshPageActionDecorations();
+ RefreshContentSettingsDecorations();
+ // AutocompleteEditView restores state if the tab is non-NULL.
+ edit_view_->Update(should_restore_state ? contents : NULL);
+ OnChanged();
+}
+
+void LocationBarViewMac::OnAutocompleteWillClosePopup() {
+ if (!update_instant_)
+ return;
+
+ InstantController* controller = browser_->instant();
+ if (controller && !controller->commit_on_mouse_up())
+ controller->DestroyPreviewContents();
+ SetSuggestedText(string16());
+}
+
+void LocationBarViewMac::OnAutocompleteLosingFocus(gfx::NativeView unused) {
+ SetSuggestedText(string16());
+
+ InstantController* instant = browser_->instant();
+ if (!instant)
+ return;
+
+ if (!instant->is_active() || !instant->GetPreviewContents())
+ return;
+
+ // If |IsMouseDownFromActivate()| returns false, the RenderWidgetHostView did
+ // not receive a mouseDown event. Therefore, we should destroy the preview.
+ // Otherwise, the RWHV was clicked, so we commit the preview.
+ if (!instant->IsMouseDownFromActivate())
+ instant->DestroyPreviewContents();
+ else if (instant->IsShowingInstant())
+ instant->SetCommitOnMouseUp();
+ else
+ instant->CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST);
+}
+
+void LocationBarViewMac::OnAutocompleteWillAccept() {
+ update_instant_ = false;
+}
+
+bool LocationBarViewMac::OnCommitSuggestedText(const std::wstring& typed_text) {
+ return edit_view_->CommitSuggestText();
+}
+
+void LocationBarViewMac::OnSetSuggestedSearchText(
+ const string16& suggested_text) {
+ SetSuggestedText(suggested_text);
+}
+
+void LocationBarViewMac::OnPopupBoundsChanged(const gfx::Rect& bounds) {
+ InstantController* instant = browser_->instant();
+ if (instant)
+ instant->SetOmniboxBounds(bounds);
+}
+
+void LocationBarViewMac::OnAutocompleteAccept(const GURL& url,
+ WindowOpenDisposition disposition,
+ PageTransition::Type transition,
+ const GURL& alternate_nav_url) {
+ // WARNING: don't add an early return here. The calls after the if must
+ // happen.
+ if (url.is_valid()) {
+ location_input_ = UTF8ToWide(url.spec());
+ disposition_ = disposition;
+ transition_ = transition;
+
+ if (command_updater_) {
+ if (!alternate_nav_url.is_valid()) {
+ command_updater_->ExecuteCommand(IDC_OPEN_CURRENT_URL);
+ } else {
+ AlternateNavURLFetcher* fetcher =
+ new AlternateNavURLFetcher(alternate_nav_url);
+ // The AlternateNavURLFetcher will listen for the pending navigation
+ // notification that will be issued as a result of the "open URL." It
+ // will automatically install itself into that navigation controller.
+ command_updater_->ExecuteCommand(IDC_OPEN_CURRENT_URL);
+ if (fetcher->state() == AlternateNavURLFetcher::NOT_STARTED) {
+ // I'm not sure this should be reachable, but I'm not also sure enough
+ // that it shouldn't to stick in a NOTREACHED(). In any case, this is
+ // harmless.
+ delete fetcher;
+ } else {
+ // The navigation controller will delete the fetcher.
+ }
+ }
+ }
+ }
+
+ if (browser_->instant())
+ browser_->instant()->DestroyPreviewContents();
+
+ update_instant_ = true;
+}
+
+void LocationBarViewMac::OnChanged() {
+ // Update the location-bar icon.
+ const int resource_id = edit_view_->GetIcon();
+ NSImage* image = AutocompleteEditViewMac::ImageForResource(resource_id);
+ location_icon_decoration_->SetImage(image);
+ ev_bubble_decoration_->SetImage(image);
+ Layout();
+
+ InstantController* instant = browser_->instant();
+ string16 suggested_text;
+ if (update_instant_ && instant && GetTabContents()) {
+ if (edit_view_->model()->user_input_in_progress() &&
+ edit_view_->model()->popup_model()->IsOpen()) {
+ instant->Update
+ (browser_->GetSelectedTabContentsWrapper(),
+ edit_view_->model()->CurrentMatch(),
+ WideToUTF16(edit_view_->GetText()),
+ edit_view_->model()->UseVerbatimInstant(),
+ &suggested_text);
+ if (!instant->IsShowingInstant())
+ edit_view_->model()->FinalizeInstantQuery(std::wstring());
+ } else {
+ instant->DestroyPreviewContents();
+ edit_view_->model()->FinalizeInstantQuery(std::wstring());
+ }
+ }
+
+ SetSuggestedText(suggested_text);
+}
+
+void LocationBarViewMac::OnSelectionBoundsChanged() {
+ NOTIMPLEMENTED();
+}
+
+void LocationBarViewMac::OnInputInProgress(bool in_progress) {
+ toolbar_model_->set_input_in_progress(in_progress);
+ Update(NULL, false);
+}
+
+void LocationBarViewMac::OnSetFocus() {
+ // Update the keyword and search hint states.
+ OnChanged();
+}
+
+void LocationBarViewMac::OnKillFocus() {
+ // Do nothing.
+}
+
+SkBitmap LocationBarViewMac::GetFavIcon() const {
+ NOTIMPLEMENTED();
+ return SkBitmap();
+}
+
+std::wstring LocationBarViewMac::GetTitle() const {
+ NOTIMPLEMENTED();
+ return std::wstring();
+}
+
+void LocationBarViewMac::Revert() {
+ edit_view_->RevertAll();
+}
+
+// TODO(pamg): Change all these, here and for other platforms, to size_t.
+int LocationBarViewMac::PageActionCount() {
+ return static_cast<int>(page_action_decorations_.size());
+}
+
+int LocationBarViewMac::PageActionVisibleCount() {
+ int result = 0;
+ for (size_t i = 0; i < page_action_decorations_.size(); ++i) {
+ if (page_action_decorations_[i]->IsVisible())
+ ++result;
+ }
+ return result;
+}
+
+TabContents* LocationBarViewMac::GetTabContents() const {
+ return browser_->GetSelectedTabContents();
+}
+
+PageActionDecoration* LocationBarViewMac::GetPageActionDecoration(
+ ExtensionAction* page_action) {
+ DCHECK(page_action);
+ for (size_t i = 0; i < page_action_decorations_.size(); ++i) {
+ if (page_action_decorations_[i]->page_action() == page_action)
+ return page_action_decorations_[i];
+ }
+ // If |page_action| is the browser action of an extension, no element in
+ // |page_action_decorations_| will match.
+ NOTREACHED();
+ return NULL;
+}
+
+void LocationBarViewMac::SetPreviewEnabledPageAction(
+ ExtensionAction* page_action, bool preview_enabled) {
+ DCHECK(page_action);
+ TabContents* contents = GetTabContents();
+ if (!contents)
+ return;
+ RefreshPageActionDecorations();
+ Layout();
+
+ PageActionDecoration* decoration = GetPageActionDecoration(page_action);
+ DCHECK(decoration);
+ if (!decoration)
+ return;
+
+ decoration->set_preview_enabled(preview_enabled);
+ decoration->UpdateVisibility(contents,
+ GURL(WideToUTF8(toolbar_model_->GetText())));
+}
+
+NSPoint LocationBarViewMac::GetPageActionBubblePoint(
+ ExtensionAction* page_action) {
+ PageActionDecoration* decoration = GetPageActionDecoration(page_action);
+ if (!decoration)
+ return NSZeroPoint;
+
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ NSRect frame = [cell frameForDecoration:decoration inFrame:[field_ bounds]];
+ DCHECK(!NSIsEmptyRect(frame));
+ if (NSIsEmptyRect(frame))
+ return NSZeroPoint;
+
+ NSPoint bubble_point = decoration->GetBubblePointInFrame(frame);
+ return [field_ convertPoint:bubble_point toView:nil];
+}
+
+NSRect LocationBarViewMac::GetBlockedPopupRect() const {
+ const size_t kPopupIndex = CONTENT_SETTINGS_TYPE_POPUPS;
+ const LocationBarDecoration* decoration =
+ content_setting_decorations_[kPopupIndex];
+ if (!decoration || !decoration->IsVisible())
+ return NSZeroRect;
+
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect frame = [cell frameForDecoration:decoration
+ inFrame:[field_ bounds]];
+ return [field_ convertRect:frame toView:nil];
+}
+
+ExtensionAction* LocationBarViewMac::GetPageAction(size_t index) {
+ if (index < page_action_decorations_.size())
+ return page_action_decorations_[index]->page_action();
+ NOTREACHED();
+ return NULL;
+}
+
+ExtensionAction* LocationBarViewMac::GetVisiblePageAction(size_t index) {
+ size_t current = 0;
+ for (size_t i = 0; i < page_action_decorations_.size(); ++i) {
+ if (page_action_decorations_[i]->IsVisible()) {
+ if (current == index)
+ return page_action_decorations_[i]->page_action();
+
+ ++current;
+ }
+ }
+
+ NOTREACHED();
+ return NULL;
+}
+
+void LocationBarViewMac::TestPageActionPressed(size_t index) {
+ DCHECK_LT(index, page_action_decorations_.size());
+ if (index < page_action_decorations_.size())
+ page_action_decorations_[index]->OnMousePressed(NSZeroRect);
+}
+
+void LocationBarViewMac::SetEditable(bool editable) {
+ [field_ setEditable:editable ? YES : NO];
+ star_decoration_->SetVisible(browser_defaults::bookmarks_enabled &&
+ editable && !toolbar_model_->input_in_progress());
+ UpdatePageActions();
+ Layout();
+}
+
+bool LocationBarViewMac::IsEditable() {
+ return [field_ isEditable] ? true : false;
+}
+
+void LocationBarViewMac::SetStarred(bool starred) {
+ star_decoration_->SetStarred(starred);
+
+ // TODO(shess): The field-editor frame and cursor rects should not
+ // change, here.
+ [field_ updateCursorAndToolTipRects];
+ [field_ resetFieldEditorFrameIfNeeded];
+ [field_ setNeedsDisplay:YES];
+}
+
+NSPoint LocationBarViewMac::GetBookmarkBubblePoint() const {
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect frame = [cell frameForDecoration:star_decoration_.get()
+ inFrame:[field_ bounds]];
+ const NSPoint point = star_decoration_->GetBubblePointInFrame(frame);
+ return [field_ convertPoint:point toView:nil];
+}
+
+NSPoint LocationBarViewMac::GetPageInfoBubblePoint() const {
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ if (ev_bubble_decoration_->IsVisible()) {
+ const NSRect frame = [cell frameForDecoration:ev_bubble_decoration_.get()
+ inFrame:[field_ bounds]];
+ const NSPoint point = ev_bubble_decoration_->GetBubblePointInFrame(frame);
+ return [field_ convertPoint:point toView:nil];
+ } else {
+ const NSRect frame =
+ [cell frameForDecoration:location_icon_decoration_.get()
+ inFrame:[field_ bounds]];
+ const NSPoint point =
+ location_icon_decoration_->GetBubblePointInFrame(frame);
+ return [field_ convertPoint:point toView:nil];
+ }
+}
+
+NSImage* LocationBarViewMac::GetKeywordImage(const std::wstring& keyword) {
+ const TemplateURL* template_url =
+ profile_->GetTemplateURLModel()->GetTemplateURLForKeyword(keyword);
+ if (template_url && template_url->IsExtensionKeyword()) {
+ const SkBitmap& bitmap = profile_->GetExtensionsService()->
+ GetOmniboxIcon(template_url->GetExtensionId());
+ return gfx::SkBitmapToNSImage(bitmap);
+ }
+
+ return AutocompleteEditViewMac::ImageForResource(IDR_OMNIBOX_SEARCH);
+}
+
+void LocationBarViewMac::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ switch (type.value) {
+ case NotificationType::EXTENSION_PAGE_ACTION_VISIBILITY_CHANGED: {
+ TabContents* contents = GetTabContents();
+ if (Details<TabContents>(contents) != details)
+ return;
+
+ [field_ updateCursorAndToolTipRects];
+ [field_ setNeedsDisplay:YES];
+ break;
+ }
+ default:
+ NOTREACHED() << "Unexpected notification";
+ break;
+ }
+}
+
+void LocationBarViewMac::PostNotification(NSString* notification) {
+ [[NSNotificationCenter defaultCenter] postNotificationName:notification
+ object:[NSValue valueWithPointer:this]];
+}
+
+bool LocationBarViewMac::RefreshContentSettingsDecorations() {
+ const bool input_in_progress = toolbar_model_->input_in_progress();
+ TabContents* tab_contents =
+ input_in_progress ? NULL : browser_->GetSelectedTabContents();
+ bool icons_updated = false;
+ for (size_t i = 0; i < content_setting_decorations_.size(); ++i) {
+ icons_updated |=
+ content_setting_decorations_[i]->UpdateFromTabContents(tab_contents);
+ }
+ return icons_updated;
+}
+
+void LocationBarViewMac::DeletePageActionDecorations() {
+ // TODO(shess): Deleting these decorations could result in the cell
+ // refering to them before things are laid out again. Meanwhile, at
+ // least fail safe.
+ [[field_ cell] clearDecorations];
+
+ page_action_decorations_.reset();
+}
+
+void LocationBarViewMac::RefreshPageActionDecorations() {
+ if (!IsEditable()) {
+ DeletePageActionDecorations();
+ return;
+ }
+
+ ExtensionsService* service = profile_->GetExtensionsService();
+ if (!service)
+ return;
+
+ std::vector<ExtensionAction*> page_actions;
+ for (size_t i = 0; i < service->extensions()->size(); ++i) {
+ if (service->extensions()->at(i)->page_action())
+ page_actions.push_back(service->extensions()->at(i)->page_action());
+ }
+
+ // On startup we sometimes haven't loaded any extensions. This makes sure
+ // we catch up when the extensions (and any Page Actions) load.
+ if (page_actions.size() != page_action_decorations_.size()) {
+ DeletePageActionDecorations(); // Delete the old views (if any).
+
+ for (size_t i = 0; i < page_actions.size(); ++i) {
+ page_action_decorations_.push_back(
+ new PageActionDecoration(this, profile_, page_actions[i]));
+ }
+ }
+
+ if (page_action_decorations_.empty())
+ return;
+
+ TabContents* contents = GetTabContents();
+ if (!contents)
+ return;
+
+ GURL url = GURL(WideToUTF8(toolbar_model_->GetText()));
+ for (size_t i = 0; i < page_action_decorations_.size(); ++i) {
+ page_action_decorations_[i]->UpdateVisibility(
+ toolbar_model_->input_in_progress() ? NULL : contents, url);
+ }
+}
+
+// TODO(shess): This function should over time grow to closely match
+// the views Layout() function.
+void LocationBarViewMac::Layout() {
+ AutocompleteTextFieldCell* cell = [field_ cell];
+
+ // Reset the left-hand decorations.
+ // TODO(shess): Shortly, this code will live somewhere else, like in
+ // the constructor. I am still wrestling with how best to deal with
+ // right-hand decorations, which are not a static set.
+ [cell clearDecorations];
+ [cell addLeftDecoration:location_icon_decoration_.get()];
+ [cell addLeftDecoration:selected_keyword_decoration_.get()];
+ [cell addLeftDecoration:ev_bubble_decoration_.get()];
+ [cell addRightDecoration:star_decoration_.get()];
+
+ // Note that display order is right to left.
+ for (size_t i = 0; i < page_action_decorations_.size(); ++i) {
+ [cell addRightDecoration:page_action_decorations_[i]];
+ }
+ for (size_t i = 0; i < content_setting_decorations_.size(); ++i) {
+ [cell addRightDecoration:content_setting_decorations_[i]];
+ }
+
+ [cell addRightDecoration:keyword_hint_decoration_.get()];
+
+ // By default only the location icon is visible.
+ location_icon_decoration_->SetVisible(true);
+ selected_keyword_decoration_->SetVisible(false);
+ ev_bubble_decoration_->SetVisible(false);
+ keyword_hint_decoration_->SetVisible(false);
+
+ // Get the keyword to use for keyword-search and hinting.
+ const std::wstring keyword(edit_view_->model()->keyword());
+ std::wstring short_name;
+ bool is_extension_keyword = false;
+ if (!keyword.empty()) {
+ short_name = profile_->GetTemplateURLModel()->
+ GetKeywordShortName(keyword, &is_extension_keyword);
+ }
+
+ const bool is_keyword_hint = edit_view_->model()->is_keyword_hint();
+
+ if (!keyword.empty() && !is_keyword_hint) {
+ // Switch from location icon to keyword mode.
+ location_icon_decoration_->SetVisible(false);
+ selected_keyword_decoration_->SetVisible(true);
+ selected_keyword_decoration_->SetKeyword(short_name, is_extension_keyword);
+ selected_keyword_decoration_->SetImage(GetKeywordImage(keyword));
+ } else if (toolbar_model_->GetSecurityLevel() == ToolbarModel::EV_SECURE) {
+ // Switch from location icon to show the EV bubble instead.
+ location_icon_decoration_->SetVisible(false);
+ ev_bubble_decoration_->SetVisible(true);
+
+ std::wstring label(toolbar_model_->GetEVCertName());
+ ev_bubble_decoration_->SetFullLabel(base::SysWideToNSString(label));
+ } else if (!keyword.empty() && is_keyword_hint) {
+ keyword_hint_decoration_->SetKeyword(short_name, is_extension_keyword);
+ keyword_hint_decoration_->SetVisible(true);
+ }
+
+ // These need to change anytime the layout changes.
+ // TODO(shess): Anytime the field editor might have changed, the
+ // cursor rects almost certainly should have changed. The tooltips
+ // might change even when the rects don't change.
+ [field_ resetFieldEditorFrameIfNeeded];
+ [field_ updateCursorAndToolTipRects];
+
+ [field_ setNeedsDisplay:YES];
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h b/chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h
new file mode 100644
index 0000000..920d4d3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h
@@ -0,0 +1,46 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_LOCATION_ICON_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_LOCATION_ICON_DECORATION_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/ui/cocoa/location_bar/image_decoration.h"
+
+class LocationBarViewMac;
+
+// LocationIconDecoration is used to display an icon to the left of
+// the address.
+
+class LocationIconDecoration : public ImageDecoration {
+ public:
+ explicit LocationIconDecoration(LocationBarViewMac* owner);
+ virtual ~LocationIconDecoration();
+
+ // Allow dragging the current URL.
+ virtual bool IsDraggable();
+ virtual NSPasteboard* GetDragPasteboard();
+ virtual NSImage* GetDragImage() { return GetImage(); }
+ virtual NSRect GetDragImageFrame(NSRect frame) {
+ return GetDrawRectInFrame(frame);
+ }
+
+ // Get the point where the page info bubble should point within the
+ // decoration's frame, in the |owner_|'s coordinates.
+ NSPoint GetBubblePointInFrame(NSRect frame);
+
+ // Show the page info panel on click.
+ virtual bool OnMousePressed(NSRect frame);
+ virtual bool AcceptsMousePress() { return true; }
+
+ private:
+ // The location bar view that owns us.
+ LocationBarViewMac* owner_;
+
+ DISALLOW_COPY_AND_ASSIGN(LocationIconDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_LOCATION_ICON_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/location_icon_decoration.mm b/chrome/browser/ui/cocoa/location_bar/location_icon_decoration.mm
new file mode 100644
index 0000000..808a45f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/location_icon_decoration.mm
@@ -0,0 +1,72 @@
+// Copyright (c) 2010 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.
+
+#include <cmath>
+
+#import "chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h"
+
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+#import "third_party/mozilla/NSPasteboard+Utils.h"
+
+// The info-bubble point should look like it points to the bottom of the lock
+// icon. Determined with Pixie.app.
+const CGFloat kBubblePointYOffset = 2.0;
+
+LocationIconDecoration::LocationIconDecoration(LocationBarViewMac* owner)
+ : owner_(owner) {
+}
+LocationIconDecoration::~LocationIconDecoration() {
+}
+
+bool LocationIconDecoration::IsDraggable() {
+ // Without a tab it will be impossible to get the information needed
+ // to perform a drag.
+ if (!owner_->GetTabContents())
+ return false;
+
+ // Do not drag if the user has been editing the location bar, or the
+ // location bar is at the NTP.
+ if (owner_->location_entry()->IsEditingOrEmpty())
+ return false;
+
+ return true;
+}
+
+NSPasteboard* LocationIconDecoration::GetDragPasteboard() {
+ TabContents* tab = owner_->GetTabContents();
+ DCHECK(tab); // See |IsDraggable()|.
+
+ NSString* url = base::SysUTF8ToNSString(tab->GetURL().spec());
+ NSString* title = base::SysUTF16ToNSString(tab->GetTitle());
+
+ NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
+ [pboard declareURLPasteboardWithAdditionalTypes:[NSArray array]
+ owner:nil];
+ [pboard setDataForURL:url title:title];
+ return pboard;
+}
+
+NSPoint LocationIconDecoration::GetBubblePointInFrame(NSRect frame) {
+ const NSRect draw_frame = GetDrawRectInFrame(frame);
+ return NSMakePoint(NSMidX(draw_frame),
+ NSMaxY(draw_frame) - kBubblePointYOffset);
+}
+
+bool LocationIconDecoration::OnMousePressed(NSRect frame) {
+ // Do not show page info if the user has been editing the location
+ // bar, or the location bar is at the NTP.
+ if (owner_->location_entry()->IsEditingOrEmpty())
+ return true;
+
+ TabContents* tab = owner_->GetTabContents();
+ NavigationEntry* nav_entry = tab->controller().GetActiveEntry();
+ if (!nav_entry) {
+ NOTREACHED();
+ return true;
+ }
+ tab->ShowPageInfo(nav_entry->url(), nav_entry->ssl(), true);
+ return true;
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.h b/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.h
new file mode 100644
index 0000000..c549a49
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.h
@@ -0,0 +1,17 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_OMNIBOX_POPUP_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_OMNIBOX_POPUP_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// The content view for the omnibox popup. Supports up to two subviews (the
+// AutocompleteMatrix containing autocomplete results and (optionally) an
+// InstantOptInView.
+@interface OmniboxPopupView : NSView
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_OMNIBOX_POPUP_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.mm b/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.mm
new file mode 100644
index 0000000..ef479e1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.mm
@@ -0,0 +1,43 @@
+// Copyright (c) 2010 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/ui/cocoa/location_bar/omnibox_popup_view.h"
+
+#include "base/logging.h"
+
+@implementation OmniboxPopupView
+
+// If there is only one subview, it is sized to fill all available space. If
+// there are two subviews, the second subview is placed at the bottom of the
+// view, and the first subview is sized to fill all remaining space.
+- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
+ NSArray* subviews = [self subviews];
+ if ([subviews count] == 0)
+ return;
+
+ DCHECK_LE([subviews count], 2U);
+
+ NSRect availableSpace = [self bounds];
+
+ if ([subviews count] >= 2) {
+ NSView* instantView = [subviews objectAtIndex:1];
+ CGFloat height = NSHeight([instantView frame]);
+ NSRect instantFrame = availableSpace;
+ instantFrame.size.height = height;
+
+ availableSpace.origin.y = height;
+ availableSpace.size.height -= height;
+ [instantView setFrame:instantFrame];
+ }
+
+ if ([subviews count] >= 1) {
+ NSView* matrixView = [subviews objectAtIndex:0];
+ if (NSHeight(availableSpace) < 0)
+ availableSpace.size.height = 0;
+
+ [matrixView setFrame:availableSpace];
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view_unittest.mm b/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view_unittest.mm
new file mode 100644
index 0000000..ac4be55
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view_unittest.mm
@@ -0,0 +1,68 @@
+// Copyright (c) 2010 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 "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.h"
+
+namespace {
+
+class OmniboxPopupViewTest : public CocoaTest {
+ public:
+ OmniboxPopupViewTest() {
+ NSRect content_frame = [[test_window() contentView] frame];
+ scoped_nsobject<OmniboxPopupView> view(
+ [[OmniboxPopupView alloc] initWithFrame:content_frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ OmniboxPopupView* view_; // Weak. Owned by the view hierarchy.
+};
+
+// Tests display, add/remove.
+TEST_VIEW(OmniboxPopupViewTest, view_);
+
+// A single subview should completely fill the popup view.
+TEST_F(OmniboxPopupViewTest, ResizeWithOneSubview) {
+ scoped_nsobject<NSView> subview1([[NSView alloc] initWithFrame:NSZeroRect]);
+
+ // Adding the subview should not change its frame.
+ [view_ addSubview:subview1];
+ EXPECT_TRUE(NSEqualRects(NSZeroRect, [subview1 frame]));
+
+ // Resizing the popup view should also resize the subview.
+ [view_ setFrame:NSMakeRect(0, 0, 100, 100)];
+ EXPECT_TRUE(NSEqualRects([view_ bounds], [subview1 frame]));
+}
+
+TEST_F(OmniboxPopupViewTest, ResizeWithTwoSubviews) {
+ const CGFloat height = 50;
+ NSRect initial = NSMakeRect(0, 0, 100, height);
+
+ scoped_nsobject<NSView> subview1([[NSView alloc] initWithFrame:NSZeroRect]);
+ scoped_nsobject<NSView> subview2([[NSView alloc] initWithFrame:initial]);
+ [view_ addSubview:subview1];
+ [view_ addSubview:subview2];
+
+ // Resize the popup view to be much larger than height. |subview2|'s height
+ // should stay the same, and |subview1| should resize to fill all available
+ // space.
+ [view_ setFrame:NSMakeRect(0, 0, 300, 4 * height)];
+ EXPECT_EQ(NSWidth([view_ frame]), NSWidth([subview1 frame]));
+ EXPECT_EQ(NSWidth([view_ frame]), NSWidth([subview2 frame]));
+ EXPECT_EQ(height, NSHeight([subview2 frame]));
+ EXPECT_EQ(NSHeight([view_ frame]),
+ NSHeight([subview1 frame]) + NSHeight([subview2 frame]));
+
+ // Now resize the popup view to be smaller than height. |subview2|'s height
+ // should stay the same, and |subview1|'s height should be zero, not negative.
+ [view_ setFrame:NSMakeRect(0, 0, 300, height - 10)];
+ EXPECT_EQ(NSWidth([view_ frame]), NSWidth([subview1 frame]));
+ EXPECT_EQ(NSWidth([view_ frame]), NSWidth([subview2 frame]));
+ EXPECT_EQ(0, NSHeight([subview1 frame]));
+ EXPECT_EQ(height, NSHeight([subview2 frame]));
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/page_action_decoration.h b/chrome/browser/ui/cocoa/location_bar/page_action_decoration.h
new file mode 100644
index 0000000..07cd94d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/page_action_decoration.h
@@ -0,0 +1,119 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_PAGE_ACTION_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_PAGE_ACTION_DECORATION_H_
+#pragma once
+
+#include "chrome/browser/extensions/image_loading_tracker.h"
+#import "chrome/browser/ui/cocoa/location_bar/image_decoration.h"
+#include "googleurl/src/gurl.h"
+
+class ExtensionAction;
+@class ExtensionActionContextMenu;
+class LocationBarViewMac;
+class Profile;
+class TabContents;
+
+// PageActionDecoration is used to display the icon for a given Page
+// Action and notify the extension when the icon is clicked.
+
+class PageActionDecoration : public ImageDecoration,
+ public ImageLoadingTracker::Observer,
+ public NotificationObserver {
+ public:
+ PageActionDecoration(LocationBarViewMac* owner,
+ Profile* profile,
+ ExtensionAction* page_action);
+ virtual ~PageActionDecoration();
+
+ ExtensionAction* page_action() { return page_action_; }
+ int current_tab_id() { return current_tab_id_; }
+ void set_preview_enabled(bool enabled) { preview_enabled_ = enabled; }
+ bool preview_enabled() const { return preview_enabled_; }
+
+ // Overridden from |ImageLoadingTracker::Observer|.
+ virtual void OnImageLoaded(
+ SkBitmap* image, ExtensionResource resource, int index);
+
+ // Called to notify the Page Action that it should determine whether
+ // to be visible or hidden. |contents| is the TabContents that is
+ // active, |url| is the current page URL.
+ void UpdateVisibility(TabContents* contents, const GURL& url);
+
+ // Sets the tooltip for this Page Action image.
+ void SetToolTip(NSString* tooltip);
+ void SetToolTip(std::string tooltip);
+
+ // Get the point where extension info bubbles should point within
+ // the given decoration frame.
+ NSPoint GetBubblePointInFrame(NSRect frame);
+
+ // Overridden from |LocationBarDecoration|
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+ virtual bool AcceptsMousePress() { return true; }
+ virtual bool OnMousePressed(NSRect frame);
+ virtual NSString* GetToolTip();
+ virtual NSMenu* GetMenu();
+
+ protected:
+ // For unit testing only.
+ PageActionDecoration() : owner_(NULL),
+ profile_(NULL),
+ page_action_(NULL),
+ tracker_(this),
+ current_tab_id_(-1),
+ preview_enabled_(false) {}
+
+ private:
+ // Overridden from NotificationObserver:
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ // The location bar view that owns us.
+ LocationBarViewMac* owner_;
+
+ // The current profile (not owned by us).
+ Profile* profile_;
+
+ // The Page Action that this view represents. The Page Action is not
+ // owned by us, it resides in the extension of this particular
+ // profile.
+ ExtensionAction* page_action_;
+
+ // A cache of images the Page Actions might need to show, mapped by
+ // path.
+ typedef std::map<std::string, SkBitmap> PageActionMap;
+ PageActionMap page_action_icons_;
+
+ // The object that is waiting for the image loading to complete
+ // asynchronously.
+ ImageLoadingTracker tracker_;
+
+ // The tab id we are currently showing the icon for.
+ int current_tab_id_;
+
+ // The URL we are currently showing the icon for.
+ GURL current_url_;
+
+ // The string to show for a tooltip.
+ scoped_nsobject<NSString> tooltip_;
+
+ // The context menu for the Page Action.
+ scoped_nsobject<ExtensionActionContextMenu> menu_;
+
+ // This is used for post-install visual feedback. The page_action
+ // icon is briefly shown even if it hasn't been enabled by its
+ // extension.
+ bool preview_enabled_;
+
+ // Used to register for notifications received by
+ // NotificationObserver.
+ NotificationRegistrar registrar_;
+
+ DISALLOW_COPY_AND_ASSIGN(PageActionDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_PAGE_ACTION_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/page_action_decoration.mm b/chrome/browser/ui/cocoa/location_bar/page_action_decoration.mm
new file mode 100644
index 0000000..610815c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/page_action_decoration.mm
@@ -0,0 +1,251 @@
+// Copyright (c) 2010 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.
+
+#include <cmath>
+
+#import "chrome/browser/ui/cocoa/location_bar/page_action_decoration.h"
+
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/extensions/extension_browser_event_router.h"
+#include "chrome/browser/extensions/extensions_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h"
+#import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+#include "chrome/common/extensions/extension_action.h"
+#include "chrome/common/extensions/extension_resource.h"
+#include "skia/ext/skia_utils_mac.h"
+
+namespace {
+
+// Distance to offset the bubble pointer from the bottom of the max
+// icon area of the decoration. This makes the popup's upper border
+// 2px away from the omnibox's lower border (matches omnibox popup
+// upper border).
+const CGFloat kBubblePointYOffset = 2.0;
+
+} // namespace
+
+PageActionDecoration::PageActionDecoration(
+ LocationBarViewMac* owner,
+ Profile* profile,
+ ExtensionAction* page_action)
+ : owner_(NULL),
+ profile_(profile),
+ page_action_(page_action),
+ tracker_(this),
+ current_tab_id_(-1),
+ preview_enabled_(false) {
+ DCHECK(profile);
+ const Extension* extension = profile->GetExtensionsService()->
+ GetExtensionById(page_action->extension_id(), false);
+ DCHECK(extension);
+
+ // Load all the icons declared in the manifest. This is the contents of the
+ // icons array, plus the default_icon property, if any.
+ std::vector<std::string> icon_paths(*page_action->icon_paths());
+ if (!page_action_->default_icon_path().empty())
+ icon_paths.push_back(page_action_->default_icon_path());
+
+ for (std::vector<std::string>::iterator iter = icon_paths.begin();
+ iter != icon_paths.end(); ++iter) {
+ tracker_.LoadImage(extension, extension->GetResource(*iter),
+ gfx::Size(Extension::kPageActionIconMaxSize,
+ Extension::kPageActionIconMaxSize),
+ ImageLoadingTracker::DONT_CACHE);
+ }
+
+ registrar_.Add(this, NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE,
+ Source<Profile>(profile_));
+
+ // We set the owner last of all so that we can determine whether we are in
+ // the process of initializing this class or not.
+ owner_ = owner;
+}
+
+PageActionDecoration::~PageActionDecoration() {}
+
+// Always |kPageActionIconMaxSize| wide. |ImageDecoration| draws the
+// image centered.
+CGFloat PageActionDecoration::GetWidthForSpace(CGFloat width) {
+ return Extension::kPageActionIconMaxSize;
+}
+
+// Either notify listeners or show a popup depending on the Page
+// Action.
+bool PageActionDecoration::OnMousePressed(NSRect frame) {
+ if (current_tab_id_ < 0) {
+ NOTREACHED() << "No current tab.";
+ // We don't want other code to try and handle this click. Returning true
+ // prevents this by indicating that we handled it.
+ return true;
+ }
+
+ if (page_action_->HasPopup(current_tab_id_)) {
+ // Anchor popup at the bottom center of the page action icon.
+ AutocompleteTextField* field = owner_->GetAutocompleteTextField();
+ NSPoint anchor = GetBubblePointInFrame(frame);
+ anchor = [field convertPoint:anchor toView:nil];
+
+ const GURL popup_url(page_action_->GetPopupUrl(current_tab_id_));
+ [ExtensionPopupController showURL:popup_url
+ inBrowser:BrowserList::GetLastActive()
+ anchoredAt:anchor
+ arrowLocation:info_bubble::kTopRight
+ devMode:NO];
+ } else {
+ ExtensionBrowserEventRouter::GetInstance()->PageActionExecuted(
+ profile_, page_action_->extension_id(), page_action_->id(),
+ current_tab_id_, current_url_.spec(),
+ 1);
+ }
+ return true;
+}
+
+void PageActionDecoration::OnImageLoaded(
+ SkBitmap* image, ExtensionResource resource, int index) {
+ // We loaded icons()->size() icons, plus one extra if the Page Action had
+ // a default icon.
+ int total_icons = static_cast<int>(page_action_->icon_paths()->size());
+ if (!page_action_->default_icon_path().empty())
+ total_icons++;
+ DCHECK(index < total_icons);
+
+ // Map the index of the loaded image back to its name. If we ever get an
+ // index greater than the number of icons, it must be the default icon.
+ if (image) {
+ if (index < static_cast<int>(page_action_->icon_paths()->size()))
+ page_action_icons_[page_action_->icon_paths()->at(index)] = *image;
+ else
+ page_action_icons_[page_action_->default_icon_path()] = *image;
+ }
+
+ // If we have no owner, that means this class is still being constructed and
+ // we should not UpdatePageActions, since it leads to the PageActions being
+ // destroyed again and new ones recreated (causing an infinite loop).
+ if (owner_)
+ owner_->UpdatePageActions();
+}
+
+void PageActionDecoration::UpdateVisibility(TabContents* contents,
+ const GURL& url) {
+ // Save this off so we can pass it back to the extension when the action gets
+ // executed. See PageActionDecoration::OnMousePressed.
+ current_tab_id_ = contents ? ExtensionTabUtil::GetTabId(contents) : -1;
+ current_url_ = url;
+
+ bool visible = contents &&
+ (preview_enabled_ || page_action_->GetIsVisible(current_tab_id_));
+ if (visible) {
+ SetToolTip(page_action_->GetTitle(current_tab_id_));
+
+ // Set the image.
+ // It can come from three places. In descending order of priority:
+ // - The developer can set it dynamically by path or bitmap. It will be in
+ // page_action_->GetIcon().
+ // - The developer can set it dynamically by index. It will be in
+ // page_action_->GetIconIndex().
+ // - It can be set in the manifest by path. It will be in page_action_->
+ // default_icon_path().
+
+ // First look for a dynamically set bitmap.
+ SkBitmap skia_icon = page_action_->GetIcon(current_tab_id_);
+ if (skia_icon.isNull()) {
+ int icon_index = page_action_->GetIconIndex(current_tab_id_);
+ std::string icon_path = (icon_index < 0) ?
+ page_action_->default_icon_path() :
+ page_action_->icon_paths()->at(icon_index);
+ if (!icon_path.empty()) {
+ PageActionMap::iterator iter = page_action_icons_.find(icon_path);
+ if (iter != page_action_icons_.end())
+ skia_icon = iter->second;
+ }
+ }
+ if (!skia_icon.isNull()) {
+ SetImage(gfx::SkBitmapToNSImage(skia_icon));
+ } else if (!GetImage()) {
+ // During install the action can be displayed before the icons
+ // have come in. Rather than deal with this in multiple places,
+ // provide a placeholder image. This will be replaced when an
+ // icon comes in.
+ const NSSize default_size = NSMakeSize(Extension::kPageActionIconMaxSize,
+ Extension::kPageActionIconMaxSize);
+ SetImage([[NSImage alloc] initWithSize:default_size]);
+ }
+ }
+
+ if (IsVisible() != visible) {
+ SetVisible(visible);
+ NotificationService::current()->Notify(
+ NotificationType::EXTENSION_PAGE_ACTION_VISIBILITY_CHANGED,
+ Source<ExtensionAction>(page_action_),
+ Details<TabContents>(contents));
+ }
+}
+
+void PageActionDecoration::SetToolTip(NSString* tooltip) {
+ tooltip_.reset([tooltip retain]);
+}
+
+void PageActionDecoration::SetToolTip(std::string tooltip) {
+ SetToolTip(tooltip.empty() ? nil : base::SysUTF8ToNSString(tooltip));
+}
+
+NSString* PageActionDecoration::GetToolTip() {
+ return tooltip_.get();
+}
+
+NSPoint PageActionDecoration::GetBubblePointInFrame(NSRect frame) {
+ // This is similar to |ImageDecoration::GetDrawRectInFrame()|,
+ // except that code centers the image, which can differ in size
+ // between actions. This centers the maximum image size, so the
+ // point will consistently be at the same y position. x position is
+ // easier (the middle of the centered image is the middle of the
+ // frame).
+ const CGFloat delta_height =
+ NSHeight(frame) - Extension::kPageActionIconMaxSize;
+ const CGFloat bottom_inset = std::ceil(delta_height / 2.0);
+
+ // Return a point just below the bottom of the maximal drawing area.
+ return NSMakePoint(NSMidX(frame),
+ NSMaxY(frame) - bottom_inset + kBubblePointYOffset);
+}
+
+NSMenu* PageActionDecoration::GetMenu() {
+ if (!profile_)
+ return nil;
+ ExtensionsService* service = profile_->GetExtensionsService();
+ if (!service)
+ return nil;
+ const Extension* extension = service->GetExtensionById(
+ page_action_->extension_id(), false);
+ DCHECK(extension);
+ if (!extension)
+ return nil;
+ menu_.reset([[ExtensionActionContextMenu alloc]
+ initWithExtension:extension
+ profile:profile_
+ extensionAction:page_action_]);
+
+ return menu_.get();
+}
+
+void PageActionDecoration::Observe(
+ NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ switch (type.value) {
+ case NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE: {
+ ExtensionPopupController* popup = [ExtensionPopupController popup];
+ if (popup && ![popup isClosing])
+ [popup close];
+
+ break;
+ }
+ default:
+ NOTREACHED() << "Unexpected notification";
+ break;
+ }
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h b/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h
new file mode 100644
index 0000000..3c9cf309
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h
@@ -0,0 +1,42 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_SELECTED_KEYWORD_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_SELECTED_KEYWORD_DECORATION_H_
+#pragma once
+
+#include <string>
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/ui/cocoa/location_bar/bubble_decoration.h"
+
+class SelectedKeywordDecoration : public BubbleDecoration {
+ public:
+ SelectedKeywordDecoration(NSFont* font);
+
+ // Calculates appropriate full and partial label strings based on
+ // inputs.
+ void SetKeyword(const std::wstring& keyword, bool is_extension_keyword);
+
+ // Determines what combination of labels and image will best fit
+ // within |width|, makes those current for |BubbleDecoration|, and
+ // return the resulting width.
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+
+ void SetImage(NSImage* image);
+
+ private:
+ friend class SelectedKeywordDecorationTest;
+ FRIEND_TEST_ALL_PREFIXES(SelectedKeywordDecorationTest,
+ UsesPartialKeywordIfNarrow);
+
+ scoped_nsobject<NSImage> search_image_;
+ scoped_nsobject<NSString> full_string_;
+ scoped_nsobject<NSString> partial_string_;
+
+ DISALLOW_COPY_AND_ASSIGN(SelectedKeywordDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_SELECTED_KEYWORD_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.mm b/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.mm
new file mode 100644
index 0000000..0bdd8e15
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.mm
@@ -0,0 +1,73 @@
+// Copyright (c) 2010 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/ui/cocoa/location_bar/selected_keyword_decoration.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/utf_string_conversions.h"
+#import "chrome/browser/autocomplete/autocomplete_edit_view_mac.h"
+#include "chrome/browser/location_bar_util.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#include "grit/theme_resources.h"
+#include "grit/generated_resources.h"
+
+SelectedKeywordDecoration::SelectedKeywordDecoration(NSFont* font)
+ : BubbleDecoration(font) {
+ search_image_.reset([AutocompleteEditViewMac::ImageForResource(
+ IDR_KEYWORD_SEARCH_MAGNIFIER) retain]);
+
+ // Matches the color of the highlighted line in the popup.
+ NSColor* background_color = [NSColor selectedControlColor];
+
+ // Match focus ring's inner color.
+ NSColor* border_color =
+ [[NSColor keyboardFocusIndicatorColor] colorWithAlphaComponent:0.5];
+ SetColors(border_color, background_color, [NSColor blackColor]);
+}
+
+CGFloat SelectedKeywordDecoration::GetWidthForSpace(CGFloat width) {
+ const CGFloat full_width =
+ GetWidthForImageAndLabel(search_image_, full_string_);
+ if (full_width <= width) {
+ BubbleDecoration::SetImage(search_image_);
+ SetLabel(full_string_);
+ return full_width;
+ }
+
+ BubbleDecoration::SetImage(nil);
+ const CGFloat no_image_width = GetWidthForImageAndLabel(nil, full_string_);
+ if (no_image_width <= width || !partial_string_) {
+ SetLabel(full_string_);
+ return no_image_width;
+ }
+
+ SetLabel(partial_string_);
+ return GetWidthForImageAndLabel(nil, partial_string_);
+}
+
+void SelectedKeywordDecoration::SetKeyword(const std::wstring& short_name,
+ bool is_extension_keyword) {
+ const std::wstring min_name(
+ location_bar_util::CalculateMinString(short_name));
+ const int message_id = is_extension_keyword ?
+ IDS_OMNIBOX_EXTENSION_KEYWORD_TEXT : IDS_OMNIBOX_KEYWORD_TEXT;
+
+ // The text will be like "Search <name>:". "<name>" is a parameter
+ // derived from |short_name|.
+ full_string_.reset(
+ [l10n_util::GetNSStringF(message_id, WideToUTF16(short_name)) copy]);
+
+ if (min_name.empty()) {
+ partial_string_.reset();
+ } else {
+ partial_string_.reset(
+ [l10n_util::GetNSStringF(message_id, WideToUTF16(min_name)) copy]);
+ }
+}
+
+void SelectedKeywordDecoration::SetImage(NSImage* image) {
+ if (image != search_image_)
+ search_image_.reset([image retain]);
+ BubbleDecoration::SetImage(image);
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration_unittest.mm b/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration_unittest.mm
new file mode 100644
index 0000000..5536fda
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration_unittest.mm
@@ -0,0 +1,64 @@
+// Copyright (c) 2010 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 <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+
+namespace {
+
+// A wide width which should fit everything.
+const CGFloat kWidth(300.0);
+
+// A narrow width for tests which test things that don't fit.
+const CGFloat kNarrowWidth(5.0);
+
+} // namespace
+
+class SelectedKeywordDecorationTest : public CocoaTest {
+ public:
+ SelectedKeywordDecorationTest()
+ : decoration_([NSFont userFontOfSize:12]) {
+ }
+
+ SelectedKeywordDecoration decoration_;
+};
+
+// Test that the cell correctly chooses the partial keyword if there's
+// not enough room.
+TEST_F(SelectedKeywordDecorationTest, UsesPartialKeywordIfNarrow) {
+
+ const std::wstring kKeyword(L"Engine");
+ NSString* const kFullString = @"Search Engine:";
+ NSString* const kPartialString = @"Search En\u2026:"; // ellipses
+
+ decoration_.SetKeyword(kKeyword, false);
+
+ // Wide width chooses the full string and image.
+ const CGFloat all_width = decoration_.GetWidthForSpace(kWidth);
+ EXPECT_TRUE(decoration_.image_);
+ EXPECT_NSEQ(kFullString, decoration_.label_);
+
+ // If not enough space to include the image, uses exactly the full
+ // string.
+ const CGFloat full_width = decoration_.GetWidthForSpace(all_width - 5.0);
+ EXPECT_LT(full_width, all_width);
+ EXPECT_FALSE(decoration_.image_);
+ EXPECT_NSEQ(kFullString, decoration_.label_);
+
+ // Narrow width chooses the partial string.
+ const CGFloat partial_width = decoration_.GetWidthForSpace(kNarrowWidth);
+ EXPECT_LT(partial_width, full_width);
+ EXPECT_FALSE(decoration_.image_);
+ EXPECT_NSEQ(kPartialString, decoration_.label_);
+
+ // Narrow doesn't choose partial string if there is not one.
+ decoration_.partial_string_.reset();
+ decoration_.GetWidthForSpace(kNarrowWidth);
+ EXPECT_FALSE(decoration_.image_);
+ EXPECT_NSEQ(kFullString, decoration_.label_);
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/star_decoration.h b/chrome/browser/ui/cocoa/location_bar/star_decoration.h
new file mode 100644
index 0000000..0d12104
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/star_decoration.h
@@ -0,0 +1,44 @@
+// Copyright (c) 2010 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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_LOCATION_BAR_STAR_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_STAR_DECORATION_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/ui/cocoa/location_bar/image_decoration.h"
+
+class CommandUpdater;
+
+// Star icon on the right side of the field.
+
+class StarDecoration : public ImageDecoration {
+ public:
+ explicit StarDecoration(CommandUpdater* command_updater);
+ virtual ~StarDecoration();
+
+ // Sets the image and tooltip based on |starred|.
+ void SetStarred(bool starred);
+
+ // Get the point where the bookmark bubble should point within the
+ // decoration's frame.
+ NSPoint GetBubblePointInFrame(NSRect frame);
+
+ // Implement |LocationBarDecoration|.
+ virtual bool AcceptsMousePress() { return true; }
+ virtual bool OnMousePressed(NSRect frame);
+ virtual NSString* GetToolTip();
+
+ private:
+ // For bringing up bookmark bar.
+ CommandUpdater* command_updater_; // Weak, owned by Browser.
+
+ // The string to show for a tooltip.
+ scoped_nsobject<NSString> tooltip_;
+
+ DISALLOW_COPY_AND_ASSIGN(StarDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_STAR_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/star_decoration.mm b/chrome/browser/ui/cocoa/location_bar/star_decoration.mm
new file mode 100644
index 0000000..2ac3450
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/star_decoration.mm
@@ -0,0 +1,53 @@
+// Copyright (c) 2010 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/ui/cocoa/location_bar/star_decoration.h"
+
+#include "app/l10n_util_mac.h"
+#include "chrome/app/chrome_command_ids.h"
+#import "chrome/browser/autocomplete/autocomplete_edit_view_mac.h"
+#include "chrome/browser/command_updater.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+
+namespace {
+
+// The info-bubble point should look like it points to the point
+// between the star's lower tips. The popup should be where the
+// Omnibox popup ends up (2px below field). Determined via Pixie.app
+// magnification.
+const CGFloat kStarPointYOffset = 2.0;
+
+} // namespace
+
+StarDecoration::StarDecoration(CommandUpdater* command_updater)
+ : command_updater_(command_updater) {
+ SetVisible(true);
+ SetStarred(false);
+}
+
+StarDecoration::~StarDecoration() {
+}
+
+void StarDecoration::SetStarred(bool starred) {
+ const int image_id = starred ? IDR_STAR_LIT : IDR_STAR;
+ const int tip_id = starred ? IDS_TOOLTIP_STARRED : IDS_TOOLTIP_STAR;
+ SetImage(AutocompleteEditViewMac::ImageForResource(image_id));
+ tooltip_.reset([l10n_util::GetNSStringWithFixup(tip_id) retain]);
+}
+
+NSPoint StarDecoration::GetBubblePointInFrame(NSRect frame) {
+ const NSRect draw_frame = GetDrawRectInFrame(frame);
+ return NSMakePoint(NSMidX(draw_frame),
+ NSMaxY(draw_frame) - kStarPointYOffset);
+}
+
+bool StarDecoration::OnMousePressed(NSRect frame) {
+ command_updater_->ExecuteCommand(IDC_BOOKMARK_PAGE);
+ return true;
+}
+
+NSString* StarDecoration::GetToolTip() {
+ return tooltip_.get();
+}