blob: 40eed8ae681f30fe24d780e3b35b2d3d1df6e7c9 [file] [log] [blame]
// Copyright 2013 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/toolbar/browser_app_menu_button.h"
#include <set>
#include "base/bind.h"
#include "base/location.h"
#include "base/single_thread_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "cc/paint/paint_flags.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_otr_state.h"
#include "chrome/browser/ui/in_product_help/in_product_help.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/toolbar/app_menu_model.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/extensions/browser_action_drag_data.h"
#include "chrome/browser/ui/views/toolbar/app_menu.h"
#include "chrome/browser/ui/views/toolbar/toolbar_button.h"
#include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/ui_base_features.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/gfx/animation/throb_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/animation/animation_delegate_views.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_highlight.h"
#include "ui/views/animation/ink_drop_mask.h"
#include "ui/views/animation/ink_drop_state.h"
#include "ui/views/controls/button/label_button_border.h"
#include "ui/views/metrics.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#if defined(OS_CHROMEOS)
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client.h"
#endif // defined(OS_CHROMEOS)
namespace {
// Button background and icon colors for in-product help promos. The first is
// the preferred color, but the selected color depends on the
// background. TODO(collinbaker): consider moving these into theme system.
constexpr SkColor kFeaturePromoHighlightDarkColor = gfx::kGoogleBlue600;
constexpr SkColor kFeaturePromoHighlightDarkExtremeColor = gfx::kGoogleBlue900;
constexpr SkColor kFeaturePromoHighlightLightColor = gfx::kGoogleGrey100;
constexpr SkColor kFeaturePromoHighlightLightExtremeColor = SK_ColorWHITE;
// Cycle duration of ink drop pulsing animation used for in-product help.
constexpr base::TimeDelta kFeaturePromoPulseDuration =
base::TimeDelta::FromMilliseconds(800);
// Max inset for pulsing animation.
constexpr float kFeaturePromoPulseInsetDip = 3.0f;
// An InkDropMask used to animate the size of the BrowserAppMenuButton's ink
// drop. This is used when showing in-product help.
class PulsingInkDropMask : public views::AnimationDelegateViews,
public views::InkDropMask {
public:
PulsingInkDropMask(views::View* layer_container,
const gfx::Size& layer_size,
const gfx::Insets& margins,
float normal_corner_radius,
float max_inset)
: AnimationDelegateViews(layer_container),
views::InkDropMask(layer_size),
layer_container_(layer_container),
margins_(margins),
normal_corner_radius_(normal_corner_radius),
max_inset_(max_inset),
throb_animation_(this) {
throb_animation_.SetThrobDuration(kFeaturePromoPulseDuration);
throb_animation_.StartThrobbing(-1);
}
private:
// views::InkDropMask:
void OnPaintLayer(const ui::PaintContext& context) override {
cc::PaintFlags flags;
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setAntiAlias(true);
ui::PaintRecorder recorder(context, layer()->size());
gfx::RectF bounds(layer()->bounds());
bounds.Inset(margins_);
const float current_inset =
throb_animation_.CurrentValueBetween(0.0f, max_inset_);
bounds.Inset(gfx::InsetsF(current_inset));
const float corner_radius = normal_corner_radius_ - current_inset;
recorder.canvas()->DrawRoundRect(bounds, corner_radius, flags);
}
// views::AnimationDelegateViews:
void AnimationProgressed(const gfx::Animation* animation) override {
DCHECK_EQ(animation, &throb_animation_);
layer()->SchedulePaint(gfx::Rect(layer()->size()));
// This is a workaround for crbug.com/935808: for scale factors >1,
// invalidating the mask layer doesn't cause the whole layer to be repainted
// on screen. TODO(crbug.com/935808): remove this workaround once the bug is
// fixed.
layer_container_->SchedulePaint();
}
// The View that contains the InkDrop layer we're masking. This must outlive
// our instance.
views::View* const layer_container_;
// Margins between the layer bounds and the visible ink drop. We use this
// because sometimes the View we're masking is larger than the ink drop we
// want to show.
const gfx::Insets margins_;
// Normal corner radius of the ink drop without animation. This is also the
// corner radius at the largest instant of the animation.
const float normal_corner_radius_;
// Max inset, used at the smallest instant of the animation.
const float max_inset_;
gfx::ThrobAnimation throb_animation_;
};
} // namespace
// static
bool BrowserAppMenuButton::g_open_app_immediately_for_testing = false;
BrowserAppMenuButton::BrowserAppMenuButton(ToolbarView* toolbar_view)
: AppMenuButton(toolbar_view), toolbar_view_(toolbar_view) {
SetInkDropMode(InkDropMode::ON);
SetHorizontalAlignment(gfx::ALIGN_RIGHT);
set_ink_drop_visible_opacity(kToolbarInkDropVisibleOpacity);
md_observer_.Add(ui::MaterialDesignController::GetInstance());
UpdateBorder();
}
BrowserAppMenuButton::~BrowserAppMenuButton() {}
void BrowserAppMenuButton::SetTypeAndSeverity(
AppMenuIconController::TypeAndSeverity type_and_severity) {
type_and_severity_ = type_and_severity;
int message_id;
base::string16 text;
if (type_and_severity.severity == AppMenuIconController::Severity::NONE) {
message_id = IDS_APPMENU_TOOLTIP;
} else if (type_and_severity.type ==
AppMenuIconController::IconType::UPGRADE_NOTIFICATION) {
message_id = IDS_APPMENU_TOOLTIP_UPDATE_AVAILABLE;
text = l10n_util::GetStringUTF16(IDS_APP_MENU_BUTTON_UPDATE);
} else {
message_id = IDS_APPMENU_TOOLTIP_ALERT;
text = l10n_util::GetStringUTF16(IDS_APP_MENU_BUTTON_ERROR);
}
base::Optional<SkColor> color;
switch (type_and_severity.severity) {
case AppMenuIconController::Severity::NONE:
break;
case AppMenuIconController::Severity::LOW:
color = AdjustHighlightColorForContrast(
GetThemeProvider(), gfx::kGoogleGreen300, gfx::kGoogleGreen600,
gfx::kGoogleGreen050, gfx::kGoogleGreen900);
break;
case AppMenuIconController::Severity::MEDIUM:
color = AdjustHighlightColorForContrast(
GetThemeProvider(), gfx::kGoogleYellow300, gfx::kGoogleYellow600,
gfx::kGoogleYellow050, gfx::kGoogleYellow900);
break;
case AppMenuIconController::Severity::HIGH:
color = AdjustHighlightColorForContrast(
GetThemeProvider(), gfx::kGoogleRed300, gfx::kGoogleRed600,
gfx::kGoogleRed050, gfx::kGoogleRed900);
break;
}
if (base::FeatureList::IsEnabled(features::kUseTextForUpdateButton))
SetHighlight(text, color);
SetTooltipText(l10n_util::GetStringUTF16(message_id));
UpdateIcon();
}
void BrowserAppMenuButton::SetPromoFeature(
base::Optional<InProductHelpFeature> promo_feature) {
if (promo_feature_ == promo_feature)
return;
promo_feature_ = promo_feature;
// We override GetInkDropBaseColor() and CreateInkDropMask(), returning the
// promo values if we are showing an in-product help promo. Calling
// HostSizeChanged() will force the new mask and color to be fetched.
//
// TODO(collinbaker): Consider adding explicit way to recreate mask instead of
// relying on HostSizeChanged() to do so.
GetInkDrop()->HostSizeChanged(size());
views::InkDropState next_state;
if (promo_feature_ || IsMenuShowing()) {
// If we are showing a promo, we must use the ACTIVATED state to show the
// highlight. Otherwise, if the menu is currently showing, we need to keep
// the ink drop in the ACTIVATED state.
next_state = views::InkDropState::ACTIVATED;
} else {
// If we are not showing a promo and the menu is hidden, we use the
// DEACTIVATED state.
next_state = views::InkDropState::DEACTIVATED;
// TODO(collinbaker): this is brittle since we don't know if something else
// should keep this ACTIVATED or in some other state. Consider adding code
// to track the correct state and restore to that.
}
GetInkDrop()->AnimateToState(next_state);
UpdateIcon();
SchedulePaint();
}
void BrowserAppMenuButton::ShowMenu(int run_types) {
if (IsMenuShowing())
return;
#if defined(OS_CHROMEOS)
auto* keyboard_client = ChromeKeyboardControllerClient::Get();
if (keyboard_client->is_keyboard_visible())
keyboard_client->HideKeyboard(ash::HideReason::kSystem);
#endif
Browser* browser = toolbar_view_->browser();
bool alert_reopen_tab_items =
promo_feature_ == InProductHelpFeature::kReopenTab;
RunMenu(
std::make_unique<AppMenuModel>(toolbar_view_, browser,
toolbar_view_->app_menu_icon_controller()),
browser, run_types, alert_reopen_tab_items);
}
void BrowserAppMenuButton::OnThemeChanged() {
AppMenuButton::OnThemeChanged();
UpdateIcon();
}
void BrowserAppMenuButton::UpdateIcon() {
if (base::FeatureList::IsEnabled(features::kUseTextForUpdateButton)) {
SetImage(
views::Button::STATE_NORMAL,
gfx::CreateVectorIcon(
ui::MaterialDesignController::touch_ui() ? kBrowserToolsTouchIcon
: kBrowserToolsIcon,
toolbar_view_->app_menu_icon_controller()->GetIconColor(
GetPromoHighlightColor())));
return;
}
SetImage(
views::Button::STATE_NORMAL,
toolbar_view_->app_menu_icon_controller()->GetIconImage(
ui::MaterialDesignController::touch_ui(), GetPromoHighlightColor()));
}
void BrowserAppMenuButton::SetTrailingMargin(int margin) {
gfx::Insets* const internal_padding = GetProperty(views::kInternalPaddingKey);
if (internal_padding->right() == margin)
return;
internal_padding->set_right(margin);
UpdateBorder();
InvalidateLayout();
}
void BrowserAppMenuButton::OnTouchUiChanged() {
UpdateIcon();
UpdateBorder();
PreferredSizeChanged();
}
void BrowserAppMenuButton::OnBoundsChanged(const gfx::Rect& previous_bounds) {
// AppMenuButton overrides parts of the ToolbarButton behavior.
// BrowserAppMenuButton is hosted on the toolbar so we need to make sure that
// ToolbarButton backgrounds etc. are properly updated.
ToolbarButton::OnBoundsChanged(previous_bounds);
}
const char* BrowserAppMenuButton::GetClassName() const {
return "BrowserAppMenuButton";
}
void BrowserAppMenuButton::UpdateBorder() {
gfx::Insets new_insets = GetLayoutInsets(TOOLBAR_BUTTON) +
*GetProperty(views::kInternalPaddingKey);
if (!border() || border()->GetInsets() != new_insets)
SetBorder(views::CreateEmptyBorder(new_insets));
}
base::Optional<SkColor> BrowserAppMenuButton::GetPromoHighlightColor() const {
if (promo_feature_) {
return ToolbarButton::AdjustHighlightColorForContrast(
GetThemeProvider(), kFeaturePromoHighlightDarkColor,
kFeaturePromoHighlightLightColor,
kFeaturePromoHighlightDarkExtremeColor,
kFeaturePromoHighlightLightExtremeColor);
}
return base::nullopt;
}
gfx::Rect BrowserAppMenuButton::GetAnchorBoundsInScreen() const {
gfx::Rect bounds = GetBoundsInScreen();
gfx::Insets insets = GetToolbarInkDropInsets(this);
// If the button is extended, don't inset the trailing edge. The anchored menu
// should extend to the screen edge as well so the menu is easier to hit
// (Fitts's law).
// TODO(pbos): Make sure the button is aware of that it is being extended or
// not (margin_trailing_ cannot be used as it can be 0 in fullscreen on
// Touch). When this is implemented, use 0 as a replacement for
// margin_trailing_ in fullscreen only. Always keep the rest.
insets.Set(insets.top(), 0, insets.bottom(), 0);
bounds.Inset(insets);
return bounds;
}
bool BrowserAppMenuButton::GetDropFormats(
int* formats,
std::set<ui::ClipboardFormatType>* format_types) {
return BrowserActionDragData::GetDropFormats(format_types);
}
bool BrowserAppMenuButton::AreDropTypesRequired() {
return BrowserActionDragData::AreDropTypesRequired();
}
bool BrowserAppMenuButton::CanDrop(const ui::OSExchangeData& data) {
if (base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu))
return false;
return BrowserActionDragData::CanDrop(data,
toolbar_view_->browser()->profile());
}
void BrowserAppMenuButton::OnDragEntered(const ui::DropTargetEvent& event) {
DCHECK(!weak_factory_.HasWeakPtrs());
int run_types = views::MenuRunner::FOR_DROP;
if (event.IsKeyEvent())
run_types |= views::MenuRunner::SHOULD_SHOW_MNEMONICS;
if (!g_open_app_immediately_for_testing) {
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&BrowserAppMenuButton::ShowMenu,
weak_factory_.GetWeakPtr(), run_types),
base::TimeDelta::FromMilliseconds(views::GetMenuShowDelay()));
} else {
ShowMenu(run_types);
}
}
int BrowserAppMenuButton::OnDragUpdated(const ui::DropTargetEvent& event) {
return ui::DragDropTypes::DRAG_MOVE;
}
void BrowserAppMenuButton::OnDragExited() {
weak_factory_.InvalidateWeakPtrs();
}
int BrowserAppMenuButton::OnPerformDrop(const ui::DropTargetEvent& event) {
return ui::DragDropTypes::DRAG_MOVE;
}
std::unique_ptr<views::InkDropHighlight>
BrowserAppMenuButton::CreateInkDropHighlight() const {
return CreateToolbarInkDropHighlight(this);
}
std::unique_ptr<views::InkDropMask> BrowserAppMenuButton::CreateInkDropMask()
const {
if (promo_feature_) {
// This gets the latest ink drop insets. |SetTrailingMargin()| is called
// whenever our margins change (i.e. due to the window maximizing or
// minimizing) and updates our internal padding property accordingly.
const gfx::Insets ink_drop_insets = GetToolbarInkDropInsets(this);
const float corner_radius =
(height() - ink_drop_insets.top() - ink_drop_insets.bottom()) / 2.0f;
return std::make_unique<PulsingInkDropMask>(ink_drop_container(), size(),
ink_drop_insets, corner_radius,
kFeaturePromoPulseInsetDip);
}
return AppMenuButton::CreateInkDropMask();
}
SkColor BrowserAppMenuButton::GetInkDropBaseColor() const {
auto promo_highlight_color = GetPromoHighlightColor();
return promo_highlight_color ? promo_highlight_color.value()
: AppMenuButton::GetInkDropBaseColor();
}
base::string16 BrowserAppMenuButton::GetTooltipText(const gfx::Point& p) const {
// Suppress tooltip when IPH is showing.
if (promo_feature_)
return base::string16();
return AppMenuButton::GetTooltipText(p);
}