diff options
author | tapted@chromium.org <tapted@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-06-14 00:12:53 +0000 |
---|---|---|
committer | tapted@chromium.org <tapted@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-06-14 00:12:53 +0000 |
commit | 9f45186915e269fa32c1f7dd44873bb535854dbd (patch) | |
tree | 6501a40e34bd532e7d6e08b5bfbcb25cd1ddffbe /ui | |
parent | ef18a5cdaabffa38db974ffb653605cfb2289446 (diff) | |
download | chromium_src-9f45186915e269fa32c1f7dd44873bb535854dbd.zip chromium_src-9f45186915e269fa32c1f7dd44873bb535854dbd.tar.gz chromium_src-9f45186915e269fa32c1f7dd44873bb535854dbd.tar.bz2 |
Menu for the OSX app launcher, HoverImageMenuButton in src/ui/base/cocoa/controls.
Adds a drop-down menu to the right of the search entry area on the OSX
app launcher. The menu is shown when clicked, and the button responds to
hover effects.
The menu button uses a new class, HoverImageMenuButton, which is derived
from an NSPopUpButton with minor extensions. Notably, it does not have a
dependency on browser themes, as does MenuButton from
chrome/browser/ui/cocoa. It tracks the mouse hover state and updates
the cell, which extends NSPopUpButtonCell and shows only the image in
the control frame -- no border, bezel, label, or dropdown arrow.
HoverImageMenuButtonCell supports a hover image, which behaves much like
an additional 'alternateImage' from NSButtonCell but for the hover
state, rather than the 'pressed' (or 'lit') state.
The menu shows the currently signed-in user, in a custom view as the
first item. It also (currently) shows menu options for Settings, Help, and
Feedback.
BUG=138633
TEST=Added app_list_unittests AppsSearchBoxMenuTest and
AppsSearchBoxMenuTest and tested manually to ensure the items are
launched correctly. Added ui_unittests HoverImageMenuButtonTest.*
Review URL: https://chromiumcodereview.appspot.com/15955003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@206237 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui')
-rw-r--r-- | ui/app_list/app_list.gyp | 2 | ||||
-rw-r--r-- | ui/app_list/app_list_menu.h | 1 | ||||
-rw-r--r-- | ui/app_list/cocoa/app_list_view_controller.h | 2 | ||||
-rw-r--r-- | ui/app_list/cocoa/app_list_view_controller.mm | 4 | ||||
-rw-r--r-- | ui/app_list/cocoa/apps_search_box_controller.h | 10 | ||||
-rw-r--r-- | ui/app_list/cocoa/apps_search_box_controller.mm | 107 | ||||
-rw-r--r-- | ui/app_list/cocoa/apps_search_box_controller_unittest.mm | 64 | ||||
-rw-r--r-- | ui/app_list/cocoa/current_user_menu_item_view.h | 24 | ||||
-rw-r--r-- | ui/app_list/cocoa/current_user_menu_item_view.mm | 85 | ||||
-rw-r--r-- | ui/base/cocoa/controls/hover_image_menu_button.h | 29 | ||||
-rw-r--r-- | ui/base/cocoa/controls/hover_image_menu_button.mm | 44 | ||||
-rw-r--r-- | ui/base/cocoa/controls/hover_image_menu_button_cell.h | 32 | ||||
-rw-r--r-- | ui/base/cocoa/controls/hover_image_menu_button_cell.mm | 69 | ||||
-rw-r--r-- | ui/base/cocoa/controls/hover_image_menu_button_unittest.mm | 168 | ||||
-rw-r--r-- | ui/ui.gyp | 4 | ||||
-rw-r--r-- | ui/ui_unittests.gypi | 1 |
16 files changed, 641 insertions, 5 deletions
diff --git a/ui/app_list/app_list.gyp b/ui/app_list/app_list.gyp index 60f21cf..34fe0ff 100644 --- a/ui/app_list/app_list.gyp +++ b/ui/app_list/app_list.gyp @@ -52,6 +52,8 @@ 'cocoa/apps_pagination_model_observer.h', 'cocoa/apps_search_box_controller.h', 'cocoa/apps_search_box_controller.mm', + 'cocoa/current_user_menu_item_view.h', + 'cocoa/current_user_menu_item_view.mm', 'cocoa/item_drag_controller.h', 'cocoa/item_drag_controller.mm', 'cocoa/scroll_view_with_no_scrollbars.h', diff --git a/ui/app_list/app_list_menu.h b/ui/app_list/app_list_menu.h index 4153f2e..ff4be49 100644 --- a/ui/app_list/app_list_menu.h +++ b/ui/app_list/app_list_menu.h @@ -26,7 +26,6 @@ class AppListMenu : public ui::SimpleMenuModel::Delegate { explicit AppListMenu(AppListViewDelegate* delegate); virtual ~AppListMenu(); - protected: ui::SimpleMenuModel* menu_model() { return &menu_model_; } private: diff --git a/ui/app_list/cocoa/app_list_view_controller.h b/ui/app_list/cocoa/app_list_view_controller.h index 40d68e3..ef82298 100644 --- a/ui/app_list/cocoa/app_list_view_controller.h +++ b/ui/app_list/cocoa/app_list_view_controller.h @@ -23,7 +23,7 @@ class AppListModel; // Controller for the top-level view of the app list UI. It creates and hosts an // AppsGridController (displaying an AppListModel), pager control to navigate -// between pages in the grid, and search entry box. +// between pages in the grid, and search entry box with a pop up menu. APP_LIST_EXPORT @interface AppListViewController : NSViewController<AppsPaginationModelObserver, AppsSearchBoxDelegate> { diff --git a/ui/app_list/cocoa/app_list_view_controller.mm b/ui/app_list/cocoa/app_list_view_controller.mm index 91d82da..1939c26 100644 --- a/ui/app_list/cocoa/app_list_view_controller.mm +++ b/ui/app_list/cocoa/app_list_view_controller.mm @@ -189,6 +189,10 @@ const CGFloat kMaxSegmentWidth = 80; return appListModel ? appListModel->search_box() : NULL; } +- (app_list::AppListViewDelegate*)appListDelegate { + return [self delegate]; +} + - (BOOL)control:(NSControl*)control textView:(NSTextView*)textView doCommandBySelector:(SEL)command { diff --git a/ui/app_list/cocoa/apps_search_box_controller.h b/ui/app_list/cocoa/apps_search_box_controller.h index 8f048e8..f373235 100644 --- a/ui/app_list/cocoa/apps_search_box_controller.h +++ b/ui/app_list/cocoa/apps_search_box_controller.h @@ -12,14 +12,19 @@ #include "ui/app_list/app_list_export.h" namespace app_list { +class AppListMenu; +class AppListViewDelegate; class SearchBoxModel; class SearchBoxModelObserverBridge; } +@class AppListMenuController; +@class HoverImageMenuButton; @class SearchTextField; @protocol AppsSearchBoxDelegate<NSTextFieldDelegate> +- (app_list::AppListViewDelegate*)appListDelegate; - (app_list::SearchBoxModel*)searchBoxModel; - (void)modelTextDidChange; @@ -31,7 +36,10 @@ APP_LIST_EXPORT @private scoped_nsobject<SearchTextField> searchTextField_; scoped_nsobject<NSImageView> searchImageView_; + scoped_nsobject<HoverImageMenuButton> menuButton_; + scoped_nsobject<AppListMenuController> menuController_; scoped_ptr<app_list::SearchBoxModelObserverBridge> bridge_; + scoped_ptr<app_list::AppListMenu> appListMenu_; id<AppsSearchBoxDelegate> delegate_; // Weak. Owns us. } @@ -46,6 +54,8 @@ APP_LIST_EXPORT @interface AppsSearchBoxController (TestingAPI) - (NSTextField*)searchTextField; +- (NSPopUpButton*)menuControl; +- (app_list::AppListMenu*)appListMenu; @end diff --git a/ui/app_list/cocoa/apps_search_box_controller.mm b/ui/app_list/cocoa/apps_search_box_controller.mm index ad35dba..1984318 100644 --- a/ui/app_list/cocoa/apps_search_box_controller.mm +++ b/ui/app_list/cocoa/apps_search_box_controller.mm @@ -6,20 +6,32 @@ #include "base/mac/foundation_util.h" #include "base/strings/sys_string_conversions.h" +#include "grit/ui_resources.h" #import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h" +#include "ui/app_list/app_list_menu.h" +#import "ui/app_list/cocoa/current_user_menu_item_view.h" #include "ui/app_list/search_box_model.h" #include "ui/app_list/search_box_model_observer.h" +#import "ui/base/cocoa/controls/hover_image_menu_button.h" +#import "ui/base/cocoa/controls/hover_image_menu_button_cell.h" +#import "ui/base/cocoa/menu_controller.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/image/image_skia_util_mac.h" namespace { -// Padding either side of the search icon. +// Padding either side of the search icon and menu button. const CGFloat kPadding = 14; // Size of the search icon. const CGFloat kSearchIconDimension = 32; +// Size of the menu button on the right. +const CGFloat kMenuButtonDimension = 29; + +// Vertical offset that the menu should appear below the menu button. +const CGFloat kMenuOffsetFromButton = 2; + } @interface AppsSearchBoxController () @@ -112,6 +124,15 @@ void SearchBoxModelObserverBridge::TextChanged() { @end +@interface AppListMenuController : MenuController { + @private + AppsSearchBoxController* searchBoxController_; // Weak. Owns us. +} + +- (id)initWithSearchBoxController:(AppsSearchBoxController*)parent; + +@end + @implementation AppsSearchBoxController @synthesize delegate = delegate_; @@ -131,18 +152,36 @@ void SearchBoxModelObserverBridge::TextChanged() { } - (void)setDelegate:(id<AppsSearchBoxDelegate>)delegate { + [[menuButton_ menu] removeAllItems]; + menuController_.reset(); + appListMenu_.reset(); bridge_.reset(); // Ensure observers are cleared before updating |delegate_|. delegate_ = delegate; if (!delegate_) return; bridge_.reset(new app_list::SearchBoxModelObserverBridge(self)); + if (![delegate_ appListDelegate]) + return; + + appListMenu_.reset(new app_list::AppListMenu([delegate_ appListDelegate])); + menuController_.reset([[AppListMenuController alloc] + initWithSearchBoxController:self]); + [menuButton_ setMenu:[menuController_ menu]]; // Menu will populate here. } - (NSTextField*)searchTextField { return searchTextField_; } +- (NSPopUpButton*)menuControl { + return menuButton_; +} + +- (app_list::AppListMenu*)appListMenu { + return appListMenu_.get(); +} + - (NSImageView*)searchImageView { return searchImageView_; } @@ -153,15 +192,32 @@ void SearchBoxModelObserverBridge::TextChanged() { kPadding, 0, kSearchIconDimension, NSHeight(viewBounds))]); searchTextField_.reset([[SearchTextField alloc] initWithFrame:viewBounds]); + ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); [searchTextField_ setDelegate:self]; - [searchTextField_ setFont:ui::ResourceBundle::GetSharedInstance().GetFont( + [searchTextField_ setFont:rb.GetFont( ui::ResourceBundle::MediumFont).GetNativeFont()]; [searchTextField_ setMarginsWithLeftMargin:NSMaxX([searchImageView_ frame]) + kPadding - rightMargin:kPadding]; + rightMargin:kMenuButtonDimension + 2 * kPadding]; + + // Add the drop-down menu, with a custom button. + NSRect buttonFrame = NSMakeRect( + NSWidth(viewBounds) - kMenuButtonDimension - kPadding, + floor(NSMidY(viewBounds) - kMenuButtonDimension / 2), + kMenuButtonDimension, + kMenuButtonDimension); + menuButton_.reset([[HoverImageMenuButton alloc] initWithFrame:buttonFrame + pullsDown:YES]); + [[menuButton_ hoverImageMenuButtonCell] setImage: + rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_NORMAL).AsNSImage()]; + [[menuButton_ hoverImageMenuButtonCell] setAlternateImage: + rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_PRESSED).AsNSImage()]; + [[menuButton_ hoverImageMenuButtonCell] setHoverImage: + rb.GetNativeImageNamed(IDR_APP_LIST_TOOLS_HOVER).AsNSImage()]; [[self view] addSubview:searchImageView_]; [[self view] addSubview:searchTextField_]; + [[self view] addSubview:menuButton_]; } - (BOOL)control:(NSControl*)control @@ -291,3 +347,48 @@ void SearchBoxModelObserverBridge::TextChanged() { } @end + +@implementation AppListMenuController + +- (id)initWithSearchBoxController:(AppsSearchBoxController*)parent { + // Need to initialze super with a NULL model, otherwise it will immediately + // try to populate, which can't be done until setting the parent. + if ((self = [super initWithModel:NULL + useWithPopUpButtonCell:YES])) { + searchBoxController_ = parent; + [super setModel:[parent appListMenu]->menu_model()]; + } + return self; +} + +- (void)addItemToMenu:(NSMenu*)menu + atIndex:(NSInteger)index + fromModel:(ui::MenuModel*)model { + [super addItemToMenu:menu + atIndex:index + fromModel:model]; + if (model->GetCommandIdAt(index) != app_list::AppListMenu::CURRENT_USER) + return; + + scoped_nsobject<NSView> customItemView([[CurrentUserMenuItemView alloc] + initWithDelegate:[[searchBoxController_ delegate] appListDelegate]]); + [[menu itemAtIndex:index] setView:customItemView]; +} + +- (NSRect)confinementRectForMenu:(NSMenu*)menu + onScreen:(NSScreen*)screen { + NSPopUpButton* menuButton = [searchBoxController_ menuControl]; + // Ensure the menu comes up below the menu button by trimming the window frame + // to a point anchored below the bottom right of the button. + NSRect anchorRect = [menuButton convertRect:[menuButton bounds] + toView:nil]; + NSPoint anchorPoint = [[menuButton window] convertBaseToScreen:NSMakePoint( + NSMaxX(anchorRect), + NSMinY(anchorRect) - kMenuOffsetFromButton)]; + NSRect confinementRect = [[menuButton window] frame]; + confinementRect.size = NSMakeSize(anchorPoint.x - NSMinX(confinementRect), + anchorPoint.y - NSMinY(confinementRect)); + return confinementRect; +} + +@end diff --git a/ui/app_list/cocoa/apps_search_box_controller_unittest.mm b/ui/app_list/cocoa/apps_search_box_controller_unittest.mm index ee47687..1188d96 100644 --- a/ui/app_list/cocoa/apps_search_box_controller_unittest.mm +++ b/ui/app_list/cocoa/apps_search_box_controller_unittest.mm @@ -8,12 +8,17 @@ #include "base/strings/sys_string_conversions.h" #include "base/strings/utf_string_conversions.h" #import "testing/gtest_mac.h" +#include "ui/app_list/app_list_menu.h" +#import "ui/app_list/cocoa/current_user_menu_item_view.h" #include "ui/app_list/search_box_model.h" +#include "ui/app_list/test/app_list_test_model.h" +#include "ui/app_list/test/app_list_test_view_delegate.h" #import "ui/base/test/ui_cocoa_test_helper.h" @interface TestAppsSearchBoxDelegate : NSObject<AppsSearchBoxDelegate> { @private app_list::SearchBoxModel searchBoxModel_; + app_list::test::AppListTestViewDelegate appListDelegate_; int textChangeCount_; } @@ -29,6 +34,10 @@ return &searchBoxModel_; } +- (app_list::AppListViewDelegate*)appListDelegate { + return &appListDelegate_; +} + - (BOOL)control:(NSControl*)control textView:(NSTextView*)textView doCommandBySelector:(SEL)command { @@ -119,5 +128,60 @@ TEST_F(AppsSearchBoxControllerTest, SearchBoxModel) { EXPECT_EQ(2, [delegate_ textChangeCount]); } +// Test the popup menu items. +TEST_F(AppsSearchBoxControllerTest, SearchBoxMenu) { + NSPopUpButton* menu_control = [apps_search_box_controller_ menuControl]; + EXPECT_TRUE([apps_search_box_controller_ appListMenu]); + ui::MenuModel* menu_model + = [apps_search_box_controller_ appListMenu]->menu_model(); + // Add one to the item count to account for the blank, first item that Cocoa + // has in its popup menus. + EXPECT_EQ(menu_model->GetItemCount() + 1, + [[menu_control menu] numberOfItems]); + + // The CURRENT_USER item should contain our custom view. + ui::MenuModel* found_menu_model = menu_model; + int index; + EXPECT_TRUE(ui::MenuModel::GetModelAndIndexForCommandId( + AppListMenu::CURRENT_USER, &menu_model, &index)); + EXPECT_EQ(found_menu_model, menu_model); + NSMenuItem* current_user_item = [[menu_control menu] itemAtIndex:index + 1]; + EXPECT_TRUE([current_user_item view]); + + // A regular item should have just the label. + EXPECT_TRUE(ui::MenuModel::GetModelAndIndexForCommandId( + AppListMenu::SHOW_SETTINGS, &menu_model, &index)); + EXPECT_EQ(found_menu_model, menu_model); + NSMenuItem* settings_item = [[menu_control menu] itemAtIndex:index + 1]; + EXPECT_FALSE([settings_item view]); + EXPECT_NSEQ(base::SysUTF16ToNSString(menu_model->GetLabelAt(index)), + [settings_item title]); +} + +// Test initialization and display of the custom menu item that shows the +// currently signed-in user. This is a non-interactive view. +class AppsSearchBoxCustomMenuItemTest : public ui::CocoaTest { + public: + AppsSearchBoxCustomMenuItemTest() { + Init(); + } + + virtual void SetUp() OVERRIDE { + scoped_ptr<AppListViewDelegate> delegate(new AppListTestViewDelegate); + current_user_menu_item_.reset([[[CurrentUserMenuItemView alloc] + initWithDelegate:delegate.get()] retain]); + ui::CocoaTest::SetUp(); + [[test_window() contentView] addSubview:current_user_menu_item_]; + } + + protected: + scoped_nsobject<NSView> current_user_menu_item_; + + private: + DISALLOW_COPY_AND_ASSIGN(AppsSearchBoxCustomMenuItemTest); +}; + +TEST_VIEW(AppsSearchBoxCustomMenuItemTest, current_user_menu_item_); + } // namespace test } // namespace app_list diff --git a/ui/app_list/cocoa/current_user_menu_item_view.h b/ui/app_list/cocoa/current_user_menu_item_view.h new file mode 100644 index 0000000..84c240a --- /dev/null +++ b/ui/app_list/cocoa/current_user_menu_item_view.h @@ -0,0 +1,24 @@ +// Copyright 2013 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 UI_APP_LIST_COCOA_CURRENT_USER_MENU_ITEM_VIEW_H_ +#define UI_APP_LIST_COCOA_CURRENT_USER_MENU_ITEM_VIEW_H_ + +#import <Cocoa/Cocoa.h> + +#include "ui/app_list/app_list_export.h" + +namespace app_list { +class AppListViewDelegate; +} + +// The custom in-menu view representing the currently signed-in user. +APP_LIST_EXPORT +@interface CurrentUserMenuItemView : NSView + +- (id)initWithDelegate:(app_list::AppListViewDelegate*)delegate; + +@end + +#endif // UI_APP_LIST_COCOA_CURRENT_USER_MENU_ITEM_VIEW_H_ diff --git a/ui/app_list/cocoa/current_user_menu_item_view.mm b/ui/app_list/cocoa/current_user_menu_item_view.mm new file mode 100644 index 0000000..dcc4838 --- /dev/null +++ b/ui/app_list/cocoa/current_user_menu_item_view.mm @@ -0,0 +1,85 @@ +// Copyright 2013 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 "ui/app_list/cocoa/current_user_menu_item_view.h" + +#include "base/logging.h" +#include "base/memory/scoped_nsobject.h" +#include "base/strings/sys_string_conversions.h" +#include "grit/ui_resources.h" +#include "ui/app_list/app_list_view_delegate.h" +#include "ui/base/resource/resource_bundle.h" + +namespace { + +// Padding on the left of the indicator icon. +const CGFloat kMenuLeftMargin = 3; + +} + +@interface CurrentUserMenuItemView () + +// Adds a text label in the custom view in the menu showing the current user. +- (NSTextField*)addLabelWithFrame:(NSPoint)origin + labelText:(const string16&)labelText; + +@end + +@implementation CurrentUserMenuItemView + +- (id)initWithDelegate:(app_list::AppListViewDelegate*)delegate { + DCHECK(delegate); + if ((self = [super initWithFrame:NSZeroRect])) { + NSImage* userImage = ui::ResourceBundle::GetSharedInstance(). + GetNativeImageNamed(IDR_APP_LIST_USER_INDICATOR).AsNSImage(); + NSRect imageRect = NSMakeRect(kMenuLeftMargin, 0, 0, 0); + imageRect.size = [userImage size]; + scoped_nsobject<NSImageView> userImageView( + [[NSImageView alloc] initWithFrame:imageRect]); + [userImageView setImage:userImage]; + [self addSubview:userImageView]; + + NSPoint labelOrigin = NSMakePoint(NSMaxX(imageRect), 0); + NSTextField* userField = + [self addLabelWithFrame:labelOrigin + labelText:delegate->GetCurrentUserName()]; + + labelOrigin.y = NSMaxY([userField frame]); + NSTextField* emailField = + [self addLabelWithFrame:labelOrigin + labelText:delegate->GetCurrentUserEmail()]; + [emailField setTextColor:[NSColor disabledControlTextColor]]; + + // Size the container view to fit the longest label. + NSRect labelFrame = [emailField frame]; + if (NSWidth([userField frame]) > NSWidth(labelFrame)) + labelFrame.size.width = NSWidth([userField frame]); + [self setFrameSize:NSMakeSize( + NSMaxX(labelFrame) + NSMaxX(imageRect), + NSMaxY(labelFrame))]; + } + return self; +} + +- (NSTextField*)addLabelWithFrame:(NSPoint)origin + labelText:(const string16&)labelText { + NSRect labelFrame = NSZeroRect; + labelFrame.origin = origin; + scoped_nsobject<NSTextField> label( + [[NSTextField alloc] initWithFrame:labelFrame]); + [label setStringValue:base::SysUTF16ToNSString(labelText)]; + [label setEditable:NO]; + [label setBordered:NO]; + [label setDrawsBackground:NO]; + [label setFont:[NSFont menuFontOfSize:0]]; + [label sizeToFit]; + [self addSubview:label]; + return label.autorelease(); +} + +- (BOOL)isFlipped { + return YES; +} + +@end diff --git a/ui/base/cocoa/controls/hover_image_menu_button.h b/ui/base/cocoa/controls/hover_image_menu_button.h new file mode 100644 index 0000000..f6af7bf --- /dev/null +++ b/ui/base/cocoa/controls/hover_image_menu_button.h @@ -0,0 +1,29 @@ +// Copyright 2013 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 UI_BASE_COCOA_CONTROLS_HOVER_IMAGE_MENU_BUTTON_H_ +#define UI_BASE_COCOA_CONTROLS_HOVER_IMAGE_MENU_BUTTON_H_ + +#import <Cocoa/Cocoa.h> + +#import "ui/base/cocoa/tracking_area.h" +#include "ui/base/ui_export.h" + +@class HoverImageMenuButtonCell; + +// An NSPopUpButton that additionally tracks mouseover state and calls +// [[self cell] setHovered:flag] when the hover state changes. Uses +// HoverImageMenuButtonCell as the default cellClass. Note that the menu item at +// index 0 is ignored and client code should populate it with a dummy item. +UI_EXPORT +@interface HoverImageMenuButton : NSPopUpButton { + @private + ui::ScopedCrTrackingArea trackingArea_; +} + +- (HoverImageMenuButtonCell*)hoverImageMenuButtonCell; + +@end + +#endif // UI_BASE_COCOA_CONTROLS_HOVER_IMAGE_MENU_BUTTON_H_ diff --git a/ui/base/cocoa/controls/hover_image_menu_button.mm b/ui/base/cocoa/controls/hover_image_menu_button.mm new file mode 100644 index 0000000..85a3118 --- /dev/null +++ b/ui/base/cocoa/controls/hover_image_menu_button.mm @@ -0,0 +1,44 @@ +// Copyright 2013 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 "ui/base/cocoa/controls/hover_image_menu_button.h" + +#include "base/mac/foundation_util.h" +#import "ui/base/cocoa/controls/hover_image_menu_button_cell.h" + +@implementation HoverImageMenuButton + ++ (Class)cellClass { + return [HoverImageMenuButtonCell class]; +} + +- (id)initWithFrame:(NSRect)frameRect + pullsDown:(BOOL)flag { + if ((self = [super initWithFrame:frameRect + pullsDown:flag])) { + trackingArea_.reset( + [[CrTrackingArea alloc] initWithRect:NSZeroRect + options:NSTrackingInVisibleRect | + NSTrackingMouseEnteredAndExited | + NSTrackingActiveInKeyWindow + owner:self + userInfo:nil]); + [self addTrackingArea:trackingArea_.get()]; + } + return self; +} + +- (HoverImageMenuButtonCell*)hoverImageMenuButtonCell { + return base::mac::ObjCCastStrict<HoverImageMenuButtonCell>([self cell]); +} + +- (void)mouseEntered:(NSEvent*)theEvent { + [[self cell] setHovered:YES]; +} + +- (void)mouseExited:(NSEvent*)theEvent { + [[self cell] setHovered:NO]; +} + +@end diff --git a/ui/base/cocoa/controls/hover_image_menu_button_cell.h b/ui/base/cocoa/controls/hover_image_menu_button_cell.h new file mode 100644 index 0000000..6d6f9ff --- /dev/null +++ b/ui/base/cocoa/controls/hover_image_menu_button_cell.h @@ -0,0 +1,32 @@ +// Copyright 2013 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 UI_BASE_COCOA_CONTROLS_HOVER_IMAGE_MENU_BUTTON_CELL_H_ +#define UI_BASE_COCOA_CONTROLS_HOVER_IMAGE_MENU_BUTTON_CELL_H_ + +#import <Cocoa/Cocoa.h> + +#include "base/memory/scoped_nsobject.h" +#include "ui/base/ui_export.h" + +// A custom NSPopUpButtonCell that permits a hover image, and draws only an +// image in its frame; no border, bezel or drop-down arrow. Use setImage to set +// the default image, setAlternateImage to set the button shown while the menu +// is active, and setHoverImage for the mouseover hover image. +UI_EXPORT +@interface HoverImageMenuButtonCell : NSPopUpButtonCell { + @private + scoped_nsobject<NSImage> hoverImage_; + BOOL hovered_; +} + +@property(retain, nonatomic) NSImage* hoverImage; +@property(assign, nonatomic, getter=isHovered) BOOL hovered; + +// Return the image that would be drawn based on the current state flags. +- (NSImage*)imageToDraw; + +@end + +#endif // UI_BASE_COCOA_CONTROLS_HOVER_IMAGE_MENU_BUTTON_CELL_H_ diff --git a/ui/base/cocoa/controls/hover_image_menu_button_cell.mm b/ui/base/cocoa/controls/hover_image_menu_button_cell.mm new file mode 100644 index 0000000..881a3fd --- /dev/null +++ b/ui/base/cocoa/controls/hover_image_menu_button_cell.mm @@ -0,0 +1,69 @@ +// Copyright 2013 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 "ui/base/cocoa/controls/hover_image_menu_button_cell.h" + +@implementation HoverImageMenuButtonCell + +@synthesize hovered = hovered_; + +- (id)initTextCell:(NSString*)stringValue + pullsDown:(BOOL)pullDown { + if ((self = [super initTextCell:stringValue + pullsDown:pullDown])) { + [self setUsesItemFromMenu:NO]; + } + return self; +} + +- (void)setHoverImage:(NSImage*)newImage { + if ([hoverImage_ isEqual:newImage]) + return; + + hoverImage_.reset([newImage retain]); + if (hovered_) + [[self controlView] setNeedsDisplay:YES]; +} + +- (NSImage*)hoverImage { + return hoverImage_; +} + +- (void)setHovered:(BOOL)hovered { + if (hovered_ == hovered) + return; + + hovered_ = hovered; + [[self controlView] setNeedsDisplay:YES]; +} + +- (NSImage*)imageToDraw { + if ([self isHighlighted] && [self alternateImage]) + return [self alternateImage]; + + if ([self isHovered] && [self hoverImage]) + return [self hoverImage]; + + // Note that NSPopUpButtonCell updates the cell image when the [self menuItem] + // changes. + return [self image]; +} + +- (void)setImage:(NSImage*)defaultImage { + scoped_nsobject<NSMenuItem> buttonMenuItem([[NSMenuItem alloc] init]); + [buttonMenuItem setImage:defaultImage]; + [self setMenuItem:buttonMenuItem]; +} + +- (void)drawWithFrame:(NSRect)cellFrame + inView:(NSView*)controlView { + [[self imageToDraw] drawInRect:cellFrame + fromRect:NSZeroRect + operation:NSCompositeSourceOver + fraction:1.0 + respectFlipped:YES + hints:nil]; +} + +@end diff --git a/ui/base/cocoa/controls/hover_image_menu_button_unittest.mm b/ui/base/cocoa/controls/hover_image_menu_button_unittest.mm new file mode 100644 index 0000000..2b121fc --- /dev/null +++ b/ui/base/cocoa/controls/hover_image_menu_button_unittest.mm @@ -0,0 +1,168 @@ +// Copyright 2013 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 "ui/base/cocoa/controls/hover_image_menu_button.h" + +#include "base/mac/foundation_util.h" +#import "testing/gtest_mac.h" +#import "ui/base/cocoa/controls/hover_image_menu_button_cell.h" +#import "ui/base/test/ui_cocoa_test_helper.h" + +namespace ui { + +namespace { + +// Test initialization and display of the NSPopUpButton that shows the drop- +// down menu. Don't try to show the menu, since it will block the thread. +class HoverImageMenuButtonTest : public CocoaTest { + public: + HoverImageMenuButtonTest() {} + + // CocoaTest override: + virtual void SetUp() OVERRIDE; + + protected: + scoped_nsobject<HoverImageMenuButton> menu_button_; + scoped_nsobject<NSImage> normal_; + scoped_nsobject<NSImage> pressed_; + scoped_nsobject<NSImage> hovered_; + + DISALLOW_COPY_AND_ASSIGN(HoverImageMenuButtonTest); +}; + +void HoverImageMenuButtonTest::SetUp() { + menu_button_.reset( + [[HoverImageMenuButton alloc] initWithFrame:NSMakeRect(0, 0, 50, 30) + pullsDown:YES]); + + normal_.reset([base::mac::ObjCCastStrict<NSImage>( + [NSImage imageNamed:NSImageNameStatusAvailable]) retain]); + pressed_.reset([base::mac::ObjCCastStrict<NSImage>( + [NSImage imageNamed:NSImageNameStatusUnavailable]) retain]); + hovered_.reset([base::mac::ObjCCastStrict<NSImage>( + [NSImage imageNamed:NSImageNameStatusPartiallyAvailable]) retain]); + [[menu_button_ hoverImageMenuButtonCell] setImage:normal_]; + [[menu_button_ hoverImageMenuButtonCell] setAlternateImage:pressed_]; + [[menu_button_ hoverImageMenuButtonCell] setHoverImage:hovered_]; + + CocoaTest::SetUp(); + [[test_window() contentView] addSubview:menu_button_]; +} + +} // namespace + +TEST_VIEW(HoverImageMenuButtonTest, menu_button_); + +// Tests that the correct image is chosen, depending on the cell's state flags. +TEST_F(HoverImageMenuButtonTest, CheckImagesForState) { + EXPECT_FALSE([[menu_button_ cell] isHovered]); + EXPECT_FALSE([[menu_button_ cell] isHighlighted]); + EXPECT_NSEQ(normal_, [[menu_button_ cell] imageToDraw]); + [menu_button_ display]; + + [[menu_button_ cell] setHovered:YES]; + EXPECT_TRUE([[menu_button_ cell] isHovered]); + EXPECT_FALSE([[menu_button_ cell] isHighlighted]); + EXPECT_NSEQ(hovered_, [[menu_button_ cell] imageToDraw]); + [menu_button_ display]; + + // Highlighted takes precendece over hover. + [[menu_button_ cell] setHighlighted:YES]; + EXPECT_TRUE([[menu_button_ cell] isHovered]); + EXPECT_TRUE([[menu_button_ cell] isHighlighted]); + EXPECT_NSEQ(pressed_, [[menu_button_ cell] imageToDraw]); + [menu_button_ display]; + + [[menu_button_ cell] setHovered:NO]; + EXPECT_FALSE([[menu_button_ cell] isHovered]); + EXPECT_TRUE([[menu_button_ cell] isHighlighted]); + EXPECT_NSEQ(pressed_, [[menu_button_ cell] imageToDraw]); + [menu_button_ display]; + + [[menu_button_ cell] setHighlighted:NO]; + EXPECT_FALSE([[menu_button_ cell] isHovered]); + EXPECT_FALSE([[menu_button_ cell] isHighlighted]); + EXPECT_NSEQ(normal_, [[menu_button_ cell] imageToDraw]); + [menu_button_ display]; +} + +// Tests that calling the various setXImage functions calls setNeedsDisplay. +TEST_F(HoverImageMenuButtonTest, NewImageCausesDisplay) { + [menu_button_ display]; + EXPECT_FALSE([menu_button_ needsDisplay]); + + // Note that regular setImage is overridden to ensure the image goes into the + // NSPopUpButtonCell's menuItem. + [[menu_button_ cell] setImage:pressed_]; + + // Highlighting the cell requires a redisplay. + [[menu_button_ cell] setHighlighted:YES]; + EXPECT_TRUE([menu_button_ needsDisplay]); + [menu_button_ display]; + EXPECT_FALSE([menu_button_ needsDisplay]); + + // setAlternateImage comes from NSButtonCell. Ensure the added setHover* + // behaviour matches. + [[menu_button_ cell] setAlternateImage:normal_]; + EXPECT_TRUE([menu_button_ needsDisplay]); + [menu_button_ display]; + EXPECT_FALSE([menu_button_ needsDisplay]); + + // Setting the same image should not cause a redisplay. + [[menu_button_ cell] setAlternateImage:normal_]; + EXPECT_FALSE([menu_button_ needsDisplay]); + + // Unhighlighting requires a redisplay. + [[menu_button_ cell] setHighlighted:NO]; + EXPECT_TRUE([menu_button_ needsDisplay]); + [menu_button_ display]; + EXPECT_FALSE([menu_button_ needsDisplay]); + + // Changing hover state requires a redisplay. + [[menu_button_ cell] setHovered:YES]; + EXPECT_TRUE([menu_button_ needsDisplay]); + [menu_button_ display]; + EXPECT_FALSE([menu_button_ needsDisplay]); + + // setHoverImage comes directly from storage in HoverImageMenuButtonCell. + [[menu_button_ cell] setHoverImage:normal_]; + EXPECT_TRUE([menu_button_ needsDisplay]); + [menu_button_ display]; + EXPECT_FALSE([menu_button_ needsDisplay]); + + // Setting the same image should not cause a redisplay. + [[menu_button_ cell] setHoverImage:normal_]; + EXPECT_FALSE([menu_button_ needsDisplay]); + + // Unhover requires a redisplay. + [[menu_button_ cell] setHovered:NO]; + EXPECT_TRUE([menu_button_ needsDisplay]); + [menu_button_ display]; + EXPECT_FALSE([menu_button_ needsDisplay]); + + // Changing the image while not hovered should not require a redisplay. + [[menu_button_ cell] setHoverImage:pressed_]; + EXPECT_FALSE([menu_button_ needsDisplay]); +} + +// Test that the mouse enter and exit is properly handled, to set hover state. +TEST_F(HoverImageMenuButtonTest, SimulateMouseEnterExit) { + [menu_button_ display]; + EXPECT_FALSE([menu_button_ needsDisplay]); + EXPECT_NSEQ(normal_, [[menu_button_ cell] imageToDraw]); + + [menu_button_ mouseEntered:nil]; + EXPECT_TRUE([menu_button_ needsDisplay]); + EXPECT_NSEQ(hovered_, [[menu_button_ cell] imageToDraw]); + [menu_button_ display]; + EXPECT_FALSE([menu_button_ needsDisplay]); + + [menu_button_ mouseExited:nil]; + EXPECT_TRUE([menu_button_ needsDisplay]); + EXPECT_NSEQ(normal_, [[menu_button_ cell] imageToDraw]); + [menu_button_ display]; + EXPECT_FALSE([menu_button_ needsDisplay]); +} + +} // namespace ui @@ -101,6 +101,10 @@ 'base/cocoa/base_view.mm', 'base/cocoa/cocoa_event_utils.h', 'base/cocoa/cocoa_event_utils.mm', + 'base/cocoa/controls/hover_image_menu_button.h', + 'base/cocoa/controls/hover_image_menu_button.mm', + 'base/cocoa/controls/hover_image_menu_button_cell.h', + 'base/cocoa/controls/hover_image_menu_button_cell.mm', 'base/cocoa/events_mac.mm', 'base/cocoa/find_pasteboard.h', 'base/cocoa/find_pasteboard.mm', diff --git a/ui/ui_unittests.gypi b/ui/ui_unittests.gypi index b6fa266..7735fcc 100644 --- a/ui/ui_unittests.gypi +++ b/ui/ui_unittests.gypi @@ -140,6 +140,7 @@ 'base/clipboard/custom_data_helper_unittest.cc', 'base/cocoa/base_view_unittest.mm', 'base/cocoa/cocoa_event_utils_unittest.mm', + 'base/cocoa/controls/hover_image_menu_button_unittest.mm', 'base/cocoa/events_mac_unittest.mm', 'base/cocoa/focus_tracker_unittest.mm', 'base/cocoa/fullscreen_window_manager_unittest.mm', |