diff options
-rw-r--r-- | build/common.gypi | 4 | ||||
-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 | 35 | ||||
-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 | 173 | ||||
-rw-r--r-- | ui/ui.gyp | 4 | ||||
-rw-r--r-- | ui/ui_unittests.gypi | 1 |
17 files changed, 651 insertions, 7 deletions
diff --git a/build/common.gypi b/build/common.gypi index ca7e6ce..051ddaa 100644 --- a/build/common.gypi +++ b/build/common.gypi @@ -621,8 +621,8 @@ 'use_system_libjpeg%': '<(android_webview_build)', }], - # Enable Settings App only on Windows. - ['enable_app_list==1 and OS=="win"', { + # Do not enable the Settings App on ChromeOS. + ['enable_app_list==1 and chromeos==0', { 'enable_settings_app%': 1, }, { 'enable_settings_app%': 0, diff --git a/ui/app_list/app_list.gyp b/ui/app_list/app_list.gyp index 24d93a8..0aaa174 100644 --- a/ui/app_list/app_list.gyp +++ b/ui/app_list/app_list.gyp @@ -56,6 +56,8 @@ 'cocoa/apps_search_results_controller.mm', 'cocoa/apps_search_results_model_bridge.h', 'cocoa/apps_search_results_model_bridge.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 4cfdb00..e0e1ad7 100644 --- a/ui/app_list/cocoa/app_list_view_controller.h +++ b/ui/app_list/cocoa/app_list_view_controller.h @@ -24,7 +24,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 eb7c5d2..50713e9 100644 --- a/ui/app_list/cocoa/app_list_view_controller.mm +++ b/ui/app_list/cocoa/app_list_view_controller.mm @@ -224,6 +224,10 @@ const NSTimeInterval kResultsAnimationDuration = 0.2; 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 8e68425..534a064 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] setDefaultImage: + 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 2c75127..e2c2563 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 { @@ -120,5 +129,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..1da338b --- /dev/null +++ b/ui/base/cocoa/controls/hover_image_menu_button_cell.h @@ -0,0 +1,35 @@ +// 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 setDefaultImage: +// 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; + +// Set the default image to show on the menu button. +- (void)setDefaultImage:(NSImage*)defaultImage; + +@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..32a9d69 --- /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)setDefaultImage:(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..45ed3689 --- /dev/null +++ b/ui/base/cocoa/controls/hover_image_menu_button_unittest.mm @@ -0,0 +1,173 @@ +// 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] setDefaultImage: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]); + + // Uses setDefaultImage rather than setImage to ensure the image goes into the + // NSPopUpButtonCell's menuItem. It is then accessible using [NSCell image]. + EXPECT_NSEQ(normal_, [[menu_button_ cell] image]); + [[menu_button_ cell] setDefaultImage:pressed_]; + EXPECT_NSEQ(pressed_, [[menu_button_ cell] image]); + EXPECT_TRUE([menu_button_ needsDisplay]); + [menu_button_ display]; + EXPECT_FALSE([menu_button_ needsDisplay]); + + // 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', |