summaryrefslogtreecommitdiffstats
path: root/chrome/browser/ui/cocoa/wrench_menu
diff options
context:
space:
mode:
authorrsesek@chromium.org <rsesek@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-01-14 17:30:19 +0000
committerrsesek@chromium.org <rsesek@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-01-14 17:30:19 +0000
commitfdf9c477b8a496ba3493067a11ddfccb13ea9984 (patch)
tree958f874f70fc0089cca78d2564b09997e81561ae /chrome/browser/ui/cocoa/wrench_menu
parent7d5070ed99759ffb6312d552a15989e3634ee623 (diff)
downloadchromium_src-fdf9c477b8a496ba3493067a11ddfccb13ea9984.zip
chromium_src-fdf9c477b8a496ba3493067a11ddfccb13ea9984.tar.gz
chromium_src-fdf9c477b8a496ba3493067a11ddfccb13ea9984.tar.bz2
[Mac] Move all the related files for the Wrench menu into a subdir of c/b/u/c/.
BUG=none TEST=compile and trybots Review URL: http://codereview.chromium.org/6283005 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@71453 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/ui/cocoa/wrench_menu')
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button.h43
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button.mm118
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button_unittest.mm117
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.h25
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.mm15
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view_unittest.mm45
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell.h19
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell.mm48
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell_unittest.mm51
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.h72
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.mm222
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller_unittest.mm84
12 files changed, 859 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button.h b/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button.h
new file mode 100644
index 0000000..5a46342
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button.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_WRENCH_MENU_MENU_TRACKED_BUTTON_H_
+#define CHROME_BROWSER_UI_COCOA_WRENCH_MENU_MENU_TRACKED_BUTTON_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// A MenuTrackedButton is meant to be used whenever a button is placed inside
+// the custom view of an NSMenuItem. If the user opens the menu in a non-sticky
+// fashion (i.e. clicks, holds, and drags) and then releases the mouse over
+// a MenuTrackedButton, it will |-performClick:| itself.
+//
+// To create the hover state effects, there are two code paths. When the menu
+// is opened sticky, a tracking rect produces mouse entered/exit events that
+// allow for setting the cell's highlight property. When in a drag cycle,
+// however, the only event received is |-mouseDragged:|. Therefore, a
+// delayed selector is scheduled to poll the mouse location after each drag
+// event. This checks if the user is still over the button after the drag
+// events stop being sent, indicating either the user is hovering without
+// movement or that the mouse is no longer over the receiver.
+@interface MenuTrackedButton : NSButton {
+ @private
+ // If the button received a |-mouseEntered:| event. This short-circuits the
+ // custom drag tracking logic.
+ BOOL didEnter_;
+
+ // Whether or not the user is in a click-drag-release event sequence. If so
+ // and this receives a |-mouseUp:|, then this will click itself.
+ BOOL tracking_;
+
+ // In order to get hover effects when the menu is sticky-opened, a tracking
+ // rect needs to be installed on the button.
+ NSTrackingRectTag trackingTag_;
+}
+
+@property (nonatomic, readonly, getter=isTracking) BOOL tracking;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_WRENCH_MENU_MENU_TRACKED_BUTTON_H_
diff --git a/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button.mm b/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button.mm
new file mode 100644
index 0000000..3f65b36
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button.mm
@@ -0,0 +1,118 @@
+// 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/wrench_menu/menu_tracked_button.h"
+
+#include <cmath>
+
+@interface MenuTrackedButton (Private)
+- (void)doHighlight:(BOOL)highlight;
+- (void)checkMouseInRect;
+- (NSRect)insetBounds;
+- (BOOL)shouldHighlightOnHover;
+@end
+
+@implementation MenuTrackedButton
+
+@synthesize tracking = tracking_;
+
+- (void)updateTrackingAreas {
+ [super updateTrackingAreas];
+ [self removeTrackingRect:trackingTag_];
+ trackingTag_ = [self addTrackingRect:NSInsetRect([self bounds], 1, 1)
+ owner:self
+ userData:NULL
+ assumeInside:NO];
+}
+
+- (void)viewDidMoveToWindow {
+ [self updateTrackingAreas];
+ [self doHighlight:NO];
+}
+
+- (void)mouseEntered:(NSEvent*)theEvent {
+ if (!tracking_) {
+ didEnter_ = YES;
+ }
+ [self doHighlight:YES];
+ [super mouseEntered:theEvent];
+}
+
+- (void)mouseExited:(NSEvent*)theEvent {
+ didEnter_ = NO;
+ tracking_ = NO;
+ [self doHighlight:NO];
+ [super mouseExited:theEvent];
+}
+
+- (void)mouseDragged:(NSEvent*)theEvent {
+ tracking_ = !didEnter_;
+
+ NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil];
+ BOOL highlight = NSPointInRect(point, [self insetBounds]);
+ [self doHighlight:highlight];
+
+ // If tracking in non-sticky mode, poll the mouse cursor to see if it is still
+ // over the button and thus needs to be highlighted. The delay is the
+ // smallest that still produces the effect while minimizing jank. Smaller
+ // values make the selector fire too close to immediately/now for the mouse to
+ // have moved off the receiver, and larger values produce lag.
+ if (tracking_ && [self shouldHighlightOnHover]) {
+ [self performSelector:@selector(checkMouseInRect)
+ withObject:nil
+ afterDelay:0.05
+ inModes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]];
+ }
+ [super mouseDragged:theEvent];
+}
+
+- (void)mouseUp:(NSEvent*)theEvent {
+ [self doHighlight:NO];
+ if (!tracking_) {
+ return [super mouseUp:theEvent];
+ }
+ [self performClick:self];
+ tracking_ = NO;
+}
+
+- (void)doHighlight:(BOOL)highlight {
+ if (![self shouldHighlightOnHover]) {
+ return;
+ }
+ [[self cell] setHighlighted:highlight];
+ [self setNeedsDisplay];
+}
+
+// Checks if the user's current mouse location is over this button. If it is,
+// the user is merely hovering here. If it is not, then disable the highlight.
+// If the menu is opened in non-sticky mode, the button does not receive enter/
+// exit mouse events and thus polling is necessary.
+- (void)checkMouseInRect {
+ NSPoint point = [NSEvent mouseLocation];
+ point = [[self window] convertScreenToBase:point];
+ point = [self convertPoint:point fromView:nil];
+ if (!NSPointInRect(point, [self insetBounds])) {
+ [self doHighlight:NO];
+ }
+}
+
+// Returns the bounds of the receiver slightly inset to avoid highlighting both
+// buttons in a pair that overlap.
+- (NSRect)insetBounds {
+ return NSInsetRect([self bounds], 2, 1);
+}
+
+- (BOOL)shouldHighlightOnHover {
+ // Apple does not define NSAppKitVersionNumber10_5 when using the 10.5 SDK.
+ // The Internets have come up with this solution.
+ #ifndef NSAppKitVersionNumber10_5
+ #define NSAppKitVersionNumber10_5 949
+ #endif
+
+ // There's a cell drawing bug in 10.5 that was fixed on 10.6. Hover states
+ // look terrible due to this, so disable highlighting on 10.5.
+ return std::floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_5;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button_unittest.mm b/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button_unittest.mm
new file mode 100644
index 0000000..16a529a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button_unittest.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 <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/wrench_menu/menu_tracked_button.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// This test does not test what you'd think it does. Testing around event
+// tracking run loops is probably not worh the effort when the size of the
+// helper MakeEvent() is larger than the class being tested. If we ever figure
+// out a good way to test event tracking, this should be revisited.
+
+@interface MenuTrackedButtonTestReceiver : NSObject {
+ @public
+ BOOL didThat_;
+}
+- (void)doThat:(id)sender;
+@end
+@implementation MenuTrackedButtonTestReceiver
+- (void)doThat:(id)sender {
+ didThat_ = YES;
+}
+@end
+
+
+class MenuTrackedButtonTest : public CocoaTest {
+ public:
+ MenuTrackedButtonTest() : event_number_(0) {}
+
+ void SetUp() {
+ listener_.reset([[MenuTrackedButtonTestReceiver alloc] init]);
+ button_.reset(
+ [[MenuTrackedButton alloc] initWithFrame:NSMakeRect(10, 10, 50, 50)]);
+ [[test_window() contentView] addSubview:button()];
+ [button_ setTarget:listener()];
+ [button_ setAction:@selector(doThat:)];
+ }
+
+ // Creates an event of |type|, with |location| in test_window()'s coordinates.
+ NSEvent* MakeEvent(NSEventType type, NSPoint location) {
+ NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
+ location = [test_window() convertBaseToScreen:location];
+ if (type == NSMouseEntered || type == NSMouseExited) {
+ return [NSEvent enterExitEventWithType:type
+ location:location
+ modifierFlags:0
+ timestamp:now
+ windowNumber:[test_window() windowNumber]
+ context:nil
+ eventNumber:event_number_++
+ trackingNumber:0
+ userData:nil];
+ } else {
+ return [NSEvent mouseEventWithType:type
+ location:location
+ modifierFlags:0
+ timestamp:now
+ windowNumber:[test_window() windowNumber]
+ context:nil
+ eventNumber:event_number_++
+ clickCount:1
+ pressure:1.0];
+ }
+ }
+
+ MenuTrackedButtonTestReceiver* listener() { return listener_.get(); }
+ NSButton* button() { return button_.get(); }
+
+ scoped_nsobject<MenuTrackedButtonTestReceiver> listener_;
+ scoped_nsobject<MenuTrackedButton> button_;
+ NSInteger event_number_;
+};
+
+// User mouses over and then off.
+TEST_F(MenuTrackedButtonTest, DISABLED_EnterExit) {
+ [NSApp postEvent:MakeEvent(NSMouseEntered, NSMakePoint(11, 11)) atStart:YES];
+ [NSApp postEvent:MakeEvent(NSMouseExited, NSMakePoint(9, 9)) atStart:YES];
+ EXPECT_FALSE(listener()->didThat_);
+}
+
+// User mouses over, clicks, drags, and exits.
+TEST_F(MenuTrackedButtonTest, DISABLED_EnterDragExit) {
+ [NSApp postEvent:MakeEvent(NSMouseEntered, NSMakePoint(11, 11)) atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseDown, NSMakePoint(12, 12)) atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseDragged, NSMakePoint(13, 11))
+ atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseDragged, NSMakePoint(13, 10))
+ atStart:YES];
+ [NSApp postEvent:MakeEvent(NSMouseExited, NSMakePoint(13, 9)) atStart:YES];
+ EXPECT_FALSE(listener()->didThat_);
+}
+
+// User mouses over, clicks, drags, and releases.
+TEST_F(MenuTrackedButtonTest, DISABLED_EnterDragUp) {
+ [NSApp postEvent:MakeEvent(NSMouseEntered, NSMakePoint(11, 11)) atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseDown, NSMakePoint(12, 12)) atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseDragged, NSMakePoint(13, 13))
+ atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseUp, NSMakePoint(14, 14)) atStart:YES];
+ EXPECT_TRUE(listener()->didThat_);
+}
+
+// User drags in and releases.
+TEST_F(MenuTrackedButtonTest, DISABLED_DragUp) {
+ [NSApp postEvent:MakeEvent(NSLeftMouseDragged, NSMakePoint(11, 11))
+ atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseDragged, NSMakePoint(12, 12))
+ atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseUp, NSMakePoint(13, 13))
+ atStart:YES];
+ EXPECT_TRUE(listener()->didThat_);
+}
diff --git a/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.h b/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.h
new file mode 100644
index 0000000..7a5ab4c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.h
@@ -0,0 +1,25 @@
+// 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_WRENCH_MENU_MENU_TRACKED_ROOT_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_WRENCH_MENU_MENU_TRACKED_ROOT_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// An instance of MenuTrackedRootView should be the root of the view hierarchy
+// of the custom view of NSMenuItems. If the user opens the menu in a non-
+// sticky fashion (i.e. clicks, holds, and drags) and then releases the mouse
+// over the menu item, it will cancel tracking on the |[menuItem_ menu]|.
+@interface MenuTrackedRootView : NSView {
+ @private
+ // The menu item whose custom view's root view is an instance of this class.
+ NSMenuItem* menuItem_; // weak
+}
+
+@property (assign, nonatomic) NSMenuItem* menuItem;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_WRENCH_MENU_MENU_TRACKED_ROOT_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.mm b/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.mm
new file mode 100644
index 0000000..96b3b26
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.mm
@@ -0,0 +1,15 @@
+// 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/wrench_menu/menu_tracked_root_view.h"
+
+@implementation MenuTrackedRootView
+
+@synthesize menuItem = menuItem_;
+
+- (void)mouseUp:(NSEvent*)theEvent {
+ [[menuItem_ menu] cancelTracking];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view_unittest.mm b/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view_unittest.mm
new file mode 100644
index 0000000..a853824
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view_unittest.mm
@@ -0,0 +1,45 @@
+// 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/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+class MenuTrackedRootViewTest : public CocoaTest {
+ public:
+ void SetUp() {
+ CocoaTest::SetUp();
+ view_.reset([[MenuTrackedRootView alloc] init]);
+ }
+
+ scoped_nsobject<MenuTrackedRootView> view_;
+};
+
+TEST_F(MenuTrackedRootViewTest, MouseUp) {
+ id menu = [OCMockObject mockForClass:[NSMenu class]];
+ [[menu expect] cancelTracking];
+
+ id menuItem = [OCMockObject mockForClass:[NSMenuItem class]];
+ [[[menuItem stub] andReturn:menu] menu];
+
+ [view_ setMenuItem:menuItem];
+ NSEvent* event = [NSEvent mouseEventWithType:NSLeftMouseUp
+ location:NSMakePoint(42, 42)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:[test_window() windowNumber]
+ context:nil
+ eventNumber:1
+ clickCount:1
+ pressure:1.0];
+ [view_ mouseUp:event];
+
+ [menu verify];
+ [menuItem verify];
+}
diff --git a/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell.h b/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell.h
new file mode 100644
index 0000000..4c40059
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell.h
@@ -0,0 +1,19 @@
+// 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_WRENCH_MENU_WRENCH_MENU_BUTTON_CELL_H_
+#define CHROME_BROWSER_UI_COCOA_WRENCH_MENU_WRENCH_MENU_BUTTON_CELL_H_
+
+#import <Cocoa/Cocoa.h>
+
+// The WrenchMenuButtonCell overrides drawing the background gradient to use
+// the same colors as NSSmallSquareBezelStyle but as a smooth gradient, rather
+// than two blocks of colors. This also uses the blue menu highlight color for
+// the pressed state.
+@interface WrenchMenuButtonCell : NSButtonCell {
+}
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_WRENCH_MENU_WRENCH_MENU_BUTTON_CELL_H_
diff --git a/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell.mm b/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell.mm
new file mode 100644
index 0000000..d042989
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell.mm
@@ -0,0 +1,48 @@
+// 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/wrench_menu/wrench_menu_button_cell.h"
+
+#include "base/scoped_nsobject.h"
+
+@implementation WrenchMenuButtonCell
+
+- (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView {
+ [NSGraphicsContext saveGraphicsState];
+
+ // Inset the rect to match the appearance of the layout of interface builder.
+ // The bounding rect of buttons is actually larger than the display rect shown
+ // there.
+ frame = NSInsetRect(frame, 0.0, 1.0);
+
+ // Stroking the rect gives a weak stroke. Filling and insetting gives a
+ // strong, un-anti-aliased border.
+ [[NSColor colorWithDeviceWhite:0.663 alpha:1.0] set];
+ NSRectFill(frame);
+ frame = NSInsetRect(frame, 1.0, 1.0);
+
+ // The default state should be a subtle gray gradient.
+ if (![self isHighlighted]) {
+ NSColor* end = [NSColor colorWithDeviceWhite:0.922 alpha:1.0];
+ scoped_nsobject<NSGradient> gradient(
+ [[NSGradient alloc] initWithStartingColor:[NSColor whiteColor]
+ endingColor:end]);
+ [gradient drawInRect:frame angle:90.0];
+ } else {
+ // |+selectedMenuItemColor| appears to be a gradient, so just filling the
+ // rect with that color produces the desired effect.
+ [[NSColor selectedMenuItemColor] set];
+ NSRectFill(frame);
+ }
+
+ [NSGraphicsContext restoreGraphicsState];
+}
+
+- (NSBackgroundStyle)interiorBackgroundStyle {
+ if ([self isHighlighted])
+ return NSBackgroundStyleDark;
+ return [super interiorBackgroundStyle];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell_unittest.mm b/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell_unittest.mm
new file mode 100644
index 0000000..fc9e403
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell_unittest.mm
@@ -0,0 +1,51 @@
+// 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 "chrome/app/chrome_command_ids.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/wrench_menu/wrench_menu_button_cell.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface TestWrenchMenuButton : NSButton
+@end
+@implementation TestWrenchMenuButton
++ (Class)cellClass {
+ return [WrenchMenuButtonCell class];
+}
+@end
+
+namespace {
+
+class WrenchMenuButtonCellTest : public CocoaTest {
+ public:
+ void SetUp() {
+ CocoaTest::SetUp();
+
+ NSRect frame = NSMakeRect(10, 10, 50, 19);
+ button_.reset([[TestWrenchMenuButton alloc] initWithFrame:frame]);
+ [button_ setBezelStyle:NSSmallSquareBezelStyle];
+ [[button_ cell] setControlSize:NSSmallControlSize];
+ [button_ setTitle:@"Allays"];
+ [button_ setButtonType:NSMomentaryPushInButton];
+ }
+
+ scoped_nsobject<NSButton> button_;
+};
+
+TEST_F(WrenchMenuButtonCellTest, Draw) {
+ ASSERT_TRUE(button_.get());
+ [[test_window() contentView] addSubview:button_.get()];
+ [button_ setNeedsDisplay:YES];
+}
+
+TEST_F(WrenchMenuButtonCellTest, DrawHighlight) {
+ ASSERT_TRUE(button_.get());
+ [[test_window() contentView] addSubview:button_.get()];
+ [button_ highlight:YES];
+ [button_ setNeedsDisplay:YES];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.h b/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.h
new file mode 100644
index 0000000..ec68120
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.h
@@ -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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_WRENCH_MENU_WRENCH_MENU_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_WRENCH_MENU_WRENCH_MENU_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/mac/cocoa_protocols.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/menu_controller.h"
+
+@class MenuTrackedRootView;
+@class ToolbarController;
+class WrenchMenuModel;
+
+namespace WrenchMenuControllerInternal {
+class ZoomLevelObserver;
+} // namespace WrenchMenuControllerInternal
+
+// The Wrench menu has a creative layout, with buttons in menu items. There is
+// a cross-platform model for this special menu, but on the Mac it's easier to
+// get spacing and alignment precisely right using a NIB. To do that, we
+// subclass the generic MenuController implementation and special-case the two
+// items that require specific layout and load them from the NIB.
+//
+// This object is instantiated in Toolbar.xib and is configured by the
+// ToolbarController.
+@interface WrenchMenuController : MenuController<NSMenuDelegate> {
+ IBOutlet MenuTrackedRootView* editItem_;
+ IBOutlet NSButton* editCut_;
+ IBOutlet NSButton* editCopy_;
+ IBOutlet NSButton* editPaste_;
+
+ IBOutlet MenuTrackedRootView* zoomItem_;
+ IBOutlet NSButton* zoomPlus_;
+ IBOutlet NSButton* zoomDisplay_;
+ IBOutlet NSButton* zoomMinus_;
+ IBOutlet NSButton* zoomFullScreen_;
+
+ scoped_ptr<WrenchMenuControllerInternal::ZoomLevelObserver> observer_;
+}
+
+// Designated initializer; called within the NIB.
+- (id)init;
+
+// Used to dispatch commands from the Wrench menu. The custom items within the
+// menu cannot be hooked up directly to First Responder because the window in
+// which the controls reside is not the BrowserWindowController, but a
+// NSCarbonMenuWindow; this screws up the typical |-commandDispatch:| system.
+- (IBAction)dispatchWrenchMenuCommand:(id)sender;
+
+// Returns the weak reference to the WrenchMenuModel.
+- (WrenchMenuModel*)wrenchMenuModel;
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////
+
+@interface WrenchMenuController (UnitTesting)
+// |-dispatchWrenchMenuCommand:| calls this after it has determined the tag of
+// the sender. The default implementation executes the command on the outermost
+// run loop using |-performSelector...withDelay:|. This is not desirable in
+// unit tests because it's hard to test around run loops in a deterministic
+// manner. To avoid those headaches, tests should provide an alternative
+// implementation.
+- (void)dispatchCommandInternal:(NSInteger)tag;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_WRENCH_MENU_WRENCH_MENU_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.mm b/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.mm
new file mode 100644
index 0000000..7720154
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.mm
@@ -0,0 +1,222 @@
+// Copyright (c) 2011 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/wrench_menu/wrench_menu_controller.h"
+
+#include "app/l10n_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/background_page_tracker.h"
+#include "chrome/browser/metrics/user_metrics.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+#import "chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/toolbar/wrench_menu_model.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/notification_source.h"
+#include "chrome/common/notification_type.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+#include "ui/base/models/menu_model.h"
+
+@interface WrenchMenuController (Private)
+- (void)adjustPositioning;
+- (void)performCommandDispatch:(NSNumber*)tag;
+- (NSButton*)zoomDisplay;
+@end
+
+namespace WrenchMenuControllerInternal {
+
+class ZoomLevelObserver : public NotificationObserver {
+ public:
+ explicit ZoomLevelObserver(WrenchMenuController* controller)
+ : controller_(controller) {
+ registrar_.Add(this, NotificationType::ZOOM_LEVEL_CHANGED,
+ NotificationService::AllSources());
+ }
+
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ DCHECK_EQ(type.value, NotificationType::ZOOM_LEVEL_CHANGED);
+ WrenchMenuModel* wrenchMenuModel = [controller_ wrenchMenuModel];
+ wrenchMenuModel->UpdateZoomControls();
+ const string16 level =
+ wrenchMenuModel->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY);
+ [[controller_ zoomDisplay] setTitle:SysUTF16ToNSString(level)];
+ }
+
+ private:
+ NotificationRegistrar registrar_;
+ WrenchMenuController* controller_; // Weak; owns this.
+};
+
+} // namespace WrenchMenuControllerInternal
+
+@implementation WrenchMenuController
+
+- (id)init {
+ if ((self = [super init])) {
+ observer_.reset(new WrenchMenuControllerInternal::ZoomLevelObserver(self));
+ }
+ return self;
+}
+
+- (void)addItemToMenu:(NSMenu*)menu
+ atIndex:(NSInteger)index
+ fromModel:(ui::MenuModel*)model
+ modelIndex:(int)modelIndex {
+ // Non-button item types should be built as normal items.
+ ui::MenuModel::ItemType type = model->GetTypeAt(modelIndex);
+ if (type != ui::MenuModel::TYPE_BUTTON_ITEM) {
+ [super addItemToMenu:menu
+ atIndex:index
+ fromModel:model
+ modelIndex:modelIndex];
+ return;
+ }
+
+ // Handle the special-cased menu items.
+ int command_id = model->GetCommandIdAt(modelIndex);
+ scoped_nsobject<NSMenuItem> customItem(
+ [[NSMenuItem alloc] initWithTitle:@""
+ action:nil
+ keyEquivalent:@""]);
+ switch (command_id) {
+ case IDC_EDIT_MENU:
+ DCHECK(editItem_);
+ [customItem setView:editItem_];
+ [editItem_ setMenuItem:customItem];
+ break;
+ case IDC_ZOOM_MENU:
+ DCHECK(zoomItem_);
+ [customItem setView:zoomItem_];
+ [zoomItem_ setMenuItem:customItem];
+ break;
+ default:
+ NOTREACHED();
+ break;
+ }
+ [self adjustPositioning];
+ [menu insertItem:customItem.get() atIndex:index];
+}
+
+- (NSMenu*)menu {
+ NSMenu* menu = [super menu];
+ if (![menu delegate]) {
+ [menu setDelegate:self];
+ }
+ return menu;
+}
+
+- (void)menuWillOpen:(NSMenu*)menu {
+ NSString* title = base::SysUTF16ToNSString(
+ [self wrenchMenuModel]->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY));
+ [[zoomItem_ viewWithTag:IDC_ZOOM_PERCENT_DISPLAY] setTitle:title];
+ UserMetrics::RecordAction(UserMetricsAction("ShowAppMenu"));
+
+ NSImage* icon = [self wrenchMenuModel]->browser()->window()->IsFullscreen() ?
+ [NSImage imageNamed:NSImageNameExitFullScreenTemplate] :
+ [NSImage imageNamed:NSImageNameEnterFullScreenTemplate];
+ [zoomFullScreen_ setImage:icon];
+}
+
+- (void)menuDidClose:(NSMenu*)menu {
+ // When the menu is closed, acknowledge the background pages so the badges go
+ // away.
+ BackgroundPageTracker::GetInstance()->AcknowledgeBackgroundPages();
+}
+
+// Used to dispatch commands from the Wrench menu. The custom items within the
+// menu cannot be hooked up directly to First Responder because the window in
+// which the controls reside is not the BrowserWindowController, but a
+// NSCarbonMenuWindow; this screws up the typical |-commandDispatch:| system.
+- (IBAction)dispatchWrenchMenuCommand:(id)sender {
+ NSInteger tag = [sender tag];
+ if (sender == zoomPlus_ || sender == zoomMinus_) {
+ // Do a direct dispatch rather than scheduling on the outermost run loop,
+ // which would not get hit until after the menu had closed.
+ [self performCommandDispatch:[NSNumber numberWithInt:tag]];
+
+ // The zoom buttons should not close the menu if opened sticky.
+ if ([sender respondsToSelector:@selector(isTracking)] &&
+ [sender performSelector:@selector(isTracking)]) {
+ [menu_ cancelTracking];
+ }
+ } else {
+ // The custom views within the Wrench menu are abnormal and keep the menu
+ // open after a target-action. Close the menu manually.
+ [menu_ cancelTracking];
+ [self dispatchCommandInternal:tag];
+ }
+}
+
+- (void)dispatchCommandInternal:(NSInteger)tag {
+ // Executing certain commands from the nested run loop of the menu can lead
+ // to wonky behavior (e.g. http://crbug.com/49716). To avoid this, schedule
+ // the dispatch on the outermost run loop.
+ [self performSelector:@selector(performCommandDispatch:)
+ withObject:[NSNumber numberWithInt:tag]
+ afterDelay:0.0];
+}
+
+// Used to perform the actual dispatch on the outermost runloop.
+- (void)performCommandDispatch:(NSNumber*)tag {
+ [self wrenchMenuModel]->ExecuteCommand([tag intValue]);
+}
+
+- (WrenchMenuModel*)wrenchMenuModel {
+ return static_cast<WrenchMenuModel*>(model_);
+}
+
+// Fit the localized strings into the Cut/Copy/Paste control, then resize the
+// whole menu item accordingly.
+- (void)adjustPositioning {
+ const CGFloat kButtonPadding = 12;
+ CGFloat delta = 0;
+
+ // Go through the three buttons from right-to-left, adjusting the size to fit
+ // the localized strings while keeping them all aligned on their horizontal
+ // edges.
+ const size_t kAdjustViewCount = 3;
+ NSButton* views[kAdjustViewCount] = { editPaste_, editCopy_, editCut_ };
+ for (size_t i = 0; i < kAdjustViewCount; ++i) {
+ NSButton* button = views[i];
+ CGFloat originalWidth = NSWidth([button frame]);
+
+ // Do not let |-sizeToFit| change the height of the button.
+ NSSize size = [button frame].size;
+ [button sizeToFit];
+ size.width = [button frame].size.width + kButtonPadding;
+ [button setFrameSize:size];
+
+ CGFloat newWidth = size.width;
+ delta += newWidth - originalWidth;
+
+ NSRect frame = [button frame];
+ frame.origin.x -= delta;
+ [button setFrame:frame];
+ }
+
+ // Resize the menu item by the total amound the buttons changed so that the
+ // spacing between the buttons and the title remains the same.
+ NSRect itemFrame = [editItem_ frame];
+ itemFrame.size.width += delta;
+ [editItem_ setFrame:itemFrame];
+
+ // Also resize the superview of the buttons, which is an NSView used to slide
+ // when the item title is too big and GTM resizes it.
+ NSRect parentFrame = [[editCut_ superview] frame];
+ parentFrame.size.width += delta;
+ parentFrame.origin.x -= delta;
+ [[editCut_ superview] setFrame:parentFrame];
+}
+
+- (NSButton*)zoomDisplay {
+ return zoomDisplay_;
+}
+
+@end // @implementation WrenchMenuController
diff --git a/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller_unittest.mm b/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller_unittest.mm
new file mode 100644
index 0000000..d3832aa
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller_unittest.mm
@@ -0,0 +1,84 @@
+// 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 "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+#import "chrome/browser/ui/cocoa/view_resizer_pong.h"
+#import "chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.h"
+#include "chrome/browser/ui/toolbar/wrench_menu_model.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// Override to avoid dealing with run loops in the testing environment.
+@implementation WrenchMenuController (UnitTesting)
+- (void)dispatchCommandInternal:(NSInteger)tag {
+ [self wrenchMenuModel]->ExecuteCommand(tag);
+}
+@end
+
+
+namespace {
+
+class MockWrenchMenuModel : public WrenchMenuModel {
+ public:
+ MockWrenchMenuModel() : WrenchMenuModel() {}
+ ~MockWrenchMenuModel() {
+ // This dirty, ugly hack gets around a bug in the test. In
+ // ~WrenchMenuModel(), there's a call to TabstripModel::RemoveObserver(this)
+ // which mysteriously leads to this crash: http://crbug.com/49206 . It
+ // seems that the vector of observers is getting hosed somewhere between
+ // |-[ToolbarController dealloc]| and ~MockWrenchMenuModel(). This line
+ // short-circuits the parent destructor to avoid this crash.
+ tabstrip_model_ = NULL;
+ }
+ MOCK_METHOD1(ExecuteCommand, void(int command_id));
+};
+
+class WrenchMenuControllerTest : public CocoaTest {
+ public:
+ void SetUp() {
+ Browser* browser = helper_.browser();
+ resize_delegate_.reset([[ViewResizerPong alloc] init]);
+ toolbar_controller_.reset(
+ [[ToolbarController alloc] initWithModel:browser->toolbar_model()
+ commands:browser->command_updater()
+ profile:helper_.profile()
+ browser:browser
+ resizeDelegate:resize_delegate_.get()]);
+ EXPECT_TRUE([toolbar_controller_ view]);
+ NSView* parent = [test_window() contentView];
+ [parent addSubview:[toolbar_controller_ view]];
+ }
+
+ WrenchMenuController* controller() {
+ return [toolbar_controller_ wrenchMenuController];
+ }
+
+ BrowserTestHelper helper_;
+ scoped_nsobject<ViewResizerPong> resize_delegate_;
+ MockWrenchMenuModel fake_model_;
+ scoped_nsobject<ToolbarController> toolbar_controller_;
+};
+
+TEST_F(WrenchMenuControllerTest, Initialized) {
+ EXPECT_TRUE([controller() menu]);
+ EXPECT_GE([[controller() menu] numberOfItems], 5);
+}
+
+TEST_F(WrenchMenuControllerTest, DispatchSimple) {
+ scoped_nsobject<NSButton> button([[NSButton alloc] init]);
+ [button setTag:IDC_ZOOM_PLUS];
+
+ // Set fake model to test dispatching.
+ EXPECT_CALL(fake_model_, ExecuteCommand(IDC_ZOOM_PLUS));
+ [controller() setModel:&fake_model_];
+
+ [controller() dispatchWrenchMenuCommand:button.get()];
+}
+
+} // namespace