| // Copyright 2018 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. |
| |
| #include "chrome/browser/ui/views/profiles/avatar_toolbar_button.h" |
| |
| #include "build/build_config.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "chrome/app/vector_icons/vector_icons.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/profiles/avatar_menu.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_avatar_icon_util.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/profiles/profiles_state.h" |
| #include "chrome/browser/signin/account_consistency_mode_manager.h" |
| #include "chrome/browser/signin/identity_manager_factory.h" |
| #include "chrome/browser/signin/signin_ui_util.h" |
| #include "chrome/browser/sync/sync_ui_util.h" |
| #include "chrome/browser/themes/theme_properties.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/layout_constants.h" |
| #include "chrome/browser/ui/view_ids.h" |
| #include "chrome/browser/ui/views/chrome_layout_provider.h" |
| #include "chrome/browser/ui/views/profiles/incognito_window_count_view.h" |
| #include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "services/identity/public/cpp/identity_manager.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/material_design/material_design_controller.h" |
| #include "ui/base/models/menu_model.h" |
| #include "ui/base/theme_provider.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/views/controls/button/label_button_border.h" |
| |
| namespace { |
| |
| ProfileAttributesEntry* GetProfileAttributesEntry(Profile* profile) { |
| ProfileAttributesEntry* entry; |
| if (!g_browser_process->profile_manager() |
| ->GetProfileAttributesStorage() |
| .GetProfileAttributesWithPath(profile->GetPath(), &entry)) { |
| return nullptr; |
| } |
| return entry; |
| } |
| |
| } // namespace |
| |
| AvatarToolbarButton::AvatarToolbarButton(Browser* browser) |
| : ToolbarButton(nullptr), |
| browser_(browser), |
| profile_(browser_->profile()), |
| #if !defined(OS_CHROMEOS) |
| error_controller_(this, profile_), |
| #endif // !defined(OS_CHROMEOS) |
| browser_list_observer_(this), |
| profile_observer_(this), |
| identity_manager_observer_(this) { |
| |
| if (IsIncognitoCounterActive()) |
| browser_list_observer_.Add(BrowserList::GetInstance()); |
| |
| profile_observer_.Add( |
| &g_browser_process->profile_manager()->GetProfileAttributesStorage()); |
| |
| if (!IsIncognito() && !profile_->IsGuestSession()) { |
| identity_manager_observer_.Add( |
| IdentityManagerFactory::GetForProfile(profile_)); |
| } |
| |
| // Activate on press for left-mouse-button only to mimic other MenuButtons |
| // without drag-drop actions (specifically the adjacent browser menu). |
| set_notify_action(Button::NOTIFY_ON_PRESS); |
| set_triggerable_event_flags(ui::EF_LEFT_MOUSE_BUTTON); |
| |
| set_tag(IDC_SHOW_AVATAR_MENU); |
| |
| // The avatar should not flip with RTL UI. This does not affect text rendering |
| // and LabelButton image/label placement is still flipped like usual. |
| EnableCanvasFlippingForRTLUI(false); |
| |
| Init(); |
| |
| #if defined(OS_CHROMEOS) |
| // On CrOS the avatar toolbar button should only show as badging for Incognito |
| // and Guest sessions. It should not be instantiated for regular profiles and |
| // it should not be enabled as there's no profile switcher to trigger / show, |
| // unless incognito window counter is available. |
| DCHECK(IsIncognito() || profile_->IsGuestSession()); |
| SetEnabled(IsIncognitoCounterActive()); |
| #else |
| // The profile switcher is only available outside incognito or if incognito |
| // window counter is enabled. |
| SetEnabled(!IsIncognito() || IsIncognitoCounterActive()); |
| #endif // !defined(OS_CHROMEOS) |
| |
| // The incognito window counter uses left-aligned text. |
| if (IsIncognitoCounterActive()) |
| SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| |
| // Set initial text and tooltip. UpdateIcon() needs to be called from the |
| // outside as GetThemeProvider() is not available until the button is added to |
| // ToolbarView's hierarchy. |
| UpdateText(); |
| |
| md_observer_.Add(ui::MaterialDesignController::GetInstance()); |
| } |
| |
| AvatarToolbarButton::~AvatarToolbarButton() {} |
| |
| void AvatarToolbarButton::UpdateIcon() { |
| SetImage(views::Button::STATE_NORMAL, GetAvatarIcon()); |
| } |
| |
| void AvatarToolbarButton::UpdateText() { |
| base::Optional<SkColor> color; |
| base::string16 text; |
| |
| const SyncState sync_state = GetSyncState(); |
| |
| if (IsIncognito() && GetThemeProvider()) { |
| color = GetThemeProvider()->GetColor( |
| ThemeProperties::COLOR_TOOLBAR_BUTTON_ICON); |
| // TODO(pbos): Remove this once the incognito chip is always enabled and |
| // triggers a menu. |
| SetTextColor(STATE_DISABLED, *color); |
| } |
| |
| if (IsIncognitoCounterActive()) { |
| const int incognito_window_count = |
| BrowserList::GetIncognitoSessionsActiveForProfile(profile_); |
| if (incognito_window_count > 1) |
| text = base::NumberToString16(incognito_window_count); |
| } else if (IsIncognito()) { |
| text = l10n_util::GetStringUTF16(IDS_AVATAR_BUTTON_INCOGNITO); |
| } else if (sync_state == SyncState::kError) { |
| color = |
| AdjustHighlightColorForContrast(gfx::kGoogleRed300, gfx::kGoogleRed600, |
| gfx::kGoogleRed050, gfx::kGoogleRed900); |
| text = l10n_util::GetStringUTF16(IDS_AVATAR_BUTTON_SYNC_ERROR); |
| } else if (sync_state == SyncState::kPaused) { |
| color = AdjustHighlightColorForContrast( |
| gfx::kGoogleBlue300, gfx::kGoogleBlue600, gfx::kGoogleBlue050, |
| gfx::kGoogleBlue900); |
| |
| text = l10n_util::GetStringUTF16(IDS_AVATAR_BUTTON_SYNC_PAUSED); |
| } |
| |
| SetInsets(); |
| SetHighlightColor(color); |
| SetText(text); |
| SetTooltipText(GetAvatarTooltipText()); |
| } |
| |
| void AvatarToolbarButton::NotifyClick(const ui::Event& event) { |
| Button::NotifyClick(event); |
| // TODO(bsep): Other toolbar buttons have ToolbarView as a listener and let it |
| // call ExecuteCommandWithDisposition on their behalf. Unfortunately, it's not |
| // possible to plumb IsKeyEvent through, so this has to be a special case. |
| if (IsIncognitoCounterActive()) { |
| IncognitoWindowCountView::ShowBubble( |
| this, browser_, |
| BrowserList::GetIncognitoSessionsActiveForProfile(profile_)); |
| } else { |
| browser_->window()->ShowAvatarBubbleFromAvatarButton( |
| BrowserWindow::AVATAR_BUBBLE_MODE_DEFAULT, |
| signin::ManageAccountsParams(), |
| signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN, |
| event.IsKeyEvent()); |
| } |
| } |
| |
| void AvatarToolbarButton::OnThemeChanged() { |
| UpdateIcon(); |
| UpdateText(); |
| } |
| |
| void AvatarToolbarButton::AddedToWidget() { |
| UpdateText(); |
| } |
| |
| void AvatarToolbarButton::OnAvatarErrorChanged() { |
| UpdateIcon(); |
| UpdateText(); |
| } |
| |
| void AvatarToolbarButton::OnBrowserAdded(Browser* browser) { |
| UpdateIcon(); |
| UpdateText(); |
| } |
| |
| void AvatarToolbarButton::OnBrowserRemoved(Browser* browser) { |
| UpdateIcon(); |
| UpdateText(); |
| } |
| |
| void AvatarToolbarButton::OnProfileAdded(const base::FilePath& profile_path) { |
| // Adding any profile changes the profile count, we might go from showing a |
| // generic avatar button to profile pictures here. Update icon accordingly. |
| UpdateIcon(); |
| } |
| |
| void AvatarToolbarButton::OnProfileWasRemoved( |
| const base::FilePath& profile_path, |
| const base::string16& profile_name) { |
| // Removing a profile changes the profile count, we might go from showing |
| // per-profile icons back to a generic avatar icon. Update icon accordingly. |
| UpdateIcon(); |
| } |
| |
| void AvatarToolbarButton::OnProfileAvatarChanged( |
| const base::FilePath& profile_path) { |
| UpdateIcon(); |
| } |
| |
| void AvatarToolbarButton::OnProfileHighResAvatarLoaded( |
| const base::FilePath& profile_path) { |
| UpdateIcon(); |
| } |
| |
| void AvatarToolbarButton::OnProfileNameChanged( |
| const base::FilePath& profile_path, |
| const base::string16& old_profile_name) { |
| UpdateText(); |
| } |
| |
| void AvatarToolbarButton::OnAccountsInCookieUpdated( |
| const identity::AccountsInCookieJarInfo& accounts_in_cookie_jar_info, |
| const GoogleServiceAuthError& error) { |
| UpdateIcon(); |
| } |
| |
| void AvatarToolbarButton::OnAccountUpdated(const AccountInfo& info) { |
| UpdateIcon(); |
| } |
| |
| void AvatarToolbarButton::OnAccountRemovedWithInfo(const AccountInfo& info) { |
| UpdateIcon(); |
| } |
| |
| void AvatarToolbarButton::OnTouchUiChanged() { |
| SetInsets(); |
| PreferredSizeChanged(); |
| } |
| |
| bool AvatarToolbarButton::IsIncognito() const { |
| return profile_->IsOffTheRecord() && !profile_->IsGuestSession(); |
| } |
| |
| bool AvatarToolbarButton::IsIncognitoCounterActive() const { |
| return IsIncognito() && |
| base::FeatureList::IsEnabled(features::kEnableIncognitoWindowCounter); |
| } |
| |
| bool AvatarToolbarButton::ShouldShowGenericIcon() const { |
| // This function should only be used for regular profiles. Guest and Incognito |
| // sessions should be handled separately and never call this function. |
| DCHECK(!profile_->IsGuestSession()); |
| DCHECK(!profile_->IsOffTheRecord()); |
| #if !defined(OS_CHROMEOS) |
| if (!signin_ui_util::GetAccountsForDicePromos(profile_).empty()) |
| return false; |
| #endif // !defined(OS_CHROMEOS) |
| |
| ProfileAttributesEntry* entry = GetProfileAttributesEntry(profile_); |
| if (!entry) { |
| // This can happen if the user deletes the current profile. |
| return true; |
| } |
| return entry->IsUsingDefaultAvatar() && |
| g_browser_process->profile_manager() |
| ->GetProfileAttributesStorage() |
| .GetNumberOfProfiles() == 1 && |
| !IdentityManagerFactory::GetForProfile(profile_)->HasPrimaryAccount(); |
| } |
| |
| base::string16 AvatarToolbarButton::GetAvatarTooltipText() const { |
| if (IsIncognito()) |
| return l10n_util::GetStringUTF16(IDS_AVATAR_BUTTON_INCOGNITO_TOOLTIP); |
| |
| if (profile_->IsGuestSession()) |
| return l10n_util::GetStringUTF16(IDS_GUEST_PROFILE_NAME); |
| |
| if (ShouldShowGenericIcon()) |
| return l10n_util::GetStringUTF16(IDS_GENERIC_USER_AVATAR_LABEL); |
| |
| const base::string16 profile_name = |
| profiles::GetAvatarNameForProfile(profile_->GetPath()); |
| switch (GetSyncState()) { |
| case SyncState::kNormal: |
| return profile_name; |
| case SyncState::kPaused: |
| return l10n_util::GetStringFUTF16(IDS_AVATAR_BUTTON_SYNC_PAUSED_TOOLTIP, |
| profile_name); |
| case SyncState::kError: |
| return l10n_util::GetStringFUTF16(IDS_AVATAR_BUTTON_SYNC_ERROR_TOOLTIP, |
| profile_name); |
| } |
| |
| NOTREACHED(); |
| return base::string16(); |
| } |
| |
| gfx::ImageSkia AvatarToolbarButton::GetAvatarIcon() const { |
| const int icon_size = ui::MaterialDesignController::touch_ui() ? 24 : 20; |
| |
| SkColor icon_color = |
| GetThemeProvider()->GetColor(ThemeProperties::COLOR_TOOLBAR_BUTTON_ICON); |
| |
| if (IsIncognito()) |
| return gfx::CreateVectorIcon(kIncognitoIcon, icon_size, icon_color); |
| |
| if (profile_->IsGuestSession()) |
| return gfx::CreateVectorIcon(kUserMenuGuestIcon, icon_size, icon_color); |
| |
| gfx::Image avatar_icon; |
| if (!ShouldShowGenericIcon()) |
| avatar_icon = GetIconImageFromProfile(); |
| |
| if (!avatar_icon.IsEmpty()) { |
| return profiles::GetSizedAvatarIcon(avatar_icon, true, icon_size, icon_size, |
| profiles::SHAPE_CIRCLE) |
| .AsImageSkia(); |
| } |
| |
| return gfx::CreateVectorIcon(kUserAccountAvatarIcon, icon_size, icon_color); |
| } |
| |
| gfx::Image AvatarToolbarButton::GetIconImageFromProfile() const { |
| ProfileAttributesEntry* entry = GetProfileAttributesEntry(profile_); |
| if (!entry) { |
| // This can happen if the user deletes the current profile. |
| return gfx::Image(); |
| } |
| |
| // If there is a GAIA image available, try to use that. |
| if (entry->IsUsingGAIAPicture()) { |
| // The GetGAIAPicture API call will trigger an async image load from disk if |
| // it has not been loaded. |
| const gfx::Image* gaia_image = entry->GetGAIAPicture(); |
| |
| if (gaia_image) |
| return *gaia_image; |
| return gfx::Image(); |
| } |
| |
| #if !defined(OS_CHROMEOS) |
| // Try to show the first account icon of the sync promo when the following |
| // conditions are satisfied: |
| // - the user is migrated to Dice |
| // - the user isn't signed in |
| // - the profile icon wasn't explicitly changed |
| if (AccountConsistencyModeManager::IsDiceEnabledForProfile(profile_) && |
| !IdentityManagerFactory::GetForProfile(profile_)->HasPrimaryAccount() && |
| entry->IsUsingDefaultAvatar()) { |
| std::vector<AccountInfo> promo_accounts = |
| signin_ui_util::GetAccountsForDicePromos(profile_); |
| if (!promo_accounts.empty()) { |
| return promo_accounts.front().account_image; |
| } |
| } |
| #endif // !defined(OS_CHROMEOS) |
| |
| return entry->GetAvatarIcon(); |
| } |
| |
| AvatarToolbarButton::SyncState AvatarToolbarButton::GetSyncState() const { |
| #if !defined(OS_CHROMEOS) |
| identity::IdentityManager* identity_manager = |
| IdentityManagerFactory::GetForProfile(profile_); |
| if (identity_manager && identity_manager->HasPrimaryAccount() && |
| profile_->IsSyncAllowed() && error_controller_.HasAvatarError()) { |
| // When DICE is enabled and the error is an auth error, the sync-paused |
| // icon is shown. |
| int unused; |
| const bool should_show_sync_paused_ui = |
| AccountConsistencyModeManager::IsDiceEnabledForProfile(profile_) && |
| sync_ui_util::GetMessagesForAvatarSyncError( |
| profile_, &unused, &unused) == sync_ui_util::AUTH_ERROR; |
| return should_show_sync_paused_ui ? SyncState::kPaused : SyncState::kError; |
| } |
| #endif // !defined(OS_CHROMEOS) |
| return SyncState::kNormal; |
| } |
| |
| void AvatarToolbarButton::SetInsets() { |
| // In non-touch mode we use a larger-than-normal icon size for avatars as 16dp |
| // is hard to read for user avatars, so we need to set corresponding insets. |
| gfx::Insets layout_insets(ui::MaterialDesignController::touch_ui() ? 0 : -2); |
| |
| // When the incognito counter is displaying, we need to add additional insets |
| // to the icon side because the existing ones don't look balanced. |
| if (IsIncognitoCounterActive()) { |
| const int incognito_window_count = |
| BrowserList::GetIncognitoSessionsActiveForProfile(profile_); |
| if (incognito_window_count > 1) { |
| const int highlight_radius = |
| ChromeLayoutProvider::Get()->GetCornerRadiusMetric( |
| views::EMPHASIS_MAXIMUM, size()); |
| // These additional insets have been chosen to look reasonably well and |
| // scale with the (touchable or not) UI. |
| layout_insets.set_left(layout_insets.left() + highlight_radius / 4); |
| } |
| } |
| |
| SetLayoutInsetDelta(layout_insets); |
| } |
| |
| SkColor AvatarToolbarButton::AdjustHighlightColorForContrast( |
| SkColor desired_dark_color, |
| SkColor desired_light_color, |
| SkColor dark_extreme, |
| SkColor light_extreme) const { |
| const ui::ThemeProvider* theme_provider = GetThemeProvider(); |
| if (!theme_provider) |
| return desired_light_color; |
| SkColor toolbar_color = |
| GetThemeProvider()->GetColor(ThemeProperties::COLOR_TOOLBAR); |
| SkColor contrasting_color = color_utils::PickContrastingColor( |
| desired_dark_color, desired_light_color, toolbar_color); |
| SkColor limit = |
| contrasting_color == desired_dark_color ? dark_extreme : light_extreme; |
| // Setting highlight color will set the text to the highlight color, and the |
| // background to the same color with a low alpha. This means that our target |
| // contrast is between the text (the highlight color) and a blend of the |
| // highlight color and the toolbar color. |
| SkColor base_color = color_utils::AlphaBlend(contrasting_color, toolbar_color, |
| kToolbarButtonBackgroundAlpha); |
| |
| // Add a fudge factor to the minimum contrast ratio since we'll actually be |
| // blending with the adjusted color. |
| SkAlpha blend_alpha = color_utils::GetBlendValueWithMinimumContrast( |
| contrasting_color, limit, base_color, |
| color_utils::kMinimumReadableContrastRatio * 1.05); |
| |
| return color_utils::AlphaBlend(limit, contrasting_color, blend_alpha); |
| } |