blob: ec738c361536bff856fa7827ebfea5d6dc856722 [file] [log] [blame]
// Copyright (c) 2012 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/frame/browser_non_client_frame_view.h"
#include "base/metrics/histogram_macros.h"
#include "build/build_config.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_attributes_entry.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/extensions/hosted_app_browser_controller.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/theme_resources.h"
#include "components/signin/core/browser/profile_management_switches.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/material_design/material_design_controller.h"
#include "ui/base/theme_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/views/background.h"
#if defined(OS_CHROMEOS)
#include "chrome/browser/ui/ash/multi_user/multi_user_window_manager.h"
#include "chrome/browser/ui/ash/session_util.h"
#endif // defined(OS_CHROMEOS)
#if defined(OS_WIN)
#include "chrome/browser/ui/views/frame/taskbar_decorator_win.h"
#endif
using MD = ui::MaterialDesignController;
BrowserNonClientFrameView::BrowserNonClientFrameView(BrowserFrame* frame,
BrowserView* browser_view)
: frame_(frame),
browser_view_(browser_view),
profile_switcher_(this),
profile_indicator_icon_(nullptr),
tab_strip_observer_(this) {
// The profile manager may by null in tests.
if (g_browser_process->profile_manager()) {
g_browser_process->profile_manager()->
GetProfileAttributesStorage().AddObserver(this);
}
MaybeObserveTabstrip();
}
BrowserNonClientFrameView::~BrowserNonClientFrameView() {
// The profile manager may by null in tests.
if (g_browser_process->profile_manager()) {
g_browser_process->profile_manager()->
GetProfileAttributesStorage().RemoveObserver(this);
}
}
// static
int BrowserNonClientFrameView::GetAvatarIconPadding() {
return MD::IsNewerMaterialUi() ? 8 : 4;
}
// static
int BrowserNonClientFrameView::GetTabstripPadding() {
// In Refresh, the apparent padding around the tabstrip is contained within
// the tabs and/or new tab button.
return MD::IsRefreshUi() ? 0 : 4;
}
void BrowserNonClientFrameView::OnBrowserViewInitViewsComplete() {
MaybeObserveTabstrip();
OnSingleTabModeChanged();
UpdateMinimumSize();
}
void BrowserNonClientFrameView::OnMaximizedStateChanged() {}
void BrowserNonClientFrameView::OnFullscreenStateChanged() {}
bool BrowserNonClientFrameView::CaptionButtonsOnLeadingEdge() const {
return false;
}
void BrowserNonClientFrameView::UpdateFullscreenTopUI(
bool is_exiting_fullscreen) {}
bool BrowserNonClientFrameView::ShouldHideTopUIForFullscreen() const {
return frame()->IsFullscreen();
}
gfx::ImageSkia BrowserNonClientFrameView::GetIncognitoAvatarIcon() const {
const SkColor icon_color = color_utils::PickContrastingColor(
SK_ColorWHITE, gfx::kChromeIconGrey, GetFrameColor());
return gfx::CreateVectorIcon(kIncognitoIcon, icon_color);
}
SkColor BrowserNonClientFrameView::GetToolbarTopSeparatorColor() const {
const auto color_id =
ShouldPaintAsActive()
? ThemeProperties::COLOR_TOOLBAR_TOP_SEPARATOR
: ThemeProperties::COLOR_TOOLBAR_TOP_SEPARATOR_INACTIVE;
return GetThemeOrDefaultColor(color_id);
}
SkColor BrowserNonClientFrameView::GetTabSeparatorColor() const {
DCHECK(MD::IsRefreshUi());
constexpr SkAlpha kTabSeparatorAlpha = 0x4D; // 30%
const SkColor frame_color = GetFrameColor();
const SkColor base_color =
color_utils::BlendTowardOppositeLuma(frame_color, SK_AlphaOPAQUE);
return color_utils::AlphaBlend(base_color, frame_color, kTabSeparatorAlpha);
}
SkColor BrowserNonClientFrameView::GetTabBackgroundColor(TabState state) const {
if (state == TAB_INACTIVE && MD::IsRefreshUi())
return GetFrameColor();
const auto color_id = state == TAB_ACTIVE
? ThemeProperties::COLOR_TOOLBAR
: ThemeProperties::COLOR_BACKGROUND_TAB;
return GetThemeOrDefaultColor(color_id);
}
SkColor BrowserNonClientFrameView::GetTabForegroundColor(TabState state) const {
if (MD::IsRefreshUi() && state == TAB_INACTIVE &&
!GetThemeProvider()->HasCustomColor(
ThemeProperties::COLOR_BACKGROUND_TAB_TEXT)) {
return color_utils::IsDark(GetFrameColor()) ? SK_ColorWHITE
: gfx::kGoogleGrey800;
}
const auto color_id = state == TAB_ACTIVE
? ThemeProperties::COLOR_TAB_TEXT
: ThemeProperties::COLOR_BACKGROUND_TAB_TEXT;
return GetThemeOrDefaultColor(color_id);
}
views::Button* BrowserNonClientFrameView::GetProfileSwitcherButton() const {
return profile_switcher_.avatar_button();
}
void BrowserNonClientFrameView::UpdateClientArea() {}
void BrowserNonClientFrameView::UpdateMinimumSize() {}
int BrowserNonClientFrameView::GetTabStripLeftInset() const {
int left_inset = GetTabstripPadding();
if (profile_indicator_icon())
left_inset += GetAvatarIconPadding() + GetIncognitoAvatarIcon().width();
return left_inset;
}
void BrowserNonClientFrameView::ChildPreferredSizeChanged(views::View* child) {
if (child == GetProfileSwitcherButton()) {
// Perform a re-layout if the avatar button has changed, since that can
// affect the size of the tabs.
frame()->GetRootView()->Layout();
}
}
void BrowserNonClientFrameView::VisibilityChanged(views::View* starting_from,
bool is_visible) {
// UpdateTaskbarDecoration() calls DrawTaskbarDecoration(), but that does
// nothing if the window is not visible. So even if we've already gotten the
// up-to-date decoration, we need to run the update procedure again here when
// the window becomes visible.
if (is_visible)
OnProfileAvatarChanged(base::FilePath());
}
void BrowserNonClientFrameView::OnSingleTabModeChanged() {
SchedulePaint();
}
bool BrowserNonClientFrameView::ShouldPaintAsThemed() const {
return browser_view_->IsBrowserTypeNormal();
}
bool BrowserNonClientFrameView::IsSingleTabModeAvailable() const {
// Single-tab mode is only available in Refresh and when the window is active.
// The special color we use won't be visible if there's a frame image, but
// since it's used to determine constrast of other UI elements, the theme
// color should be used instead.
return base::FeatureList::IsEnabled(features::kSingleTabMode) &&
MD::IsRefreshUi() && ShouldPaintAsActive() && GetFrameImage().isNull();
}
bool BrowserNonClientFrameView::ShouldPaintAsSingleTabMode() const {
return browser_view()->IsTabStripVisible() &&
browser_view()->tabstrip()->SingleTabMode();
}
SkColor BrowserNonClientFrameView::GetFrameColor(bool active) const {
extensions::HostedAppBrowserController* hosted_app_controller =
browser_view()->browser()->hosted_app_controller();
if (hosted_app_controller && hosted_app_controller->GetThemeColor())
return *hosted_app_controller->GetThemeColor();
ThemeProperties::OverwritableByUserThemeProperty color_id;
if (ShouldPaintAsSingleTabMode()) {
color_id = ThemeProperties::COLOR_TOOLBAR;
} else {
color_id = active ? ThemeProperties::COLOR_FRAME
: ThemeProperties::COLOR_FRAME_INACTIVE;
}
return ShouldPaintAsThemed()
? GetThemeProviderForProfile()->GetColor(color_id)
: ThemeProperties::GetDefaultColor(color_id,
browser_view_->IsIncognito());
}
gfx::ImageSkia BrowserNonClientFrameView::GetFrameImage(bool active) const {
const ui::ThemeProvider* tp = GetThemeProviderForProfile();
int frame_image_id = active ? IDR_THEME_FRAME : IDR_THEME_FRAME_INACTIVE;
return ShouldPaintAsThemed() && (tp->HasCustomImage(frame_image_id) ||
tp->HasCustomImage(IDR_THEME_FRAME))
? *tp->GetImageSkiaNamed(frame_image_id)
: gfx::ImageSkia();
}
gfx::ImageSkia BrowserNonClientFrameView::GetFrameOverlayImage(
bool active) const {
if (browser_view_->IsIncognito() || !browser_view_->IsBrowserTypeNormal())
return gfx::ImageSkia();
const ui::ThemeProvider* tp = GetThemeProviderForProfile();
int frame_overlay_image_id =
active ? IDR_THEME_FRAME_OVERLAY : IDR_THEME_FRAME_OVERLAY_INACTIVE;
return tp->HasCustomImage(frame_overlay_image_id)
? *tp->GetImageSkiaNamed(frame_overlay_image_id)
: gfx::ImageSkia();
}
SkColor BrowserNonClientFrameView::GetFrameColor() const {
return GetFrameColor(ShouldPaintAsActive());
}
gfx::ImageSkia BrowserNonClientFrameView::GetFrameImage() const {
return GetFrameImage(ShouldPaintAsActive());
}
gfx::ImageSkia BrowserNonClientFrameView::GetFrameOverlayImage() const {
return GetFrameOverlayImage(ShouldPaintAsActive());
}
void BrowserNonClientFrameView::UpdateProfileIcons() {
const AvatarButtonStyle avatar_button_style = GetAvatarButtonStyle();
if (avatar_button_style != AvatarButtonStyle::NONE &&
browser_view()->IsRegularOrGuestSession()) {
// Platform supports a profile switcher that will be shown. Skip the rest.
profile_switcher_.Update(avatar_button_style);
return;
}
if (!ShouldShowProfileIndicatorIcon()) {
if (profile_indicator_icon_) {
delete profile_indicator_icon_;
profile_indicator_icon_ = nullptr;
frame_->GetRootView()->Layout();
}
return;
}
if (!profile_indicator_icon_) {
profile_indicator_icon_ = new ProfileIndicatorIcon();
profile_indicator_icon_->set_id(VIEW_ID_PROFILE_INDICATOR_ICON);
AddChildView(profile_indicator_icon_);
// Invalidate here because adding a child does not invalidate the layout.
InvalidateLayout();
frame_->GetRootView()->Layout();
}
gfx::Image icon;
Profile* profile = browser_view()->browser()->profile();
const bool is_incognito =
profile->GetProfileType() == Profile::INCOGNITO_PROFILE;
if (is_incognito) {
icon = gfx::Image(GetIncognitoAvatarIcon());
profile_indicator_icon_->set_stroke_color(SK_ColorTRANSPARENT);
} else {
#if defined(OS_CHROMEOS)
icon = gfx::Image(GetAvatarImageForContext(profile));
// Draw a stroke around the profile icon only for the avatar.
profile_indicator_icon_->set_stroke_color(GetToolbarTopSeparatorColor());
#else
NOTREACHED();
#endif
}
profile_indicator_icon_->SetIcon(icon);
}
void BrowserNonClientFrameView::LayoutIncognitoButton() {
DCHECK(profile_indicator_icon());
#if !defined(OS_CHROMEOS)
// ChromeOS shows avatar on V1 app.
DCHECK(browser_view()->IsTabStripVisible());
#endif
gfx::ImageSkia incognito_icon = GetIncognitoAvatarIcon();
int avatar_bottom = GetTopInset(false) + browser_view()->GetTabStripHeight() -
GetAvatarIconPadding();
int avatar_y = avatar_bottom - incognito_icon.height();
int avatar_height = incognito_icon.height();
gfx::Rect avatar_bounds(GetAvatarIconPadding(), avatar_y,
incognito_icon.width(), avatar_height);
profile_indicator_icon()->SetBoundsRect(avatar_bounds);
profile_indicator_icon()->SetVisible(true);
}
void BrowserNonClientFrameView::PaintToolbarTopStroke(
gfx::Canvas* canvas) const {
if (TabStrip::ShouldDrawStrokes()) {
gfx::Rect toolbar_bounds(browser_view()->GetToolbarBounds());
gfx::Rect tabstrip_bounds =
GetMirroredRect(GetBoundsForTabStrip(browser_view()->tabstrip()));
gfx::ScopedCanvas scoped_canvas(canvas);
canvas->ClipRect(tabstrip_bounds, SkClipOp::kDifference);
const gfx::Rect separator_rect(toolbar_bounds.x(), tabstrip_bounds.bottom(),
toolbar_bounds.width(), 0);
BrowserView::Paint1pxHorizontalLine(canvas, GetToolbarTopSeparatorColor(),
separator_rect, true);
}
}
void BrowserNonClientFrameView::ViewHierarchyChanged(
const ViewHierarchyChangedDetails& details) {
if (details.is_add && details.child == this)
UpdateProfileIcons();
}
void BrowserNonClientFrameView::ActivationChanged(bool active) {
// On Windows, while deactivating the widget, this is called before the
// active HWND has actually been changed. Since we want the avatar state to
// reflect that the window is inactive, we force NonClientFrameView to see the
// "correct" state as an override.
set_active_state_override(&active);
UpdateProfileIcons();
if (MD::IsRefreshUi()) {
// Single-tab mode's availability depends on activation, but even if it's
// unavailable for other reasons the inactive tabs' text color still needs
// to be recalculated if the frame color changes. SingleTabModeChanged will
// handle both cases.
browser_view_->tabstrip()->SingleTabModeChanged();
} else {
// The toolbar top separator color (used as the stroke around the tabs and
// the new tab button) needs to be recalculated.
browser_view_->tabstrip()->FrameColorsChanged();
}
set_active_state_override(nullptr);
// Changing the activation state may change the visible frame color.
SchedulePaint();
}
bool BrowserNonClientFrameView::DoesIntersectRect(const views::View* target,
const gfx::Rect& rect) const {
DCHECK_EQ(target, this);
if (!views::ViewTargeterDelegate::DoesIntersectRect(this, rect)) {
// |rect| is outside the frame's bounds.
return false;
}
bool should_leave_to_top_container = false;
#if defined(OS_CHROMEOS)
// In immersive mode, the caption buttons container is reparented to the
// TopContainerView and hence |rect| should not be claimed here. See
// BrowserNonClientFrameViewAsh::OnImmersiveRevealStarted().
should_leave_to_top_container =
browser_view()->immersive_mode_controller()->IsRevealed();
#endif // defined(OS_CHROMEOS)
if (!browser_view()->IsTabStripVisible()) {
// Claim |rect| if it is above the top of the topmost client area view.
return !should_leave_to_top_container && (rect.y() < GetTopInset(false));
}
// If the rect is outside the bounds of the client area, claim it.
gfx::RectF rect_in_client_view_coords_f(rect);
View::ConvertRectToTarget(this, frame()->client_view(),
&rect_in_client_view_coords_f);
gfx::Rect rect_in_client_view_coords =
gfx::ToEnclosingRect(rect_in_client_view_coords_f);
if (!frame()->client_view()->HitTestRect(rect_in_client_view_coords))
return true;
// Otherwise, claim |rect| only if it is above the bottom of the tabstrip in
// a non-tab portion.
TabStrip* tabstrip = browser_view()->tabstrip();
gfx::RectF rect_in_tabstrip_coords_f(rect);
View::ConvertRectToTarget(this, tabstrip, &rect_in_tabstrip_coords_f);
gfx::Rect rect_in_tabstrip_coords =
gfx::ToEnclosingRect(rect_in_tabstrip_coords_f);
if (rect_in_tabstrip_coords.y() >= tabstrip->GetLocalBounds().bottom()) {
// |rect| is below the tabstrip.
return false;
}
if (tabstrip->HitTestRect(rect_in_tabstrip_coords)) {
// Claim |rect| if it is in a non-tab portion of the tabstrip.
return tabstrip->IsRectInWindowCaption(rect_in_tabstrip_coords);
}
// We claim |rect| because it is above the bottom of the tabstrip, but
// not in the tabstrip itself. In particular, the avatar label/button is left
// of the tabstrip and the window controls are right of the tabstrip.
return !should_leave_to_top_container;
}
void BrowserNonClientFrameView::OnProfileAdded(
const base::FilePath& profile_path) {
OnProfileAvatarChanged(profile_path);
}
void BrowserNonClientFrameView::OnProfileWasRemoved(
const base::FilePath& profile_path,
const base::string16& profile_name) {
OnProfileAvatarChanged(profile_path);
}
void BrowserNonClientFrameView::OnProfileAvatarChanged(
const base::FilePath& profile_path) {
UpdateTaskbarDecoration();
UpdateProfileIcons();
}
void BrowserNonClientFrameView::OnProfileHighResAvatarLoaded(
const base::FilePath& profile_path) {
UpdateTaskbarDecoration();
}
void BrowserNonClientFrameView::MaybeObserveTabstrip() {
if (browser_view()->tabstrip()) {
DCHECK(!tab_strip_observer_.IsObserving(browser_view()->tabstrip()));
tab_strip_observer_.Add(browser_view()->tabstrip());
}
}
const ui::ThemeProvider*
BrowserNonClientFrameView::GetThemeProviderForProfile() const {
// Because the frame's accessor reads the ThemeProvider from the profile and
// not the widget, it can be called even before we're in a view hierarchy.
return frame_->GetThemeProvider();
}
void BrowserNonClientFrameView::UpdateTaskbarDecoration() {
#if defined(OS_WIN)
if (browser_view()->browser()->profile()->IsGuestSession() ||
// Browser process and profile manager may be null in tests.
(g_browser_process && g_browser_process->profile_manager() &&
g_browser_process->profile_manager()
->GetProfileAttributesStorage()
.GetNumberOfProfiles() <= 1)) {
chrome::DrawTaskbarDecoration(frame_->GetNativeWindow(), nullptr);
return;
}
// For popups and panels which don't have the avatar button, we still
// need to draw the taskbar decoration. Even though we have an icon on the
// window's relaunch details, we draw over it because the user may have
// pinned the badge-less Chrome shortcut which will cause Windows to ignore
// the relaunch details.
// TODO(calamity): ideally this should not be necessary but due to issues
// with the default shortcut being pinned, we add the runtime badge for
// safety. See crbug.com/313800.
gfx::Image decoration;
AvatarMenu::ImageLoadStatus status = AvatarMenu::GetImageForMenuButton(
browser_view()->browser()->profile()->GetPath(), &decoration);
UMA_HISTOGRAM_ENUMERATION(
"Profile.AvatarLoadStatus", status,
static_cast<int>(AvatarMenu::ImageLoadStatus::MAX) + 1);
// If the user is using a Gaia picture and the picture is still being loaded,
// wait until the load finishes. This taskbar decoration will be triggered
// again upon the finish of the picture load.
if (status == AvatarMenu::ImageLoadStatus::LOADING ||
status == AvatarMenu::ImageLoadStatus::PROFILE_DELETED) {
return;
}
chrome::DrawTaskbarDecoration(frame_->GetNativeWindow(), &decoration);
#endif
}
bool BrowserNonClientFrameView::ShouldShowProfileIndicatorIcon() const {
#if !defined(OS_CHROMEOS)
// Outside ChromeOS, in Material Refresh, we use a toolbar button for all
// profile/incognito-related purposes. ChromeOS uses it for teleportation (see
// below).
if (MD::IsRefreshUi())
return false;
#endif // !defined(OS_CHROMEOS)
Browser* browser = browser_view()->browser();
Profile* profile = browser->profile();
const bool is_incognito =
profile->GetProfileType() == Profile::INCOGNITO_PROFILE;
// In newer material UIs we only show the avatar icon for the teleported
// browser windows between multi-user sessions (Chrome OS only). Note that you
// can't teleport an incognito window.
if (is_incognito && MD::IsNewerMaterialUi())
return false;
#if defined(OS_CHROMEOS)
if (!browser->is_type_tabbed() && !browser->is_app())
return false;
if (!is_incognito && !MultiUserWindowManager::ShouldShowAvatar(
browser_view()->GetNativeWindow())) {
return false;
}
#endif // defined(OS_CHROMEOS)
return true;
}
SkColor BrowserNonClientFrameView::GetThemeOrDefaultColor(int color_id) const {
return ShouldPaintAsThemed() ? GetThemeProvider()->GetColor(color_id)
: ThemeProperties::GetDefaultColor(
color_id, browser_view_->IsIncognito());
}