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