diff options
author | tapted@chromium.org <tapted@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-06-18 11:09:33 +0000 |
---|---|---|
committer | tapted@chromium.org <tapted@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-06-18 11:09:33 +0000 |
commit | 5be161fb5d87646cbf59ba802a9ac573d4eab40a (patch) | |
tree | a1b3863edc190b0d4c23498bf7f0a307200540eb /ui/app_list | |
parent | ca43f4da1465d829439f68184f01448b42206483 (diff) | |
download | chromium_src-5be161fb5d87646cbf59ba802a9ac573d4eab40a.zip chromium_src-5be161fb5d87646cbf59ba802a9ac573d4eab40a.tar.gz chromium_src-5be161fb5d87646cbf59ba802a9ac573d4eab40a.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.*
Committed: https://src.chromium.org/viewvc/chrome?view=rev&revision=206237
Review URL: https://chromiumcodereview.appspot.com/15955003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@206930 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/app_list')
-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 |
9 files changed, 294 insertions, 5 deletions
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 |