blob: c2a28ff16f40d6275964efeba27cc01a624975f9 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// 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/zoom_bubble_view.h"
#include <cmath>
#include <memory>
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/i18n/number_formatting.h"
#include "base/i18n/rtl.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/ui/actions/chrome_action_id.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_actions.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/toolbar_button_provider.h"
#include "chrome/browser/ui/views/page_action/page_action_view.h"
#include "chrome/browser/ui/views/page_action/zoom_view.h"
#include "chrome/browser/ui/views/zoom/zoom_view_controller.h"
#include "chrome/grit/generated_resources.h"
#include "components/zoom/page_zoom.h"
#include "components/zoom/zoom_controller.h"
#include "extensions/browser/extension_zoom_request_client.h"
#include "extensions/common/api/extension_action/action_info.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "extensions/grit/extensions_browser_resources.h"
#include "third_party/blink/public/common/page/page_zoom.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/actions/actions.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/text_utils.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/image_button_factory.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
#if BUILDFLAG(IS_MAC)
#include "chrome/browser/ui/fullscreen_util_mac.h"
#endif
namespace {
// The default time that the bubble should stay on the screen if it will close
// automatically.
constexpr base::TimeDelta kBubbleCloseDelayDefault = base::Milliseconds(1500);
// A longer timeout used for how long the bubble should stay on the screen
// before it will close automatically after + or - buttons have been used.
constexpr base::TimeDelta kBubbleCloseDelayLong = base::Milliseconds(5000);
class ZoomButtonHighlightPathGenerator : public views::HighlightPathGenerator {
public:
ZoomButtonHighlightPathGenerator() = default;
SkPath GetHighlightPath(const views::View* view) override {
constexpr int kCircleRadiusDp = 24 / 2;
const gfx::Point center = view->GetLocalBounds().CenterPoint();
return SkPath::Circle(center.x(), center.y(), kCircleRadiusDp);
}
};
std::unique_ptr<views::ImageButton> CreateZoomButton(
views::Button::PressedCallback callback,
const gfx::VectorIcon& icon,
int tooltip_id) {
auto zoom_button =
views::CreateVectorImageButtonWithNativeTheme(std::move(callback), icon);
zoom_button->SetTooltipText(l10n_util::GetStringUTF16(tooltip_id));
views::HighlightPathGenerator::Install(
zoom_button.get(), std::make_unique<ZoomButtonHighlightPathGenerator>());
return zoom_button;
}
class ZoomValue : public views::Label {
METADATA_HEADER(ZoomValue, views::Label)
public:
explicit ZoomValue(const content::WebContents* web_contents)
: Label(std::u16string(),
views::style::CONTEXT_LABEL,
views::style::STYLE_PRIMARY),
max_width_(GetLabelMaxWidth(web_contents)) {
SetHorizontalAlignment(gfx::ALIGN_LEFT);
}
ZoomValue(const ZoomValue&) = delete;
ZoomValue& operator=(const ZoomValue&) = delete;
~ZoomValue() override = default;
// views::Label:
gfx::Size CalculatePreferredSize(
const views::SizeBounds& available_size) const override {
gfx::Size size =
views::Label::CalculatePreferredSize(views::SizeBounds(max_width_, {}));
// When the initial value of the text width is small(eg: 80%), the
// `ZoomBubbleView` will be smaller. Then after we use a larger value(eg:
// 200%), the text will not be fully displayed. It needs to be set to the
// maximum value to ensure that the size of `ZoomBubbleView` is fixed.
size.set_width(max_width_);
return size;
}
private:
int GetLabelMaxWidth(const content::WebContents* web_contents) const {
const int border_width = GetInsets().width();
int max_w = 0;
auto* zoom_controller = zoom::ZoomController::FromWebContents(web_contents);
DCHECK(zoom_controller);
// Enumerate all zoom factors that can be used in PageZoom::Zoom.
std::vector<double> zoom_factors =
zoom::PageZoom::PresetZoomFactors(zoom_controller->GetZoomPercent());
for (auto zoom : zoom_factors) {
int w = gfx::GetStringWidth(
base::FormatPercent(static_cast<int>(std::round(zoom * 100))),
font_list());
max_w = std::max(w, max_w);
}
return max_w + border_width;
}
const int max_width_;
};
BEGIN_METADATA(ZoomValue)
END_METADATA
} // namespace
void ZoomBubbleView::Refresh() {
UpdateZoomPercent();
StartTimerIfNecessary();
}
ZoomBubbleView::ZoomBubbleView(Browser* browser,
views::View* anchor_view,
content::WebContents* web_contents,
DisplayReason reason)
: LocationBarBubbleDelegateView(anchor_view, web_contents),
browser_(browser),
auto_close_duration_(kBubbleCloseDelayDefault),
auto_close_(reason == AUTOMATIC) {
SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
SetNotifyEnterExitOnChild(true);
UseCompactMargins();
}
ZoomBubbleView::~ZoomBubbleView() = default;
std::u16string ZoomBubbleView::GetAccessibleWindowTitle() const {
ToolbarButtonProvider* provider =
BrowserView::GetBrowserViewForBrowser(browser_)
->toolbar_button_provider();
return provider->GetPageActionView(kActionZoomNormal)->GetAccessibleName();
}
void ZoomBubbleView::OnFocus() {
LocationBarBubbleDelegateView::OnFocus();
StopTimer();
}
void ZoomBubbleView::OnBlur() {
LocationBarBubbleDelegateView::OnBlur();
const views::FocusManager* focus_manager = GetFocusManager();
if (focus_manager && Contains(focus_manager->GetFocusedView())) {
return;
}
StartTimerIfNecessary();
}
void ZoomBubbleView::OnGestureEvent(ui::GestureEvent* event) {
if (!auto_close_ || event->type() != ui::EventType::kGestureTap) {
return;
}
auto_close_ = false;
StopTimer();
event->SetHandled();
}
void ZoomBubbleView::OnKeyEvent(ui::KeyEvent* event) {
if (!auto_close_) {
return;
}
const views::FocusManager* focus_manager = GetFocusManager();
if (focus_manager && Contains(focus_manager->GetFocusedView())) {
StopTimer();
} else {
StartTimerIfNecessary();
}
}
void ZoomBubbleView::OnMouseEntered(const ui::MouseEvent& event) {
StopTimer();
}
void ZoomBubbleView::OnMouseExited(const ui::MouseEvent& event) {
StartTimerIfNecessary();
}
void ZoomBubbleView::Init() {
// Set up the layout of the zoom bubble.
constexpr int kPercentLabelPadding = 64;
const ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
const int spacing =
provider->GetDistanceMetric(views::DISTANCE_UNRELATED_CONTROL_HORIZONTAL);
gfx::Insets inset_border_insets =
provider->GetInsetsMetric(INSETS_TOAST) - margins();
inset_border_insets.set_top_bottom(0, 0);
auto box_layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, inset_border_insets, spacing);
box_layout->set_main_axis_alignment(
views::BoxLayout::MainAxisAlignment::kCenter);
box_layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
SetLayoutManager(std::move(box_layout));
// Calculate child views margins in |this| client view.
const int label_vertical_spacing =
provider->GetDistanceMetric(DISTANCE_TOAST_LABEL_VERTICAL);
const auto label_margin =
gfx::Insets::TLBR(label_vertical_spacing - margins().top(), 0,
label_vertical_spacing - margins().bottom(),
kPercentLabelPadding - spacing);
// Account for the apparent margins that vector buttons have around icons.
const int control_vertical_spacing =
provider->GetDistanceMetric(DISTANCE_TOAST_CONTROL_VERTICAL);
const auto control_vertical_margin =
gfx::Insets::TLBR(control_vertical_spacing - margins().top(), 0,
control_vertical_spacing - margins().bottom(), 0);
const gfx::Insets vector_button_margin(
control_vertical_margin -
provider->GetInsetsMetric(views::INSETS_VECTOR_IMAGE_BUTTON));
const auto button_pressed_callback = [this](base::RepeatingClosure closure) {
return base::BindRepeating(&ZoomBubbleView::ButtonPressed,
base::Unretained(this), std::move(closure));
};
// If this zoom change was initiated by an extension, that extension will be
// attributed by showing its icon in the zoom bubble.
if (extension_info_.icon_image) {
auto image_button = std::make_unique<views::ImageButton>(
button_pressed_callback(base::BindRepeating(
&ZoomBubbleView::ImageButtonPressed, base::Unretained(this))));
image_button->SetTooltipText(
l10n_util::GetStringFUTF16(IDS_TOOLTIP_ZOOM_EXTENSION_ICON,
base::UTF8ToUTF16(extension_info_.name)));
image_button->SetImageModel(views::Button::STATE_NORMAL,
ui::ImageModel::FromImageSkia(
extension_info_.icon_image->image_skia()));
image_button_ = AddChildView(std::move(image_button));
}
// Add zoom label with the new zoom percent.
auto label = std::make_unique<ZoomValue>(web_contents());
label->SetProperty(views::kMarginsKey, gfx::Insets(label_margin));
label->GetViewAccessibility().SetRole(ax::mojom::Role::kAlert);
label_ = label.get();
AddChildView(std::move(label));
const auto zoom_callback = [button_pressed_callback,
web_contents =
web_contents()](content::PageZoom zoom) {
return button_pressed_callback(base::BindRepeating(
&zoom::PageZoom::Zoom, base::Unretained(web_contents), zoom));
};
// Add Zoom Out ("-") button.
zoom_out_button_ =
AddChildView(CreateZoomButton(zoom_callback(content::PAGE_ZOOM_OUT),
kRemoveIcon, IDS_ACCNAME_ZOOM_MINUS2));
zoom_out_button_->SetProperty(views::kMarginsKey,
gfx::Insets(vector_button_margin));
// Add Zoom In ("+") button.
zoom_in_button_ = AddChildView(CreateZoomButton(
zoom_callback(content::PAGE_ZOOM_IN), kAddIcon, IDS_ACCNAME_ZOOM_PLUS2));
zoom_in_button_->SetProperty(views::kMarginsKey,
gfx::Insets(vector_button_margin));
// Add "Reset" button.
auto reset_button = std::make_unique<views::MdTextButton>(
zoom_callback(content::PAGE_ZOOM_RESET),
l10n_util::GetStringUTF16(IDS_ZOOM_SET_DEFAULT));
reset_button->SetTooltipText(
l10n_util::GetStringUTF16(IDS_ACCNAME_ZOOM_SET_DEFAULT));
reset_button_ = AddChildView(std::move(reset_button));
UpdateZoomPercent();
StartTimerIfNecessary();
}
void ZoomBubbleView::OnExtensionIconImageChanged(
extensions::IconImage* /* image */) {
image_button_->SetImageModel(
views::Button::STATE_NORMAL,
ui::ImageModel::FromImageSkia(extension_info_.icon_image->image_skia()));
image_button_->SchedulePaint();
}
void ZoomBubbleView::SetExtensionInfo(const extensions::Extension* extension) {
DCHECK(extension);
extension_info_.id = extension->id();
extension_info_.name = extension->name();
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
const gfx::ImageSkia& default_extension_icon_image =
*rb.GetImageSkiaNamed(IDR_EXTENSIONS_FAVICON);
int icon_size = gfx::kFaviconSize;
// We give first preference to an icon from the extension's icon set that
// matches the size of the default. But not all extensions will declare an
// icon set, or may not have an icon of the default size (we don't want the
// bubble to display, for example, a very large icon). In that case, if there
// is an action icon (size-16) this is an acceptable alternative.
const ExtensionIconSet* icons = &extensions::IconsInfo::GetIcons(extension);
bool has_default_sized_icon =
!icons->Get(gfx::kFaviconSize, ExtensionIconSet::Match::kExactly).empty();
if (!has_default_sized_icon) {
const extensions::ActionInfo* action =
extensions::ActionInfo::GetExtensionActionInfo(extension);
if (!action || action->default_icon.empty()) {
return; // Out of options.
}
icons = &action->default_icon;
icon_size = icons->map().begin()->first;
}
extension_info_.icon_image = std::make_unique<extensions::IconImage>(
web_contents()->GetBrowserContext(), extension, *icons, icon_size,
default_extension_icon_image, this);
}
void ZoomBubbleView::UpdateZoomPercent() {
label_->SetText(base::FormatPercent(
zoom::ZoomController::FromWebContents(web_contents())->GetZoomPercent()));
label_->GetViewAccessibility().SetName(GetAccessibleWindowTitle());
label_->NotifyAccessibilityEventDeprecated(ax::mojom::Event::kAlert, true);
// Disable buttons at min, max and default
auto* zoom_controller = zoom::ZoomController::FromWebContents(web_contents());
double current_zoom_level = zoom_controller->GetZoomLevel();
double default_zoom_level = zoom_controller->GetDefaultZoomLevel();
std::vector<double> zoom_levels =
zoom::PageZoom::PresetZoomLevels(default_zoom_level);
DCHECK(zoom_out_button_);
zoom_out_button_->SetEnabled(
!blink::ZoomValuesEqual(zoom_levels.front(), current_zoom_level));
DCHECK(zoom_in_button_);
zoom_in_button_->SetEnabled(
!blink::ZoomValuesEqual(zoom_levels.back(), current_zoom_level));
}
void ZoomBubbleView::StartTimerIfNecessary() {
if (!auto_close_) {
return;
}
auto_close_timer_.Start(
FROM_HERE, auto_close_duration_,
base::BindOnce(&ZoomBubbleView::Close, base::Unretained(this)));
}
void ZoomBubbleView::Close() {
if (GetWidget()) {
GetWidget()->Close();
}
}
void ZoomBubbleView::StopTimer() {
auto_close_timer_.Stop();
}
void ZoomBubbleView::ButtonPressed(base::RepeatingClosure closure) {
// No button presses in this dialog should cause the dialog to close,
// including when the zoom level is set to 100% as a result of a button press.
base::AutoReset<bool> auto_ignore_close_bubble(&ignore_close_bubble_, true);
// Extend the timer to give a user more time after any button is pressed.
auto_close_duration_ = kBubbleCloseDelayLong;
StartTimerIfNecessary();
closure.Run();
}
void ZoomBubbleView::ImageButtonPressed() {
DCHECK(extension_info_.icon_image) << "Invalid button press.";
chrome::AddSelectedTabWithURL(
browser_,
GURL(base::StringPrintf("chrome://extensions?id=%s",
extension_info_.id.c_str())),
ui::PAGE_TRANSITION_FROM_API);
}
std::u16string_view ZoomBubbleView::GetLabelForTesting() const {
return label_->GetText();
}
base::OneShotTimer* ZoomBubbleView::GetAutoCloseTimerForTesting() {
return &auto_close_timer_;
}
views::Button* ZoomBubbleView::GetResetButtonForTesting() {
return reset_button_;
}
views::Button* ZoomBubbleView::GetZoomInButtonForTesting() {
return zoom_in_button_;
}
void ZoomBubbleView::OnKeyEventForTesting(ui::KeyEvent* event) {
OnKeyEvent(event);
}
ZoomBubbleView::ZoomBubbleExtensionInfo::ZoomBubbleExtensionInfo() = default;
ZoomBubbleView::ZoomBubbleExtensionInfo::~ZoomBubbleExtensionInfo() = default;
BEGIN_METADATA(ZoomBubbleView)
END_METADATA