| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/cocoa/profiles/profile_menu_controller.h" |
| |
| #include <AppKit/AppKit.h> |
| #include <stddef.h> |
| |
| #include <memory> |
| #include <optional> |
| |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/profiles/avatar_menu.h" |
| #include "chrome/browser/profiles/avatar_menu_observer.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_attributes_storage.h" |
| #include "chrome/browser/profiles/profile_avatar_icon_util.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/profiles/profile_metrics.h" |
| #include "chrome/browser/profiles/profile_window.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/browser_list_observer.h" |
| #include "chrome/browser/ui/ui_features.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/prefs/pref_service.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| #include "ui/gfx/image/image.h" |
| |
| namespace { |
| |
| NSString* GetProfileMenuTitle() { |
| return l10n_util::GetNSStringWithFixup(IDS_PROFILES_MENU_NAME); |
| } |
| |
| } // namespace |
| |
| @interface ProfileMenuController (Private) |
| - (void)initializeMenuWithProfileAttributesStorage: |
| (ProfileAttributesStorage*)storage; |
| - (void)rebuildMenu; |
| @end |
| |
| namespace ProfileMenuControllerInternal { |
| |
| class Observer : public BrowserListObserver, public AvatarMenuObserver { |
| public: |
| explicit Observer(ProfileMenuController* controller) |
| : controller_(controller) { |
| BrowserList::AddObserver(this); |
| } |
| |
| ~Observer() override { BrowserList::RemoveObserver(this); } |
| |
| // BrowserListObserver: |
| void OnBrowserAdded(Browser* browser) override {} |
| void OnBrowserRemoved(Browser* browser) override { |
| [controller_ activeBrowserChangedTo:chrome::FindLastActive()]; |
| } |
| void OnBrowserSetLastActive(Browser* browser) override { |
| [controller_ activeBrowserChangedTo:browser]; |
| } |
| |
| // AvatarMenuObserver: |
| void OnAvatarMenuChanged(AvatarMenu* menu) override { |
| [controller_ rebuildMenu]; |
| } |
| |
| private: |
| ProfileMenuController* controller_; // Weak; owns this. |
| }; |
| |
| } // namespace ProfileMenuControllerInternal |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| @implementation ProfileMenuController { |
| // An observer to be notified when the active browser changes and when the |
| // menu model changes. |
| std::unique_ptr<ProfileMenuControllerInternal::Observer> _observer; |
| |
| // The controller for the profile submenu. |
| std::unique_ptr<AvatarMenu> _avatarMenu; |
| |
| // The main menu item to which the profile menu is attached. |
| NSMenuItem* __strong _mainMenuItem; |
| } |
| |
| - (instancetype)initWithMainMenuItem:(NSMenuItem*)item |
| profileAttributesStorage:(ProfileAttributesStorage*)storage { |
| if ((self = [super init])) { |
| _mainMenuItem = item; |
| |
| _mainMenuItem.submenu = |
| [[NSMenu alloc] initWithTitle:GetProfileMenuTitle()]; |
| |
| // When this object is constructed in non-test code, right after the main |
| // menu is created, that happens before the message loop starts and thus |
| // `g_browser_process` is not yet available. In that case, schedule |
| // initialization on the loop to do work when the browser is ready. For test |
| // code, the required object is available, so initialize immediately to |
| // allow test code to avoid loop spinning calls, which could cause |
| // flakiness. |
| |
| if (storage) { |
| [self initializeMenuWithProfileAttributesStorage:storage]; |
| } else { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [self initializeMenuWithProfileAttributesStorage: |
| &g_browser_process->profile_manager() |
| ->GetProfileAttributesStorage()]; |
| }); |
| } |
| } |
| return self; |
| } |
| |
| - (instancetype)initWithMainMenuItem:(NSMenuItem*)item { |
| return [self initWithMainMenuItem:item profileAttributesStorage:nullptr]; |
| } |
| |
| - (instancetype)initSynchronouslyForTestingWithMainMenuItem:(NSMenuItem*)item |
| profileAttributesStorage: |
| (ProfileAttributesStorage*)storage { |
| return [self initWithMainMenuItem:item profileAttributesStorage:storage]; |
| } |
| |
| - (void)deinitialize { |
| _avatarMenu.reset(); |
| _observer.reset(); |
| } |
| |
| - (IBAction)switchToProfileFromMenu:(id)sender { |
| _avatarMenu->SwitchToProfile([sender tag], false); |
| } |
| |
| - (IBAction)switchToProfileFromDock:(id)sender { |
| // Explicitly bring to the foreground when taking action from the dock. |
| [NSApp activateIgnoringOtherApps:YES]; |
| _avatarMenu->SwitchToProfile([sender tag], false); |
| } |
| |
| - (IBAction)editProfile:(id)sender { |
| std::optional<size_t> active_profile_index = |
| _avatarMenu->GetActiveProfileIndex(); |
| DCHECK(active_profile_index); |
| _avatarMenu->EditProfile(*active_profile_index); |
| } |
| |
| - (IBAction)newProfile:(id)sender { |
| _avatarMenu->AddNewProfile(); |
| } |
| |
| - (BOOL)insertItemsIntoMenu:(NSMenu*)menu |
| atOffset:(NSInteger)offset |
| fromDock:(BOOL)dock { |
| if (!_avatarMenu) { |
| return NO; |
| } |
| |
| // Don't show the list of profiles in the dock if only one profile exists. |
| if (dock && _avatarMenu->GetNumberOfItems() <= 1) { |
| return NO; |
| } |
| |
| if (dock) { |
| NSMenuItem* header; |
| if (@available(macOS 14, *)) { |
| header = [NSMenuItem sectionHeaderWithTitle:GetProfileMenuTitle()]; |
| } else { |
| header = [[NSMenuItem alloc] initWithTitle:GetProfileMenuTitle() |
| action:nil |
| keyEquivalent:@""]; |
| } |
| header.enabled = NO; |
| [menu insertItem:header atIndex:offset++]; |
| } |
| |
| for (size_t i = 0; i < _avatarMenu->GetNumberOfItems(); ++i) { |
| const AvatarMenu::Item& itemData = _avatarMenu->GetItemAt(i); |
| NSString* name = base::SysUTF16ToNSString(itemData.name); |
| SEL action = dock ? @selector(switchToProfileFromDock:) |
| : @selector(switchToProfileFromMenu:); |
| NSMenuItem* item = [self createItemWithTitle:name action:action]; |
| item.tag = itemData.menu_index; |
| if (!dock) { |
| gfx::Image itemIcon = |
| profiles::GetAvatarIconForNSMenu(itemData.profile_path); |
| item.image = itemIcon.ToNSImage(); |
| item.state = |
| itemData.active ? NSControlStateValueOn : NSControlStateValueOff; |
| } |
| [menu insertItem:item atIndex:i + offset]; |
| } |
| |
| return YES; |
| } |
| |
| - (BOOL)validateMenuItem:(NSMenuItem*)menuItem { |
| if (!_avatarMenu->ShouldShowAddNewProfileLink() && |
| menuItem.action == @selector(newProfile:)) { |
| return NO; |
| } |
| |
| if (!_avatarMenu->ShouldShowEditProfileLink() && menuItem.action == @selector |
| (editProfile:)) { |
| return NO; |
| } |
| |
| return YES; |
| } |
| |
| // Private ///////////////////////////////////////////////////////////////////// |
| |
| - (NSMenu*)menu { |
| return _mainMenuItem.submenu; |
| } |
| |
| - (void)initializeMenuWithProfileAttributesStorage: |
| (ProfileAttributesStorage*)storage { |
| _observer = std::make_unique<ProfileMenuControllerInternal::Observer>(self); |
| _avatarMenu = std::make_unique<AvatarMenu>(storage, _observer.get(), |
| /*browser=*/nullptr); |
| _avatarMenu->RebuildMenu(); |
| |
| [self.menu addItem:[NSMenuItem separatorItem]]; |
| |
| NSMenuItem* item = |
| [self createItemWithTitle:l10n_util::GetNSStringWithFixup( |
| IDS_PROFILES_MANAGE_BUTTON_LABEL) |
| action:@selector(editProfile:)]; |
| [self.menu addItem:item]; |
| |
| if (_avatarMenu->ShouldShowAddNewProfileLink()) { |
| [self.menu addItem:[NSMenuItem separatorItem]]; |
| |
| item = [self createItemWithTitle:l10n_util::GetNSStringWithFixup( |
| IDS_PROFILES_ADD_PROFILE_LABEL) |
| action:@selector(newProfile:)]; |
| [self.menu addItem:item]; |
| } |
| |
| [self rebuildMenu]; |
| } |
| |
| // Notifies the controller that the active browser has changed and that the |
| // menu item and menu need to be updated to reflect that. |
| - (void)activeBrowserChangedTo:(Browser*)browser { |
| // Tell the menu that the browser has changed. |
| _avatarMenu->ActiveBrowserChanged(browser); |
| |
| // If |browser| is NULL, it may be because the current profile was deleted |
| // and there are no other loaded profiles. |
| // |
| // An early return provides the desired behavior: |
| // a) If the profile was deleted, the menu would have been rebuilt and no |
| // profile will have a check mark. |
| // b) If the profile was not deleted, but there is no active browser, then |
| // the previous profile will remain checked. |
| if (!browser) { |
| return; |
| } |
| |
| // Update the avatar menu to get the active item states. Don't call |
| // avatarMenu_->GetActiveProfileIndex() as the index might be |
| // incorrect if -activeBrowserChangedTo: is called while we deleting the |
| // active profile and closing all its browser windows. |
| _avatarMenu->RebuildMenu(); |
| |
| // Update the state for the menu items. |
| for (size_t i = 0; i < _avatarMenu->GetNumberOfItems(); ++i) { |
| const AvatarMenu::Item& itemData = _avatarMenu->GetItemAt(i); |
| [[self.menu itemWithTag:itemData.menu_index] |
| setState:itemData.active ? NSControlStateValueOn |
| : NSControlStateValueOff]; |
| } |
| } |
| |
| - (void)rebuildMenu { |
| NSMenu* menu = self.menu; |
| |
| for (NSMenuItem* item = [menu itemAtIndex:0]; !item.separatorItem; |
| item = [menu itemAtIndex:0]) { |
| [menu removeItemAtIndex:0]; |
| } |
| |
| BOOL hasContent = [self insertItemsIntoMenu:menu atOffset:0 fromDock:NO]; |
| |
| _mainMenuItem.hidden = !hasContent; |
| } |
| |
| - (NSMenuItem*)createItemWithTitle:(NSString*)title action:(SEL)sel { |
| NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:title |
| action:sel |
| keyEquivalent:@""]; |
| item.target = self; |
| return item; |
| } |
| |
| @end |