1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/ui/views/toolbar/browser_app_menu_button.h"
6 
7 #include <set>
8 
9 #include "base/bind.h"
10 #include "base/location.h"
11 #include "base/single_thread_task_runner.h"
12 #include "base/threading/thread_task_runner_handle.h"
13 #include "base/time/time.h"
14 #include "cc/paint/paint_flags.h"
15 #include "chrome/browser/ui/browser.h"
16 #include "chrome/browser/ui/browser_otr_state.h"
17 #include "chrome/browser/ui/layout_constants.h"
18 #include "chrome/browser/ui/toolbar/app_menu_model.h"
19 #include "chrome/browser/ui/ui_features.h"
20 #include "chrome/browser/ui/views/chrome_layout_provider.h"
21 #include "chrome/browser/ui/views/chrome_view_class_properties.h"
22 #include "chrome/browser/ui/views/extensions/browser_action_drag_data.h"
23 #include "chrome/browser/ui/views/frame/browser_view.h"
24 #include "chrome/browser/ui/views/toolbar/app_menu.h"
25 #include "chrome/browser/ui/views/toolbar/toolbar_button.h"
26 #include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h"
27 #include "chrome/browser/ui/views/toolbar/toolbar_view.h"
28 #include "chrome/browser/ui/views/user_education/feature_promo_colors.h"
29 #include "chrome/browser/ui/views/user_education/feature_promo_controller_views.h"
30 #include "chrome/grit/chromium_strings.h"
31 #include "chrome/grit/generated_resources.h"
32 #include "components/feature_engagement/public/feature_constants.h"
33 #include "ui/base/l10n/l10n_util.h"
34 #include "ui/base/resource/resource_bundle.h"
35 #include "ui/base/ui_base_features.h"
36 #include "ui/compositor/paint_recorder.h"
37 #include "ui/gfx/animation/throb_animation.h"
38 #include "ui/gfx/canvas.h"
39 #include "ui/gfx/color_palette.h"
40 #include "ui/gfx/color_utils.h"
41 #include "ui/gfx/paint_vector_icon.h"
42 #include "ui/views/animation/animation_delegate_views.h"
43 #include "ui/views/animation/ink_drop.h"
44 #include "ui/views/animation/ink_drop_highlight.h"
45 #include "ui/views/animation/ink_drop_mask.h"
46 #include "ui/views/animation/ink_drop_state.h"
47 #include "ui/views/controls/button/label_button_border.h"
48 #include "ui/views/metrics.h"
49 #include "ui/views/view.h"
50 #include "ui/views/view_class_properties.h"
51 
52 #if defined(OS_CHROMEOS)
53 #include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client.h"
54 #endif  // defined(OS_CHROMEOS)
55 
56 namespace {
57 
58 // Cycle duration of ink drop pulsing animation used for in-product help.
59 constexpr base::TimeDelta kFeaturePromoPulseDuration =
60     base::TimeDelta::FromMilliseconds(800);
61 
62 // Max inset for pulsing animation.
63 constexpr float kFeaturePromoPulseInsetDip = 3.0f;
64 
65 // An InkDropMask used to animate the size of the BrowserAppMenuButton's ink
66 // drop. This is used when showing in-product help.
67 class PulsingInkDropMask : public views::AnimationDelegateViews,
68                            public views::InkDropMask {
69  public:
PulsingInkDropMask(views::View * layer_container,const gfx::Size & layer_size,const gfx::Insets & margins,float normal_corner_radius,float max_inset)70   PulsingInkDropMask(views::View* layer_container,
71                      const gfx::Size& layer_size,
72                      const gfx::Insets& margins,
73                      float normal_corner_radius,
74                      float max_inset)
75       : AnimationDelegateViews(layer_container),
76         views::InkDropMask(layer_size),
77         layer_container_(layer_container),
78         margins_(margins),
79         normal_corner_radius_(normal_corner_radius),
80         max_inset_(max_inset),
81         throb_animation_(this) {
82     throb_animation_.SetThrobDuration(kFeaturePromoPulseDuration);
83     throb_animation_.StartThrobbing(-1);
84   }
85 
86  private:
87   // views::InkDropMask:
OnPaintLayer(const ui::PaintContext & context)88   void OnPaintLayer(const ui::PaintContext& context) override {
89     cc::PaintFlags flags;
90     flags.setStyle(cc::PaintFlags::kFill_Style);
91     flags.setAntiAlias(true);
92 
93     ui::PaintRecorder recorder(context, layer()->size());
94 
95     gfx::RectF bounds(layer()->bounds());
96     bounds.Inset(margins_);
97 
98     const float current_inset =
99         throb_animation_.CurrentValueBetween(0.0f, max_inset_);
100     bounds.Inset(gfx::InsetsF(current_inset));
101     const float corner_radius = normal_corner_radius_ - current_inset;
102 
103     recorder.canvas()->DrawRoundRect(bounds, corner_radius, flags);
104   }
105 
106   // views::AnimationDelegateViews:
AnimationProgressed(const gfx::Animation * animation)107   void AnimationProgressed(const gfx::Animation* animation) override {
108     DCHECK_EQ(animation, &throb_animation_);
109     layer()->SchedulePaint(gfx::Rect(layer()->size()));
110 
111     // This is a workaround for crbug.com/935808: for scale factors >1,
112     // invalidating the mask layer doesn't cause the whole layer to be repainted
113     // on screen. TODO(crbug.com/935808): remove this workaround once the bug is
114     // fixed.
115     layer_container_->SchedulePaint();
116   }
117 
118   // The View that contains the InkDrop layer we're masking. This must outlive
119   // our instance.
120   views::View* const layer_container_;
121 
122   // Margins between the layer bounds and the visible ink drop. We use this
123   // because sometimes the View we're masking is larger than the ink drop we
124   // want to show.
125   const gfx::Insets margins_;
126 
127   // Normal corner radius of the ink drop without animation. This is also the
128   // corner radius at the largest instant of the animation.
129   const float normal_corner_radius_;
130 
131   // Max inset, used at the smallest instant of the animation.
132   const float max_inset_;
133 
134   gfx::ThrobAnimation throb_animation_;
135 };
136 
137 }  // namespace
138 
139 // static
140 bool BrowserAppMenuButton::g_open_app_immediately_for_testing = false;
141 
BrowserAppMenuButton(PressedCallback callback,ToolbarView * toolbar_view)142 BrowserAppMenuButton::BrowserAppMenuButton(PressedCallback callback,
143                                            ToolbarView* toolbar_view)
144     : AppMenuButton(std::move(callback)), toolbar_view_(toolbar_view) {
145   SetInkDropMode(InkDropMode::ON);
146   SetHorizontalAlignment(gfx::ALIGN_RIGHT);
147 
148   SetInkDropVisibleOpacity(kToolbarInkDropVisibleOpacity);
149 }
150 
~BrowserAppMenuButton()151 BrowserAppMenuButton::~BrowserAppMenuButton() {}
152 
SetTypeAndSeverity(AppMenuIconController::TypeAndSeverity type_and_severity)153 void BrowserAppMenuButton::SetTypeAndSeverity(
154     AppMenuIconController::TypeAndSeverity type_and_severity) {
155   type_and_severity_ = type_and_severity;
156 
157   UpdateIcon();
158   UpdateTextAndHighlightColor();
159 }
160 
ShowMenu(int run_types)161 void BrowserAppMenuButton::ShowMenu(int run_types) {
162   if (IsMenuShowing())
163     return;
164 
165 #if defined(OS_CHROMEOS)
166   auto* keyboard_client = ChromeKeyboardControllerClient::Get();
167   if (keyboard_client->is_keyboard_visible())
168     keyboard_client->HideKeyboard(ash::HideReason::kSystem);
169 #endif
170 
171   Browser* browser = toolbar_view_->browser();
172 
173   FeaturePromoControllerViews* const feature_promo_controller =
174       BrowserView::GetBrowserViewForBrowser(toolbar_view_->browser())
175           ->feature_promo_controller();
176 
177   // If the menu was opened while reopen tab in-product help was
178   // showing, we continue the IPH into the menu. Notify the promo
179   // controller we are taking control of the promo.
180   DCHECK(!reopen_tab_promo_handle_);
181   if (feature_promo_controller->BubbleIsShowing(
182           feature_engagement::kIPHReopenTabFeature)) {
183     reopen_tab_promo_handle_ =
184         feature_promo_controller->CloseBubbleAndContinuePromo(
185             feature_engagement::kIPHReopenTabFeature);
186   }
187 
188   bool alert_reopen_tab_items = reopen_tab_promo_handle_.has_value();
189 
190   RunMenu(
191       std::make_unique<AppMenuModel>(toolbar_view_, browser,
192                                      toolbar_view_->app_menu_icon_controller()),
193       browser, run_types, alert_reopen_tab_items);
194 }
195 
OnThemeChanged()196 void BrowserAppMenuButton::OnThemeChanged() {
197   UpdateTextAndHighlightColor();
198   AppMenuButton::OnThemeChanged();
199 }
200 
UpdateIcon()201 void BrowserAppMenuButton::UpdateIcon() {
202   const gfx::VectorIcon& icon = ui::TouchUiController::Get()->touch_ui()
203                                     ? kBrowserToolsTouchIcon
204                                     : kBrowserToolsIcon;
205   for (auto state : kButtonStates) {
206     SkColor icon_color =
207         toolbar_view_->app_menu_icon_controller()->GetIconColor(
208             GetForegroundColor(state));
209     SetImageModel(state, ui::ImageModel::FromVectorIcon(icon, icon_color));
210   }
211 }
212 
HandleMenuClosed()213 void BrowserAppMenuButton::HandleMenuClosed() {
214   // If we were showing a promo in the menu, drop the handle to notify
215   // FeaturePromoController we're done. This is a no-op if we weren't
216   // showing the promo.
217   reopen_tab_promo_handle_.reset();
218 }
219 
AfterPropertyChange(const void * key,int64_t old_value)220 void BrowserAppMenuButton::AfterPropertyChange(const void* key,
221                                                int64_t old_value) {
222   if (key != kHasInProductHelpPromoKey)
223     return;
224   // FeaturePromoControllerViews sets the following property when a
225   // bubble is showing. When the state changes, update our highlight for
226   // the promo.
227   SetHasInProductHelpPromo(GetProperty(kHasInProductHelpPromoKey));
228 }
229 
UpdateTextAndHighlightColor()230 void BrowserAppMenuButton::UpdateTextAndHighlightColor() {
231   int tooltip_message_id;
232   base::string16 text;
233   if (type_and_severity_.severity == AppMenuIconController::Severity::NONE) {
234     tooltip_message_id = IDS_APPMENU_TOOLTIP;
235   } else if (type_and_severity_.type ==
236              AppMenuIconController::IconType::UPGRADE_NOTIFICATION) {
237     tooltip_message_id = IDS_APPMENU_TOOLTIP_UPDATE_AVAILABLE;
238     text = l10n_util::GetStringUTF16(IDS_APP_MENU_BUTTON_UPDATE);
239   } else {
240     tooltip_message_id = IDS_APPMENU_TOOLTIP_ALERT;
241     text = l10n_util::GetStringUTF16(IDS_APP_MENU_BUTTON_ERROR);
242   }
243 
244   base::Optional<SkColor> color;
245   switch (type_and_severity_.severity) {
246     case AppMenuIconController::Severity::NONE:
247       break;
248     case AppMenuIconController::Severity::LOW:
249       color = AdjustHighlightColorForContrast(
250           GetThemeProvider(), gfx::kGoogleGreen300, gfx::kGoogleGreen600,
251           gfx::kGoogleGreen050, gfx::kGoogleGreen900);
252 
253       break;
254     case AppMenuIconController::Severity::MEDIUM:
255       color = AdjustHighlightColorForContrast(
256           GetThemeProvider(), gfx::kGoogleYellow300, gfx::kGoogleYellow600,
257           gfx::kGoogleYellow050, gfx::kGoogleYellow900);
258 
259       break;
260     case AppMenuIconController::Severity::HIGH:
261       color = AdjustHighlightColorForContrast(
262           GetThemeProvider(), gfx::kGoogleRed300, gfx::kGoogleRed600,
263           gfx::kGoogleRed050, gfx::kGoogleRed900);
264 
265       break;
266   }
267 
268   SetTooltipText(l10n_util::GetStringUTF16(tooltip_message_id));
269   SetHighlight(text, color);
270 }
271 
SetHasInProductHelpPromo(bool has_in_product_help_promo)272 void BrowserAppMenuButton::SetHasInProductHelpPromo(
273     bool has_in_product_help_promo) {
274   if (has_in_product_help_promo_ == has_in_product_help_promo)
275     return;
276 
277   has_in_product_help_promo_ = has_in_product_help_promo;
278 
279   // We override GetInkDropBaseColor() and CreateInkDropMask(), returning the
280   // promo values if we are showing an in-product help promo. Calling
281   // HostSizeChanged() will force the new mask and color to be fetched.
282   //
283   // TODO(collinbaker): Consider adding explicit way to recreate mask instead of
284   // relying on HostSizeChanged() to do so.
285   GetInkDrop()->HostSizeChanged(size());
286 
287   views::InkDropState next_state;
288   if (has_in_product_help_promo_ || IsMenuShowing()) {
289     // If we are showing a promo, we must use the ACTIVATED state to show the
290     // highlight. Otherwise, if the menu is currently showing, we need to keep
291     // the ink drop in the ACTIVATED state.
292     next_state = views::InkDropState::ACTIVATED;
293   } else {
294     // If we are not showing a promo and the menu is hidden, we use the
295     // DEACTIVATED state.
296     next_state = views::InkDropState::DEACTIVATED;
297     // TODO(collinbaker): this is brittle since we don't know if something else
298     // should keep this ACTIVATED or in some other state. Consider adding code
299     // to track the correct state and restore to that.
300   }
301   GetInkDrop()->AnimateToState(next_state);
302 
303   UpdateIcon();
304   SchedulePaint();
305 }
306 
GetClassName() const307 const char* BrowserAppMenuButton::GetClassName() const {
308   return "BrowserAppMenuButton";
309 }
310 
GetForegroundColor(ButtonState state) const311 SkColor BrowserAppMenuButton::GetForegroundColor(ButtonState state) const {
312   return has_in_product_help_promo_
313              ? GetFeaturePromoHighlightColorForToolbar(GetThemeProvider())
314              : ToolbarButton::GetForegroundColor(state);
315 }
316 
GetDropFormats(int * formats,std::set<ui::ClipboardFormatType> * format_types)317 bool BrowserAppMenuButton::GetDropFormats(
318     int* formats,
319     std::set<ui::ClipboardFormatType>* format_types) {
320   return BrowserActionDragData::GetDropFormats(format_types);
321 }
322 
AreDropTypesRequired()323 bool BrowserAppMenuButton::AreDropTypesRequired() {
324   return BrowserActionDragData::AreDropTypesRequired();
325 }
326 
CanDrop(const ui::OSExchangeData & data)327 bool BrowserAppMenuButton::CanDrop(const ui::OSExchangeData& data) {
328   if (base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu))
329     return false;
330   return BrowserActionDragData::CanDrop(data,
331                                         toolbar_view_->browser()->profile());
332 }
333 
OnDragEntered(const ui::DropTargetEvent & event)334 void BrowserAppMenuButton::OnDragEntered(const ui::DropTargetEvent& event) {
335   DCHECK(!weak_factory_.HasWeakPtrs());
336   int run_types = views::MenuRunner::FOR_DROP;
337   if (event.IsKeyEvent())
338     run_types |= views::MenuRunner::SHOULD_SHOW_MNEMONICS;
339 
340   if (!g_open_app_immediately_for_testing) {
341     base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
342         FROM_HERE,
343         base::BindOnce(&BrowserAppMenuButton::ShowMenu,
344                        weak_factory_.GetWeakPtr(), run_types),
345         base::TimeDelta::FromMilliseconds(views::GetMenuShowDelay()));
346   } else {
347     ShowMenu(run_types);
348   }
349 }
350 
OnDragUpdated(const ui::DropTargetEvent & event)351 int BrowserAppMenuButton::OnDragUpdated(const ui::DropTargetEvent& event) {
352   return ui::DragDropTypes::DRAG_MOVE;
353 }
354 
OnDragExited()355 void BrowserAppMenuButton::OnDragExited() {
356   weak_factory_.InvalidateWeakPtrs();
357 }
358 
OnPerformDrop(const ui::DropTargetEvent & event)359 int BrowserAppMenuButton::OnPerformDrop(const ui::DropTargetEvent& event) {
360   return ui::DragDropTypes::DRAG_MOVE;
361 }
362 
363 std::unique_ptr<views::InkDropHighlight>
CreateInkDropHighlight() const364 BrowserAppMenuButton::CreateInkDropHighlight() const {
365   return CreateToolbarInkDropHighlight(this);
366 }
367 
CreateInkDropMask() const368 std::unique_ptr<views::InkDropMask> BrowserAppMenuButton::CreateInkDropMask()
369     const {
370   if (has_in_product_help_promo_) {
371     // This gets the latest ink drop insets. |SetTrailingMargin()| is called
372     // whenever our margins change (i.e. due to the window maximizing or
373     // minimizing) and updates our internal padding property accordingly.
374     const gfx::Insets ink_drop_insets = GetToolbarInkDropInsets(this);
375     const float corner_radius =
376         (height() - ink_drop_insets.top() - ink_drop_insets.bottom()) / 2.0f;
377     return std::make_unique<PulsingInkDropMask>(ink_drop_container(), size(),
378                                                 ink_drop_insets, corner_radius,
379                                                 kFeaturePromoPulseInsetDip);
380   }
381 
382   return AppMenuButton::CreateInkDropMask();
383 }
384 
GetInkDropBaseColor() const385 SkColor BrowserAppMenuButton::GetInkDropBaseColor() const {
386   return has_in_product_help_promo_
387              ? GetFeaturePromoHighlightColorForToolbar(GetThemeProvider())
388              : AppMenuButton::GetInkDropBaseColor();
389 }
390 
GetTooltipText(const gfx::Point & p) const391 base::string16 BrowserAppMenuButton::GetTooltipText(const gfx::Point& p) const {
392   // Suppress tooltip when IPH is showing.
393   if (has_in_product_help_promo_)
394     return base::string16();
395 
396   return AppMenuButton::GetTooltipText(p);
397 }
398 
OnTouchUiChanged()399 void BrowserAppMenuButton::OnTouchUiChanged() {
400   UpdateColorsAndInsets();
401   PreferredSizeChanged();
402 }
403