diff options
author | ben@chromium.org <ben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-12-01 16:34:49 +0000 |
---|---|---|
committer | ben@chromium.org <ben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-12-01 16:34:49 +0000 |
commit | 7d791652c7ede4209a2014d885148e2713f49bce (patch) | |
tree | c26baf12593bed381c631b81c736106809d46b44 /chrome/browser/ui/cocoa/location_bar | |
parent | 3b94427c99bdf12836fd455eeb1499fdde511e26 (diff) | |
download | chromium_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')
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(); +} |