blob: b84e89dbece7afd34054db3a6860e4f9f156d292 [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 "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_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/frame/hosted_app_button_container.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/theme_resources.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/hit_test.h"
#include "ui/base/theme_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.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"
#include "ui/views/window/hit_test_utils.h"
#if defined(OS_WIN)
#include "chrome/browser/ui/views/frame/taskbar_decorator_win.h"
#endif
// static
constexpr int BrowserNonClientFrameView::kMinimumDragHeight;
BrowserNonClientFrameView::BrowserNonClientFrameView(BrowserFrame* frame,
BrowserView* browser_view)
: frame_(frame),
browser_view_(browser_view),
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);
}
}
void BrowserNonClientFrameView::OnBrowserViewInitViewsComplete() {
MaybeObserveTabstrip();
OnSingleTabModeChanged();
UpdateMinimumSize();
}
void BrowserNonClientFrameView::OnFullscreenStateChanged() {}
bool BrowserNonClientFrameView::CaptionButtonsOnLeadingEdge() const {
return false;
}
void BrowserNonClientFrameView::UpdateFullscreenTopUI(
bool needs_check_tab_fullscreen) {}
bool BrowserNonClientFrameView::ShouldHideTopUIForFullscreen() const {
return frame_->IsFullscreen();
}
bool BrowserNonClientFrameView::CanUserExitFullscreen() const {
return true;
}
bool BrowserNonClientFrameView::IsFrameCondensed() const {
return frame_ && (frame_->IsMaximized() || frame_->IsFullscreen());
}
bool BrowserNonClientFrameView::HasVisibleBackgroundTabShapes(
ActiveState active_state) const {
DCHECK(browser_view_->IsTabStripVisible());
TabStrip* const tab_strip = browser_view_->tabstrip();
bool has_custom_image;
const int fill_id =
tab_strip->GetBackgroundResourceId(&has_custom_image, active_state);
const bool active = ShouldPaintAsActive(active_state);
if (has_custom_image) {
// If the theme has a custom tab background image, assume tab shapes are
// visible. This is pessimistic; the theme may use the same image as the
// frame, just shifted to align, or a solid-color image the same color as
// the frame; but to detect this we'd need to do some kind of aligned
// rendering comparison, which seems not worth it.
const ui::ThemeProvider* tp = GetThemeProvider();
if (tp->HasCustomImage(fill_id))
return true;
// Inactive tab background images are copied from the active ones, so in the
// inactive case, check the active image as well.
if (!active) {
const int active_id = browser_view_->IsIncognito()
? IDR_THEME_TAB_BACKGROUND_INCOGNITO
: IDR_THEME_TAB_BACKGROUND;
if (tp->HasCustomImage(active_id))
return true;
}
// The tab image is a tinted version of the frame image. Tabs are visible
// iff the tint has some visible effect.
return color_utils::IsHSLShiftMeaningful(
tp->GetTint(ThemeProperties::TINT_BACKGROUND_TAB));
}
// Background tab shapes are visible iff the tab color differs from the frame
// color.
return tab_strip->GetTabBackgroundColor(TAB_INACTIVE, active_state) !=
GetFrameColor(active_state);
}
bool BrowserNonClientFrameView::EverHasVisibleBackgroundTabShapes() const {
return HasVisibleBackgroundTabShapes(kActive) ||
HasVisibleBackgroundTabShapes(kInactive);
}
bool BrowserNonClientFrameView::CanDrawStrokes() const {
// Hosted apps should not draw strokes, as they don't have a tab strip.
return !browser_view_->browser()->hosted_app_controller();
}
SkColor BrowserNonClientFrameView::GetFrameColor(
ActiveState active_state) const {
ThemeProperties::OverwritableByUserThemeProperty color_id;
if (ShouldPaintAsSingleTabMode()) {
color_id = ThemeProperties::COLOR_TOOLBAR;
} else {
color_id = ShouldPaintAsActive(active_state)
? ThemeProperties::COLOR_FRAME
: ThemeProperties::COLOR_FRAME_INACTIVE;
}
// For hosted app windows, if "painting as themed" (which is only true when on
// Linux and using the system theme), prefer the system theme color over the
// hosted app theme color. The title bar will be painted in the system theme
// color (regardless of what we do here), so by returning the system title bar
// background color here, we ensure that:
// a) The side and bottom borders are painted in the same color as the title
// bar background, and
// b) The title text is painted in a color that contrasts with the title bar
// background.
if (ShouldPaintAsThemed())
return GetThemeProviderForProfile()->GetColor(color_id);
extensions::HostedAppBrowserController* hosted_app_controller =
browser_view_->browser()->hosted_app_controller();
if (hosted_app_controller && hosted_app_controller->GetThemeColor())
return *hosted_app_controller->GetThemeColor();
return ThemeProperties::GetDefaultColor(color_id,
browser_view_->IsIncognito());
}
SkColor BrowserNonClientFrameView::GetToolbarTopSeparatorColor() const {
const int color_id =
ShouldPaintAsActive()
? ThemeProperties::COLOR_TOOLBAR_TOP_SEPARATOR
: ThemeProperties::COLOR_TOOLBAR_TOP_SEPARATOR_INACTIVE;
// The vertical tab separator might show through the stroke if the stroke
// color is translucent. To prevent this, always use an opaque stroke color.
return color_utils::GetResultingPaintColor(GetThemeOrDefaultColor(color_id),
GetFrameColor());
}
int BrowserNonClientFrameView::GetTabBackgroundResourceId(
ActiveState active_state,
bool* has_custom_image) const {
const ui::ThemeProvider* tp = GetThemeProvider();
const bool incognito = browser_view_->IsIncognito();
const bool active = ShouldPaintAsActive(active_state);
const int active_id =
incognito ? IDR_THEME_TAB_BACKGROUND_INCOGNITO : IDR_THEME_TAB_BACKGROUND;
const int inactive_id =
incognito ? IDR_THEME_TAB_BACKGROUND_INCOGNITO_INACTIVE
: IDR_THEME_TAB_BACKGROUND_INACTIVE;
const int id = active ? active_id : inactive_id;
// tp->HasCustomImage() will only return true if the supplied ID has been
// customized directly. We also account for the following fallback cases:
// * The inactive images are copied directly from the active ones if present
// * Tab backgrounds are generated from frame backgrounds if present, and
// * The incognito frame image is generated from the normal frame image, so
// in incognito mode we look at both.
*has_custom_image =
tp->HasCustomImage(id) || (!active && tp->HasCustomImage(active_id)) ||
tp->HasCustomImage(IDR_THEME_FRAME) ||
(incognito && tp->HasCustomImage(IDR_THEME_FRAME_INCOGNITO));
return id;
}
void BrowserNonClientFrameView::UpdateClientArea() {}
void BrowserNonClientFrameView::UpdateMinimumSize() {}
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());
}
int BrowserNonClientFrameView::NonClientHitTest(const gfx::Point& point) {
if (hosted_app_button_container_) {
int hosted_app_component =
views::GetHitTestComponent(hosted_app_button_container_, point);
if (hosted_app_component != HTNOWHERE)
return hosted_app_component;
}
return HTNOWHERE;
}
void BrowserNonClientFrameView::ResetWindowControls() {
if (hosted_app_button_container_)
hosted_app_button_container_->UpdateStatusIconsVisibility();
}
void BrowserNonClientFrameView::OnSingleTabModeChanged() {
SchedulePaint();
}
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;
}
// We 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::IsSingleTabModeAvailable() const {
// Single-tab mode is only available in 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 contrast of other UI elements, the theme color
// should be used instead.
return base::FeatureList::IsEnabled(features::kSingleTabMode) &&
ShouldPaintAsActive() && GetFrameImage().isNull();
}
bool BrowserNonClientFrameView::ShouldPaintAsSingleTabMode() const {
return browser_view_->IsTabStripVisible() &&
browser_view_->tabstrip()->SingleTabMode();
}
bool BrowserNonClientFrameView::ShouldPaintAsThemed() const {
return browser_view_->IsBrowserTypeNormal();
}
SkColor BrowserNonClientFrameView::GetCaptionColor(
ActiveState active_state) const {
return color_utils::GetColorWithMaxContrast(GetFrameColor(active_state));
}
bool BrowserNonClientFrameView::ShouldPaintAsActive(
ActiveState active_state) const {
return (active_state == kUseCurrent) ? ShouldPaintAsActive()
: (active_state == kActive);
}
gfx::ImageSkia BrowserNonClientFrameView::GetFrameImage(
ActiveState active_state) const {
const ui::ThemeProvider* tp = GetThemeProviderForProfile();
const int frame_image_id = ShouldPaintAsActive(active_state)
? 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(
ActiveState active_state) const {
if (browser_view_->IsIncognito() || !browser_view_->IsBrowserTypeNormal())
return gfx::ImageSkia();
const ui::ThemeProvider* tp = GetThemeProviderForProfile();
const int frame_overlay_image_id = ShouldPaintAsActive(active_state)
? IDR_THEME_FRAME_OVERLAY
: IDR_THEME_FRAME_OVERLAY_INACTIVE;
return tp->HasCustomImage(frame_overlay_image_id)
? *tp->GetImageSkiaNamed(frame_overlay_image_id)
: gfx::ImageSkia();
}
void BrowserNonClientFrameView::ChildPreferredSizeChanged(views::View* child) {
if (browser_view()->initialized() && child == hosted_app_button_container_)
Layout();
}
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 state to reflect
// that the window is inactive, we force NonClientFrameView to see the
// "correct" state as an override.
set_active_state_override(&active);
// 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();
set_active_state_override(nullptr);
if (hosted_app_button_container_)
hosted_app_button_container_->SetPaintAsActive(active);
// 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();
// The tabstrip may not be in a Widget (e.g. when switching into immersive
// reveal).
if (tabstrip->GetWidget()) {
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.
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();
}
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();
}
SkColor BrowserNonClientFrameView::GetThemeOrDefaultColor(int color_id) const {
// During shutdown, there may no longer be a widget, and thus no theme
// provider.
const auto* theme_provider = GetThemeProvider();
return ShouldPaintAsThemed() && theme_provider
? theme_provider->GetColor(color_id)
: ThemeProperties::GetDefaultColor(color_id,
browser_view_->IsIncognito());
}