diff options
author | rsesek@chromium.org <rsesek@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-01-14 17:30:19 +0000 |
---|---|---|
committer | rsesek@chromium.org <rsesek@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-01-14 17:30:19 +0000 |
commit | fdf9c477b8a496ba3493067a11ddfccb13ea9984 (patch) | |
tree | 958f874f70fc0089cca78d2564b09997e81561ae /chrome/browser/ui/cocoa/wrench_menu | |
parent | 7d5070ed99759ffb6312d552a15989e3634ee623 (diff) | |
download | chromium_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')
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 |