blob: 6d363bdfe267ea794e127713948253acf5d17f80 [file] [log] [blame]
// 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/location_bar/custom_tab_bar_view.h"
#include <memory>
#include "base/metrics/user_metrics.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/omnibox/omnibox_theme.h"
#include "chrome/browser/ui/page_info/page_info_dialog.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/grit/generated_resources.h"
#include "components/security_interstitials/content/security_interstitial_tab_helper.h"
#include "components/url_formatter/url_formatter.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_constants.h"
#include "ui/native_theme/native_theme.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/image_button_factory.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/style/typography.h"
#include "ui/views/style/typography_provider.h"
#include "ui/views/view_class_properties.h"
#include "url/gurl.h"
#if defined(OS_CHROMEOS)
#include "ash/public/cpp/ash_constants.h"
#else
#include "chrome/browser/themes/theme_properties.h"
#endif
namespace {
std::unique_ptr<views::ImageButton> CreateCloseButton(
views::ButtonListener* listener) {
auto close_button = CreateVectorImageButton(listener);
close_button->SetTooltipText(l10n_util::GetStringUTF16(IDS_APP_CLOSE));
close_button->SetBorder(views::CreateEmptyBorder(
gfx::Insets(GetLayoutConstant(LOCATION_BAR_CHILD_INTERIOR_PADDING))));
close_button->SizeToPreferredSize();
views::InstallCircleHighlightPathGenerator(close_button.get());
return close_button;
}
bool ShouldDisplayUrl(content::WebContents* contents) {
auto* tab_helper =
security_interstitials::SecurityInterstitialTabHelper::FromWebContents(
contents);
if (tab_helper && tab_helper->IsDisplayingInterstitial())
return tab_helper->ShouldDisplayURL();
return true;
}
bool IsInitialUrlInAppScope(web_app::AppBrowserController* app_controller) {
return app_controller
? app_controller->IsUrlInAppScope(app_controller->initial_url())
: false;
}
bool IsUrlInAppScope(web_app::AppBrowserController* app_controller, GURL url) {
return app_controller ? app_controller->IsUrlInAppScope(url) : false;
}
} // namespace
// Container view for laying out and rendering the title/origin of the current
// page.
class CustomTabBarTitleOriginView : public views::View {
public:
CustomTabBarTitleOriginView(SkColor background_color,
bool should_show_title) {
auto location_label = std::make_unique<views::Label>(
base::string16(), views::style::CONTEXT_LABEL,
views::style::STYLE_SECONDARY,
gfx::DirectionalityMode::DIRECTIONALITY_AS_URL);
location_label->SetElideBehavior(gfx::ElideBehavior::ELIDE_HEAD);
location_label->SetHorizontalAlignment(
gfx::HorizontalAlignment::ALIGN_LEFT);
location_label->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
views::MaximumFlexSizeRule::kPreferred));
location_label_ = AddChildView(std::move(location_label));
if (should_show_title) {
auto title_label = std::make_unique<views::Label>(
base::string16(), views::style::CONTEXT_LABEL);
title_label->SetElideBehavior(gfx::ElideBehavior::ELIDE_TAIL);
title_label->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
title_label->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
views::MaximumFlexSizeRule::kPreferred));
title_label_ = AddChildView(std::move(title_label));
}
auto* layout = SetLayoutManager(std::make_unique<views::FlexLayout>());
layout->SetOrientation(views::LayoutOrientation::kVertical)
.SetMainAxisAlignment(views::LayoutAlignment::kCenter)
.SetCrossAxisAlignment(views::LayoutAlignment::kStart);
}
void Update(const base::string16 title, const base::string16 location) {
if (title_label_)
title_label_->SetText(title);
location_label_->SetText(location);
location_label_->SetVisible(!location.empty());
}
void SetColors(SkColor background_color) {
if (title_label_)
title_label_->SetBackgroundColor(background_color);
location_label_->SetBackgroundColor(background_color);
}
int GetMinimumWidth() const {
// As labels are not multi-line, the layout will calculate a minimum size
// that would fit the entire text (potentially a long url). Instead, set a
// minimum number of characters we want to display and elide the text if it
// overflows.
// This is in a helper function because we also have to ensure that the
// preferred size is at least as wide as the minimum size, and the
// minimum height of the control should be the preferred height.
constexpr int kMinCharacters = 20;
return title_label_
? title_label_->font_list().GetExpectedTextWidth(kMinCharacters)
: location_label_->font_list().GetExpectedTextWidth(
kMinCharacters);
}
SkColor GetLocationColor() const {
return views::style::GetColor(*this, CONTEXT_BODY_TEXT_SMALL,
views::style::TextStyle::STYLE_PRIMARY);
}
// views::View:
gfx::Size GetMinimumSize() const override {
return gfx::Size(GetMinimumWidth(), GetPreferredSize().height());
}
gfx::Size CalculatePreferredSize() const override {
// If we don't also override CalculatePreferredSize, we violate some
// assumptions in the FlexLayout (that our PreferredSize is always larger
// than our MinimumSize).
gfx::Size preferred_size = views::View::CalculatePreferredSize();
preferred_size.SetToMax(gfx::Size(GetMinimumWidth(), 0));
return preferred_size;
}
bool IsShowingOriginForTesting() const {
return location_label_ != nullptr && location_label_->GetVisible();
}
private:
// Can be nullptr.
views::Label* title_label_ = nullptr;
views::Label* location_label_ = nullptr;
};
// static
const char CustomTabBarView::kViewClassName[] = "CustomTabBarView";
CustomTabBarView::CustomTabBarView(BrowserView* browser_view,
LocationBarView::Delegate* delegate)
: TabStripModelObserver(),
delegate_(delegate),
browser_(browser_view->browser()) {
set_context_menu_controller(this);
const gfx::FontList& font_list = views::style::GetFont(
CONTEXT_OMNIBOX_PRIMARY, views::style::STYLE_PRIMARY);
close_button_ = AddChildView(CreateCloseButton(this));
location_icon_view_ =
AddChildView(std::make_unique<LocationIconView>(font_list, this, this));
auto title_origin_view = std::make_unique<CustomTabBarTitleOriginView>(
background_color_, ShouldShowTitle());
title_origin_view->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
views::MaximumFlexSizeRule::kPreferred));
title_origin_view_ = AddChildView(std::move(title_origin_view));
layout_manager_ = SetLayoutManager(std::make_unique<views::FlexLayout>());
layout_manager_->SetOrientation(views::LayoutOrientation::kHorizontal)
.SetMainAxisAlignment(views::LayoutAlignment::kStart)
.SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
.SetInteriorMargin(GetLayoutInsets(LayoutInset::TOOLBAR_INTERIOR_MARGIN));
browser_->tab_strip_model()->AddObserver(this);
}
CustomTabBarView::~CustomTabBarView() {}
gfx::Rect CustomTabBarView::GetAnchorBoundsInScreen() const {
return gfx::UnionRects(location_icon_view_->GetAnchorBoundsInScreen(),
title_origin_view_->GetAnchorBoundsInScreen());
}
const char* CustomTabBarView::GetClassName() const {
return kViewClassName;
}
void CustomTabBarView::SetVisible(bool visible) {
if (!GetVisible() && visible) {
UpdateContents();
}
View::SetVisible(visible);
}
gfx::Size CustomTabBarView::CalculatePreferredSize() const {
// ToolbarView::GetMinimumSize() uses the preferred size of its children, so
// tell it the minimum size this control will fit into (its layout will
// automatically have this control fill available space).
return gfx::Size(layout_manager_->interior_margin().width() +
title_origin_view_->GetMinimumSize().width() +
close_button_->GetPreferredSize().width() +
location_icon_view_->GetPreferredSize().width(),
GetLayoutManager()->GetPreferredSize(this).height());
}
void CustomTabBarView::OnPaintBackground(gfx::Canvas* canvas) {
views::View::OnPaintBackground(canvas);
SkColor separator_color =
color_utils::IsDark(background_color_) ? SK_ColorWHITE : SK_ColorBLACK;
constexpr float kSeparatorOpacity = 0.15f;
gfx::Rect bounds = GetLocalBounds();
const gfx::Size separator_size = gfx::Size(bounds.width(), 1);
// Inset the bounds by 1 on the bottom, so we draw the bottom border inside
// the custom tab bar.
bounds.Inset(0, 0, 0, 1);
// Custom tab/content separator (bottom border).
canvas->FillRect(gfx::Rect(bounds.bottom_left(), separator_size),
color_utils::AlphaBlend(separator_color, background_color_,
kSeparatorOpacity));
// Don't render the separator if there is already sufficient contrast between
// the custom tab bar and the title bar.
constexpr float kMaxContrastForSeparator = 1.1f;
if (color_utils::GetContrastRatio(background_color_, title_bar_color_) >
kMaxContrastForSeparator) {
return;
}
// Frame/Custom tab separator (top border).
canvas->FillRect(gfx::Rect(bounds.origin(), separator_size),
color_utils::AlphaBlend(separator_color, title_bar_color_,
kSeparatorOpacity));
}
void CustomTabBarView::ChildPreferredSizeChanged(views::View* child) {
Layout();
SchedulePaint();
}
void CustomTabBarView::OnThemeChanged() {
views::AccessiblePaneView::OnThemeChanged();
base::Optional<SkColor> optional_theme_color = GetThemeColor();
title_bar_color_ = optional_theme_color.value_or(GetDefaultFrameColor());
background_color_ = GetBackgroundColor();
SetBackground(views::CreateSolidBackground(background_color_));
const SkColor foreground_color =
color_utils::GetColorWithMaxContrast(background_color_);
SetImageFromVectorIconWithColor(
close_button_, vector_icons::kCloseRoundedIcon,
GetLayoutConstant(LOCATION_BAR_ICON_SIZE), foreground_color);
title_origin_view_->SetColors(background_color_);
}
void CustomTabBarView::TabChangedAt(content::WebContents* contents,
int index,
TabChangeType change_type) {
if (delegate_->GetWebContents() == contents)
UpdateContents();
}
void CustomTabBarView::UpdateContents() {
// If the toolbar should not be shown don't update the UI, as the toolbar may
// be animating out and it looks messy.
web_app::AppBrowserController* const app_controller =
browser_->app_controller();
if (app_controller && !app_controller->ShouldShowCustomTabBar())
return;
content::WebContents* contents = delegate_->GetWebContents();
if (!contents)
return;
content::NavigationEntry* entry = contents->GetController().GetVisibleEntry();
base::string16 title, location;
if (entry) {
title = Browser::FormatTitleForDisplay(entry->GetTitleForDisplay());
if (ShouldDisplayUrl(contents))
location = url_formatter::FormatUrl(entry->GetVirtualURL().GetOrigin(),
url_formatter::kFormatUrlOmitDefaults,
net::UnescapeRule::NORMAL, nullptr,
nullptr, nullptr);
}
title_origin_view_->Update(title, location);
location_icon_view_->Update(/*suppress animations = */ false);
// Hide location icon if we're already hiding the origin.
location_icon_view_->SetVisible(!location.empty());
last_title_ = title;
last_location_ = location;
// Only show the 'X' button if:
// a) The current url is not in scope (no point showing a back to app button
// while in scope).
// And b), if the window started in scope (this is
// important for popup windows, which may be opened outside the app).
bool set_visible =
IsInitialUrlInAppScope(app_controller) &&
!IsUrlInAppScope(app_controller, contents->GetLastCommittedURL());
close_button_->SetVisible(set_visible);
Layout();
}
SkColor CustomTabBarView::GetIconLabelBubbleSurroundingForegroundColor() const {
return title_origin_view_->GetLocationColor();
}
SkColor CustomTabBarView::GetIconLabelBubbleBackgroundColor() const {
return GetBackgroundColor();
}
content::WebContents* CustomTabBarView::GetWebContents() {
return delegate_->GetWebContents();
}
bool CustomTabBarView::IsEditingOrEmpty() const {
return false;
}
void CustomTabBarView::OnLocationIconPressed(const ui::MouseEvent& event) {}
void CustomTabBarView::OnLocationIconDragged(const ui::MouseEvent& event) {}
SkColor CustomTabBarView::GetSecurityChipColor(
security_state::SecurityLevel security_level) const {
return GetOmniboxSecurityChipColor(GetThemeProvider(), security_level);
}
bool CustomTabBarView::ShowPageInfoDialog() {
return ::ShowPageInfoDialog(
GetWebContents(),
base::BindOnce(&CustomTabBarView::AppInfoClosedCallback,
weak_factory_.GetWeakPtr()),
bubble_anchor_util::Anchor::kCustomTabBar);
}
const LocationBarModel* CustomTabBarView::GetLocationBarModel() const {
return delegate_->GetLocationBarModel();
}
gfx::ImageSkia CustomTabBarView::GetLocationIcon(
LocationIconView::Delegate::IconFetchedCallback on_icon_fetched) const {
return gfx::CreateVectorIcon(
delegate_->GetLocationBarModel()->GetVectorIcon(),
GetLayoutConstant(LOCATION_BAR_ICON_SIZE),
GetSecurityChipColor(GetLocationBarModel()->GetSecurityLevel()));
}
void CustomTabBarView::ButtonPressed(views::Button* sender,
const ui::Event& event) {
GoBackToApp();
}
void CustomTabBarView::GoBackToAppForTesting() {
GoBackToApp();
}
bool CustomTabBarView::IsShowingOriginForTesting() const {
return title_origin_view_ != nullptr &&
title_origin_view_->IsShowingOriginForTesting();
}
SkColor CustomTabBarView::GetBackgroundColor() const {
return GetNativeTheme()->ShouldUseDarkColors() ? GetDefaultFrameColor()
: SK_ColorWHITE;
}
SkColor CustomTabBarView::GetDefaultFrameColor() const {
#if defined(OS_CHROMEOS)
// Ash system frames differ from ChromeOS browser frames.
return ash::kDefaultFrameColor;
#else
return ThemeProperties::GetDefaultColor(
ThemeProperties::COLOR_FRAME_ACTIVE, false,
GetNativeTheme()->ShouldUseDarkColors());
#endif
}
void CustomTabBarView::GoBackToApp() {
content::WebContents* web_contents = GetWebContents();
content::NavigationController& controller = web_contents->GetController();
content::NavigationEntry* entry = nullptr;
int offset = 0;
web_app::AppBrowserController* application_controller = app_controller();
// Go back until we find an in scope url or run out of urls.
while ((entry = controller.GetEntryAtOffset(offset)) &&
!IsUrlInAppScope(application_controller, entry->GetURL())) {
offset--;
}
// If there are no in scope urls, push the app's launch url and clear
// the history.
if (!entry) {
if (application_controller) {
GURL initial_url = application_controller->GetAppLaunchURL();
content::NavigationController::LoadURLParams load(initial_url);
load.should_clear_history_list = true;
controller.LoadURLWithParams(load);
}
return;
}
// Otherwise, go back to the first in scope url.
controller.GoToOffset(offset);
}
void CustomTabBarView::AppInfoClosedCallback(
views::Widget::ClosedReason closed_reason,
bool reload_prompt) {
// If we're closing the bubble because the user pressed ESC or because the
// user clicked Close (rather than the user clicking directly on something
// else), we should refocus the location bar. This lets the user tab into the
// "You should reload this page" infobar rather than dumping them back out
// into a stale webpage.
if (!reload_prompt)
return;
if (closed_reason != views::Widget::ClosedReason::kEscKeyPressed &&
closed_reason != views::Widget::ClosedReason::kCloseButtonClicked) {
return;
}
GetFocusManager()->SetFocusedView(location_icon_view_);
}
void CustomTabBarView::ExecuteCommand(int command_id, int event_flags) {
if (command_id == IDC_COPY_URL) {
base::RecordAction(base::UserMetricsAction("CopyCustomTabBarUrl"));
chrome::ExecuteCommand(browser_, command_id);
}
}
void CustomTabBarView::ShowContextMenuForViewImpl(
views::View* source,
const gfx::Point& point,
ui::MenuSourceType source_type) {
if (!context_menu_model_) {
context_menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
context_menu_model_->AddItemWithStringId(IDC_COPY_URL, IDS_COPY_URL);
}
context_menu_runner_ = std::make_unique<views::MenuRunner>(
context_menu_model_.get(),
views::MenuRunner::HAS_MNEMONICS | views::MenuRunner::CONTEXT_MENU);
context_menu_runner_->RunMenuAt(
views::View::GetWidget(), nullptr, gfx::Rect(point, gfx::Size()),
views::MenuAnchorPosition::kTopLeft, source_type);
}
base::Optional<SkColor> CustomTabBarView::GetThemeColor() const {
web_app::AppBrowserController* application_controller = app_controller();
return application_controller ? application_controller->GetThemeColor()
: base::nullopt;
}
bool CustomTabBarView::ShouldShowTitle() const {
return app_controller() != nullptr;
}