Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / profiles / profile_menu_controller.mm
blob5ee576fe66a875dec003d89759ba4a46724199e2
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/profiles/profile_menu_controller.h"
7 #include "base/mac/scoped_nsobject.h"
8 #include "base/metrics/histogram_macros.h"
9 #include "base/strings/sys_string_conversions.h"
10 #include "chrome/browser/browser_process.h"
11 #include "chrome/browser/profiles/avatar_menu.h"
12 #include "chrome/browser/profiles/avatar_menu_observer.h"
13 #include "chrome/browser/profiles/profile.h"
14 #include "chrome/browser/profiles/profile_avatar_icon_util.h"
15 #include "chrome/browser/profiles/profile_info_cache.h"
16 #include "chrome/browser/profiles/profile_info_interface.h"
17 #include "chrome/browser/profiles/profile_manager.h"
18 #include "chrome/browser/profiles/profile_metrics.h"
19 #include "chrome/browser/profiles/profile_window.h"
20 #include "chrome/browser/ui/browser.h"
21 #include "chrome/browser/ui/browser_list.h"
22 #include "chrome/browser/ui/browser_list_observer.h"
23 #include "chrome/browser/ui/cocoa/last_active_browser_cocoa.h"
24 #include "chrome/grit/generated_resources.h"
25 #include "components/signin/core/common/profile_management_switches.h"
26 #include "ui/base/l10n/l10n_util_mac.h"
27 #include "ui/gfx/image/image.h"
29 namespace {
31 // Used in UMA histogram macros, shouldn't be reordered or renumbered
32 enum ValidateMenuItemSelector {
33   UNKNOWN_SELECTOR = 0,
34   NEW_PROFILE,
35   EDIT_PROFILE,
36   SWITCH_PROFILE_MENU,
37   SWITCH_PROFILE_DOCK,
38   MAX_VALIDATE_MENU_SELECTOR,
41 }  // namespace
43 @interface ProfileMenuController (Private)
44 - (void)initializeMenu;
45 @end
47 namespace ProfileMenuControllerInternal {
49 class Observer : public chrome::BrowserListObserver,
50                  public AvatarMenuObserver {
51  public:
52   Observer(ProfileMenuController* controller) : controller_(controller) {
53     BrowserList::AddObserver(this);
54   }
56   ~Observer() override { BrowserList::RemoveObserver(this); }
58   // chrome::BrowserListObserver:
59   void OnBrowserAdded(Browser* browser) override {}
60   void OnBrowserRemoved(Browser* browser) override {
61     [controller_ activeBrowserChangedTo:chrome::GetLastActiveBrowser()];
62   }
63   void OnBrowserSetLastActive(Browser* browser) override {
64     [controller_ activeBrowserChangedTo:browser];
65   }
67   // AvatarMenuObserver:
68   void OnAvatarMenuChanged(AvatarMenu* menu) override {
69     [controller_ rebuildMenu];
70   }
72  private:
73   ProfileMenuController* controller_;  // Weak; owns this.
76 }  // namespace ProfileMenuControllerInternal
78 ////////////////////////////////////////////////////////////////////////////////
80 @implementation ProfileMenuController
82 - (id)initWithMainMenuItem:(NSMenuItem*)item {
83   if ((self = [super init])) {
84     mainMenuItem_ = item;
86     base::scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:
87         l10n_util::GetNSStringWithFixup(IDS_PROFILES_OPTIONS_GROUP_NAME)]);
88     [mainMenuItem_ setSubmenu:menu];
90     // This object will be constructed as part of nib loading, which happens
91     // before the message loop starts and g_browser_process is available.
92     // Schedule this on the loop to do work when the browser is ready.
93     [self performSelector:@selector(initializeMenu)
94                withObject:nil
95                afterDelay:0];
96   }
97   return self;
100 - (IBAction)switchToProfileFromMenu:(id)sender {
101   avatarMenu_->SwitchToProfile([sender tag], false,
102                                ProfileMetrics::SWITCH_PROFILE_MENU);
105 - (IBAction)switchToProfileFromDock:(id)sender {
106   // Explicitly bring to the foreground when taking action from the dock.
107   [NSApp activateIgnoringOtherApps:YES];
108   avatarMenu_->SwitchToProfile([sender tag], false,
109                                ProfileMetrics::SWITCH_PROFILE_DOCK);
112 - (IBAction)editProfile:(id)sender {
113   avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
116 - (IBAction)newProfile:(id)sender {
117   profiles::CreateAndSwitchToNewProfile(chrome::HOST_DESKTOP_TYPE_NATIVE,
118                                         ProfileManager::CreateCallback(),
119                                         ProfileMetrics::ADD_NEW_USER_MENU);
122 - (BOOL)insertItemsIntoMenu:(NSMenu*)menu
123                    atOffset:(NSInteger)offset
124                    fromDock:(BOOL)dock {
125   if (!avatarMenu_ || !avatarMenu_->ShouldShowAvatarMenu())
126     return NO;
128   // Don't show the list of profiles in the dock if only one profile exists.
129   if (dock && avatarMenu_->GetNumberOfItems() <= 1)
130     return NO;
132   if (dock) {
133     NSString* headerName =
134         l10n_util::GetNSStringWithFixup(IDS_PROFILES_OPTIONS_GROUP_NAME);
135     base::scoped_nsobject<NSMenuItem> header(
136         [[NSMenuItem alloc] initWithTitle:headerName
137                                    action:NULL
138                             keyEquivalent:@""]);
139     [header setEnabled:NO];
140     [menu insertItem:header atIndex:offset++];
141   }
143   for (size_t i = 0; i < avatarMenu_->GetNumberOfItems(); ++i) {
144     const AvatarMenu::Item& itemData = avatarMenu_->GetItemAt(i);
145     NSString* name = base::SysUTF16ToNSString(itemData.name);
146     SEL action = dock ? @selector(switchToProfileFromDock:)
147                       : @selector(switchToProfileFromMenu:);
148     NSMenuItem* item = [self createItemWithTitle:name
149                                           action:action];
150     [item setTag:itemData.menu_index];
151     if (dock) {
152       [item setIndentationLevel:1];
153     } else {
154       gfx::Image itemIcon;
155       bool isRectangle;
156       // Always use the low-res, small default avatars in the menu.
157       AvatarMenu::GetImageForMenuButton(itemData.profile_path,
158                                         &itemIcon,
159                                         &isRectangle);
161       // The image might be too large and need to be resized (i.e. if this is
162       // a signed-in user using the GAIA profile photo).
163       if (itemIcon.Width() > profiles::kAvatarIconWidth ||
164           itemIcon.Height() > profiles::kAvatarIconHeight) {
165         itemIcon = profiles::GetAvatarIconForWebUI(itemIcon, true);
166       }
167       DCHECK(itemIcon.Width() <= profiles::kAvatarIconWidth);
168       DCHECK(itemIcon.Height() <= profiles::kAvatarIconHeight);
169       [item setImage:itemIcon.ToNSImage()];
170       [item setState:itemData.active ? NSOnState : NSOffState];
171     }
172     [menu insertItem:item atIndex:i + offset];
173   }
175   return YES;
178 - (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
179   // In guest mode, chrome://settings isn't available, so disallow creating
180   // or editing a profile.
181   Profile* activeProfile = ProfileManager::GetLastUsedProfile();
182   if (activeProfile->IsGuestSession()) {
183     return [menuItem action] != @selector(newProfile:) &&
184            [menuItem action] != @selector(editProfile:);
185   }
187   size_t index = avatarMenu_->GetActiveProfileIndex();
188   if (avatarMenu_->GetNumberOfItems() <= index) {
189     ValidateMenuItemSelector currentSelector = UNKNOWN_SELECTOR;
190     if ([menuItem action] == @selector(newProfile:))
191       currentSelector = NEW_PROFILE;
192     else if ([menuItem action] == @selector(editProfile:))
193       currentSelector = EDIT_PROFILE;
194     else if ([menuItem action] == @selector(switchToProfileFromMenu:))
195       currentSelector = SWITCH_PROFILE_MENU;
196     else if ([menuItem action] == @selector(switchToProfileFromDock:))
197       currentSelector = SWITCH_PROFILE_DOCK;
198     UMA_HISTOGRAM_BOOLEAN("Profile.ValidateMenuItemInvalidIndex.IsGuest",
199                           activeProfile->IsGuestSession());
200     UMA_HISTOGRAM_CUSTOM_COUNTS(
201         "Profile.ValidateMenuItemInvalidIndex.ProfileCount",
202         avatarMenu_->GetNumberOfItems(),
203         1, 20, 20);
204     UMA_HISTOGRAM_ENUMERATION("Profile.ValidateMenuItemInvalidIndex.Selector",
205                               currentSelector,
206                               MAX_VALIDATE_MENU_SELECTOR);
208     return NO;
209   }
211   const AvatarMenu::Item& itemData = avatarMenu_->GetItemAt(index);
212   if ([menuItem action] == @selector(switchToProfileFromDock:) ||
213       [menuItem action] == @selector(switchToProfileFromMenu:)) {
214     if (!itemData.legacy_supervised)
215       return YES;
217     return [menuItem tag] == static_cast<NSInteger>(itemData.menu_index);
218   }
220   if ([menuItem action] == @selector(newProfile:))
221     return !itemData.legacy_supervised;
223   return YES;
226 // Private /////////////////////////////////////////////////////////////////////
228 - (NSMenu*)menu {
229   return [mainMenuItem_ submenu];
232 - (void)initializeMenu {
233   observer_.reset(new ProfileMenuControllerInternal::Observer(self));
234   avatarMenu_.reset(new AvatarMenu(
235       &g_browser_process->profile_manager()->GetProfileInfoCache(),
236       observer_.get(),
237       NULL));
238   avatarMenu_->RebuildMenu();
240   [[self menu] addItem:[NSMenuItem separatorItem]];
242   NSMenuItem* item = [self createItemWithTitle:
243       l10n_util::GetNSStringWithFixup(IDS_PROFILES_MANAGE_BUTTON_LABEL)
244                                         action:@selector(editProfile:)];
245   [[self menu] addItem:item];
247   [[self menu] addItem:[NSMenuItem separatorItem]];
248   item = [self createItemWithTitle:l10n_util::GetNSStringWithFixup(
249       IDS_PROFILES_CREATE_NEW_PROFILE_OPTION)
250                             action:@selector(newProfile:)];
251   [[self menu] addItem:item];
253   [self rebuildMenu];
256 // Notifies the controller that the active browser has changed and that the
257 // menu item and menu need to be updated to reflect that.
258 - (void)activeBrowserChangedTo:(Browser*)browser {
259   // Tell the menu that the browser has changed.
260   avatarMenu_->ActiveBrowserChanged(browser);
262   // If |browser| is NULL, it may be because the current profile was deleted
263   // and there are no other loaded profiles. In this case, calling
264   // |avatarMenu_->GetActiveProfileIndex()| may result in a profile being
265   // loaded, which is inappropriate to do on the UI thread.
266   //
267   // An early return provides the desired behavior:
268   //   a) If the profile was deleted, the menu would have been rebuilt and no
269   //      profile will have a check mark.
270   //   b) If the profile was not deleted, but there is no active browser, then
271   //      the previous profile will remain checked.
272   if (!browser)
273     return;
275   // Update the avatar menu to get the active item states. Don't call
276   // avatarMenu_->GetActiveProfileIndex() as the index might be
277   // incorrect if -activeBrowserChangedTo: is called while we deleting the
278   // active profile and closing all its browser windows.
279   avatarMenu_->RebuildMenu();
281   // Update the state for the menu items.
282   for (size_t i = 0; i < avatarMenu_->GetNumberOfItems(); ++i) {
283     const AvatarMenu::Item& itemData = avatarMenu_->GetItemAt(i);
284     [[[self menu] itemWithTag:itemData.menu_index]
285         setState:itemData.active ? NSOnState : NSOffState];
286   }
289 - (void)rebuildMenu {
290   NSMenu* menu = [self menu];
292   for (NSMenuItem* item = [menu itemAtIndex:0];
293        ![item isSeparatorItem];
294        item = [menu itemAtIndex:0]) {
295     [menu removeItemAtIndex:0];
296   }
298   BOOL hasContent = [self insertItemsIntoMenu:menu atOffset:0 fromDock:NO];
300   [mainMenuItem_ setHidden:!hasContent];
303 - (NSMenuItem*)createItemWithTitle:(NSString*)title action:(SEL)sel {
304   base::scoped_nsobject<NSMenuItem> item(
305       [[NSMenuItem alloc] initWithTitle:title action:sel keyEquivalent:@""]);
306   [item setTarget:self];
307   return [item.release() autorelease];
310 @end