| // 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; |
| } |