1 // Copyright 2019 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/webui_tab_counter_button.h"
6
7 #include <memory>
8
9 #include "base/bind.h"
10 #include "base/i18n/message_formatter.h"
11 #include "base/i18n/number_formatting.h"
12 #include "base/strings/string16.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/app/vector_icons/vector_icons.h"
15 #include "chrome/browser/themes/theme_properties.h"
16 #include "chrome/browser/ui/browser.h"
17 #include "chrome/browser/ui/layout_constants.h"
18 #include "chrome/browser/ui/tabs/tab_strip_model.h"
19 #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
20 #include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
21 #include "chrome/browser/ui/views/chrome_layout_provider.h"
22 #include "chrome/browser/ui/views/chrome_typography.h"
23 #include "chrome/browser/ui/views/chrome_view_class_properties.h"
24 #include "chrome/browser/ui/views/flying_indicator.h"
25 #include "chrome/browser/ui/views/frame/browser_view.h"
26 #include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h"
27 #include "chrome/browser/ui/views/user_education/feature_promo_colors.h"
28 #include "chrome/grit/generated_resources.h"
29 #include "components/vector_icons/vector_icons.h"
30 #include "ui/aura/window.h"
31 #include "ui/base/l10n/l10n_util.h"
32 #include "ui/base/models/image_model.h"
33 #include "ui/base/models/menu_separator_types.h"
34 #include "ui/base/models/simple_menu_model.h"
35 #include "ui/base/theme_provider.h"
36 #include "ui/gfx/animation/multi_animation.h"
37 #include "ui/gfx/animation/slide_animation.h"
38 #include "ui/gfx/animation/tween.h"
39 #include "ui/gfx/color_palette.h"
40 #include "ui/gfx/favicon_size.h"
41 #include "ui/gfx/geometry/point.h"
42 #include "ui/gfx/geometry/vector2d.h"
43 #include "ui/gfx/paint_vector_icon.h"
44 #include "ui/views/animation/ink_drop.h"
45 #include "ui/views/animation/ink_drop_highlight.h"
46 #include "ui/views/bubble/bubble_dialog_delegate_view.h"
47 #include "ui/views/bubble/bubble_frame_view.h"
48 #include "ui/views/context_menu_controller.h"
49 #include "ui/views/controls/image_view.h"
50 #include "ui/views/controls/label.h"
51 #include "ui/views/controls/menu/menu_model_adapter.h"
52 #include "ui/views/controls/menu/menu_runner.h"
53 #include "ui/views/controls/throbber.h"
54 #include "ui/views/layout/fill_layout.h"
55 #include "ui/views/layout/flex_layout.h"
56 #include "ui/views/layout/layout_provider.h"
57 #include "ui/views/style/typography.h"
58 #include "ui/views/view_class_properties.h"
59 #include "ui/views/widget/native_widget.h"
60 #include "ui/views/widget/widget.h"
61
62 namespace {
63
64 // The distance to move a label so it appears "offscreen" - that is, the text
65 // will be clipped by the border and not visible.
66 constexpr int kOffscreenLabelDistance = 16;
67
68 constexpr base::TimeDelta kFirstPartDuration =
69 base::TimeDelta::FromMilliseconds(100);
70
71 // Returns whether |change| to |tab_strip_mode| should start the tab counter
72 // throbber animation.
ShouldChangeStartThrobber(TabStripModel * tab_strip_model,const TabStripModelChange & change)73 bool ShouldChangeStartThrobber(TabStripModel* tab_strip_model,
74 const TabStripModelChange& change) {
75 if (change.type() != TabStripModelChange::kInserted)
76 return false;
77 const auto& contents = change.GetInsert()->contents;
78 return contents.size() > 1 ||
79 tab_strip_model->GetActiveWebContents() != contents[0].contents;
80 }
81
GetTabCounterLabelText(int num_tabs)82 base::string16 GetTabCounterLabelText(int num_tabs) {
83 // In the triple-digit case, fall back to ':D' to match Android.
84 if (num_tabs >= 100)
85 return base::string16(base::ASCIIToUTF16(":D"));
86 return base::FormatNumber(num_tabs);
87 }
88
89 //------------------------------------------------------------------------
90 // NumberLabel
91
92 // Label to display a number of tabs. Because there is limited space within the
93 // tab counter border, the font shrinks when the count is 10 or higher.
94 class NumberLabel : public views::Label {
95 public:
NumberLabel()96 NumberLabel() : Label(base::string16(), CONTEXT_TAB_COUNTER) {
97 single_digit_font_ = font_list();
98 double_digit_font_ = views::style::GetFont(CONTEXT_TAB_COUNTER,
99 views::style::STYLE_SECONDARY);
100 }
101
102 ~NumberLabel() override = default;
103
SetText(const base::string16 & text)104 void SetText(const base::string16& text) override {
105 SetFontList(text.length() > 1 ? double_digit_font_ : single_digit_font_);
106 Label::SetText(text);
107 }
108
109 private:
110 gfx::FontList single_digit_font_;
111 gfx::FontList double_digit_font_;
112 };
113
114 ///////////////////////////////////////////////////////////////////////////////
115 // InteractionTracker
116
117 // Listens in on the widget event stream (as a pre target event handler) and
118 // records user interactions (mouse clicks, taps, etc.) Used so that we know
119 // where a link that was opened in a background tab was opened from so that we
120 // can play a "flying link" animation.
121 class InteractionTracker : public ui::EventHandler,
122 public views::WidgetObserver {
123 public:
InteractionTracker(views::Widget * widget)124 explicit InteractionTracker(views::Widget* widget)
125 : native_window_(widget->GetNativeWindow()) {
126 if (native_window_)
127 native_window_->AddPreTargetHandler(this);
128 scoped_widget_observer_.Add(widget);
129 }
130
131 InteractionTracker(const InteractionTracker& other) = delete;
132 void operator=(const InteractionTracker& other) = delete;
133
~InteractionTracker()134 ~InteractionTracker() override {
135 if (native_window_)
136 native_window_->RemovePreTargetHandler(this);
137 }
138
last_interaction_location() const139 const base::Optional<gfx::Point>& last_interaction_location() const {
140 return last_interaction_location_;
141 }
142
143 private:
144 // ui::EventHandler:
OnEvent(ui::Event * event)145 void OnEvent(ui::Event* event) override {
146 if (event->type() == ui::ET_MOUSE_PRESSED ||
147 event->type() == ui::ET_MOUSE_RELEASED ||
148 event->type() == ui::ET_TOUCH_PRESSED) {
149 const ui::LocatedEvent* const located = event->AsLocatedEvent();
150 last_interaction_location_ =
151 located->target()->GetScreenLocation(*located);
152 }
153 }
154
155 // views::WidgetObserver:
OnWidgetBoundsChanged(views::Widget * widget,const gfx::Rect & new_bounds)156 void OnWidgetBoundsChanged(views::Widget* widget,
157 const gfx::Rect& new_bounds) override {
158 last_interaction_location_.reset();
159 }
OnWidgetDestroying(views::Widget * widget)160 void OnWidgetDestroying(views::Widget* widget) override {
161 // Clean up all of our observers and event handlers before the native window
162 // disappears.
163 scoped_widget_observer_.Remove(widget);
164 if (widget->GetNativeWindow()) {
165 widget->GetNativeWindow()->RemovePreTargetHandler(this);
166 native_window_ = nullptr;
167 }
168 }
169
170 base::Optional<gfx::Point> last_interaction_location_;
171 gfx::NativeWindow native_window_;
172 ScopedObserver<views::Widget, views::WidgetObserver> scoped_widget_observer_{
173 this};
174 };
175
176 //------------------------------------------------------------------------
177 // TabCounterAnimator
178
179 // Animates the label and border. |border_view_| does a little bounce. At the
180 // peak of |border_view_|'s bounce, the |disappearing_label_| begins to scroll
181 // away in the same direction and is replaced with |appearing_label_|, which
182 // shows the new number of tabs. This animation is played upside-down when a tab
183 // is added vs. removed.
184 class TabCounterAnimator : public gfx::AnimationDelegate {
185 public:
186 TabCounterAnimator(views::Label* appearing_label,
187 views::Label* disappearing_label,
188 views::View* border_view,
189 views::Throbber* throbber);
190 TabCounterAnimator(const TabCounterAnimator&) = delete;
191 void operator=(const TabCounterAnimator&) = delete;
192 ~TabCounterAnimator() override = default;
193
194 void Animate(int new_num_tabs, bool should_start_throbber);
195 void StartFlyingLinkFrom(const gfx::Point& screen_position);
196 void LayoutIfAnimating();
197
198 private:
199 // Describes the current counter animation (if any). The animation is played
200 // one way to show a decrease, and upside down from that to show an increase.
201 enum class TabCounterAnimationType { kNone, kIncreasing, kDecreasing };
202
203 // AnimationDelegate:
204 void AnimationProgressed(const gfx::Animation* animation) override;
205 void AnimationEnded(const gfx::Animation* animation) override;
206
207 void MaybeStartPendingAnimation();
208 void StartAnimation();
209
210 int GetBorderTargetYDelta() const;
211 int GetBorderOvershootYDelta() const;
212 int GetAppearingLabelStartPosition() const;
213 int GetDisappearingLabelTargetPosition() const;
214 int GetBorderStartingY() const;
215
216 base::Optional<int> last_num_tabs_;
217 base::Optional<int> pending_num_tabs_ = 0;
218 bool pending_throbber_ = false;
219 TabCounterAnimationType current_animation_ = TabCounterAnimationType::kNone;
220
221 // The label that will be animated into view, showing the new value.
222 views::Label* const appearing_label_;
223 // The label that will be animated out of view, showing the old value.
224 views::Label* const disappearing_label_;
225 gfx::MultiAnimation label_animation_;
226
227 views::View* const border_view_;
228 gfx::MultiAnimation border_animation_;
229
230 views::Throbber* const throbber_;
231 base::OneShotTimer throbber_timer_;
232
233 std::unique_ptr<FlyingIndicator> flying_link_;
234 };
235
TabCounterAnimator(views::Label * appearing_label,views::Label * disappearing_label,views::View * border_view,views::Throbber * throbber)236 TabCounterAnimator::TabCounterAnimator(views::Label* appearing_label,
237 views::Label* disappearing_label,
238 views::View* border_view,
239 views::Throbber* throbber)
240 : appearing_label_(appearing_label),
241 disappearing_label_(disappearing_label),
242 label_animation_(
243 std::vector<gfx::MultiAnimation::Part>{
244 // Stay in place.
245 gfx::MultiAnimation::Part(kFirstPartDuration,
246 gfx::Tween::Type::ZERO),
247 // Swap out to the new label.
248 gfx::MultiAnimation::Part(base::TimeDelta::FromMilliseconds(200),
249 gfx::Tween::Type::EASE_IN_OUT)},
250 gfx::MultiAnimation::kDefaultTimerInterval),
251 border_view_(border_view),
252 border_animation_(
253 std::vector<gfx::MultiAnimation::Part>{
254 gfx::MultiAnimation::Part(kFirstPartDuration,
255 gfx::Tween::Type::EASE_OUT),
256 gfx::MultiAnimation::Part(base::TimeDelta::FromMilliseconds(150),
257 gfx::Tween::Type::EASE_IN_OUT),
258 gfx::MultiAnimation::Part(base::TimeDelta::FromMilliseconds(50),
259 gfx::Tween::Type::EASE_IN_OUT)},
260 gfx::MultiAnimation::kDefaultTimerInterval),
261 throbber_(throbber) {
262 label_animation_.set_delegate(this);
263 label_animation_.set_continuous(false);
264
265 border_animation_.set_delegate(this);
266 border_animation_.set_continuous(false);
267 }
268
Animate(int new_num_tabs,bool should_start_throbber)269 void TabCounterAnimator::Animate(int new_num_tabs, bool should_start_throbber) {
270 pending_num_tabs_ = new_num_tabs;
271 pending_throbber_ |= should_start_throbber;
272 MaybeStartPendingAnimation();
273 }
274
MaybeStartPendingAnimation()275 void TabCounterAnimator::MaybeStartPendingAnimation() {
276 if (flying_link_ && flying_link_->is_flying())
277 return;
278
279 if (pending_throbber_) {
280 // If the throbber is already showing, just reset the timer so that the
281 // animation continues smoothly for tabs created in quick succession.
282 if (throbber_timer_.IsRunning()) {
283 throbber_timer_.Reset();
284 } else {
285 throbber_->Start();
286
287 // Automatically stop the throbber after 1 second. Currently we do not
288 // check the real loading state of the new tab(s), as that adds
289 // unnecessary complexity. The purpose of the throbber is just to
290 // indicate to the user that some activity has happened in the
291 // background, which may not otherwise have been obvious because the tab
292 // strip is hidden in this mode.
293 throbber_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(1000),
294 throbber_, &views::Throbber::Stop);
295 }
296 pending_throbber_ = false;
297 }
298
299 if (pending_num_tabs_.has_value()) {
300 if (last_num_tabs_.has_value() &&
301 last_num_tabs_.value() != pending_num_tabs_.value()) {
302 current_animation_ = pending_num_tabs_.value() > last_num_tabs_.value()
303 ? TabCounterAnimationType::kIncreasing
304 : TabCounterAnimationType::kDecreasing;
305 disappearing_label_->SetText(appearing_label_->GetText());
306 appearing_label_->SetText(
307 GetTabCounterLabelText(pending_num_tabs_.value()));
308 border_animation_.Stop();
309 border_animation_.Start();
310 label_animation_.Stop();
311 label_animation_.Start();
312 appearing_label_->InvalidateLayout();
313 LayoutIfAnimating();
314 } else if (!last_num_tabs_.has_value()) {
315 appearing_label_->SetText(
316 GetTabCounterLabelText(pending_num_tabs_.value()));
317 }
318 last_num_tabs_ = pending_num_tabs_;
319 }
320 }
321
StartFlyingLinkFrom(const gfx::Point & screen_position)322 void TabCounterAnimator::StartFlyingLinkFrom(
323 const gfx::Point& screen_position) {
324 flying_link_ = FlyingIndicator::StartFlyingIndicator(
325 kWebIcon, screen_position, throbber_,
326 base::BindOnce(&TabCounterAnimator::MaybeStartPendingAnimation,
327 base::Unretained(this)));
328 }
329
LayoutIfAnimating()330 void TabCounterAnimator::LayoutIfAnimating() {
331 if (!border_animation_.is_animating() && !label_animation_.is_animating())
332 return;
333
334 // |border_view_| does a hop or a dip based on animation type.
335 int border_y_delta = 0;
336 switch (border_animation_.current_part_index()) {
337 case 0:
338 // Move away.
339 border_y_delta = gfx::Tween::IntValueBetween(
340 border_animation_.GetCurrentValue(), 0, GetBorderTargetYDelta());
341 break;
342 case 1:
343 // Return, slightly overshooting the start position.
344 border_y_delta = gfx::Tween::IntValueBetween(
345 border_animation_.GetCurrentValue(), GetBorderTargetYDelta(),
346 GetBorderOvershootYDelta());
347 break;
348 case 2:
349 // Return back to the start position.
350 border_y_delta = gfx::Tween::IntValueBetween(
351 border_animation_.GetCurrentValue(), GetBorderOvershootYDelta(), 0);
352 break;
353 default:
354 NOTREACHED();
355 }
356 border_view_->SetY(GetBorderStartingY() + border_y_delta);
357
358 // |appearing_label_| scrolls into view - from above if the counter is
359 // increasing, below if it is decreasing.
360 const int appearing_label_position = gfx::Tween::IntValueBetween(
361 label_animation_.GetCurrentValue(), GetAppearingLabelStartPosition(), 0);
362 appearing_label_->SetY(appearing_label_position - border_y_delta);
363
364 // |disappearing_label_| scrolls out of view - out the bottom if
365 // |appearing_label_| is decreasing, and from below if increasing.
366 const int disappearing_label_position =
367 gfx::Tween::IntValueBetween(label_animation_.GetCurrentValue(), 0,
368 GetDisappearingLabelTargetPosition());
369 disappearing_label_->SetY(disappearing_label_position - border_y_delta);
370 }
371
AnimationProgressed(const gfx::Animation * animation)372 void TabCounterAnimator::AnimationProgressed(const gfx::Animation* animation) {
373 LayoutIfAnimating();
374 }
375
AnimationEnded(const gfx::Animation * animation)376 void TabCounterAnimator::AnimationEnded(const gfx::Animation* animation) {
377 AnimationProgressed(animation);
378 }
379
GetBorderTargetYDelta() const380 int TabCounterAnimator::GetBorderTargetYDelta() const {
381 constexpr int kBorderBounceDistance = 4;
382 switch (current_animation_) {
383 case TabCounterAnimationType::kIncreasing:
384 return kBorderBounceDistance;
385 case TabCounterAnimationType::kDecreasing:
386 return -kBorderBounceDistance;
387 default:
388 NOTREACHED();
389 return 0;
390 }
391 }
392
GetBorderOvershootYDelta() const393 int TabCounterAnimator::GetBorderOvershootYDelta() const {
394 constexpr int kBorderBounceOvershoot = 2;
395 switch (current_animation_) {
396 case TabCounterAnimationType::kIncreasing:
397 return -kBorderBounceOvershoot;
398 case TabCounterAnimationType::kDecreasing:
399 return kBorderBounceOvershoot;
400 default:
401 NOTREACHED();
402 return 0;
403 }
404 }
405
GetAppearingLabelStartPosition() const406 int TabCounterAnimator::GetAppearingLabelStartPosition() const {
407 switch (current_animation_) {
408 case TabCounterAnimationType::kIncreasing:
409 return -kOffscreenLabelDistance;
410 case TabCounterAnimationType::kDecreasing:
411 return kOffscreenLabelDistance;
412 default:
413 NOTREACHED();
414 return 0;
415 }
416 }
417
GetDisappearingLabelTargetPosition() const418 int TabCounterAnimator::GetDisappearingLabelTargetPosition() const {
419 // We want to exit out the opposite side that |appearing_label_| entered
420 // from.
421 return -GetAppearingLabelStartPosition();
422 }
423
GetBorderStartingY() const424 int TabCounterAnimator::GetBorderStartingY() const {
425 // When at rest, |border_view_| should be vertically centered within its
426 // container.
427 views::View* border_container = border_view_->parent();
428 int border_available_space = border_container->GetLocalBounds().height();
429 return (border_available_space - border_view_->GetLocalBounds().height()) / 2;
430 }
431
432 //------------------------------------------------------------------------
433 // WebUITabCounterButton
434
435 class WebUITabCounterButton : public views::Button,
436 public TabStripModelObserver,
437 public views::ContextMenuController,
438 public ui::SimpleMenuModel::Delegate {
439 public:
440 static constexpr int WEBUI_TAB_COUNTER_CXMENU_CLOSE_TAB = 13;
441 static constexpr int WEBUI_TAB_COUNTER_CXMENU_NEW_TAB = 14;
442
443 WebUITabCounterButton(PressedCallback pressed_callback,
444 BrowserView* browser_view);
445 ~WebUITabCounterButton() override;
446
447 void UpdateTooltip(int tab_count);
448 void UpdateColors();
449 void Init();
450
451 private:
452 // views::Button:
453 void AddedToWidget() override;
454 void AfterPropertyChange(const void* key, int64_t old_value) override;
455 void AddLayerBeneathView(ui::Layer* new_layer) override;
456 void RemoveLayerBeneathView(ui::Layer* old_layer) override;
457 void OnThemeChanged() override;
458 void Layout() override;
459
460 // TabStripModelObserver:
461 void OnTabStripModelChanged(
462 TabStripModel* tab_strip_model,
463 const TabStripModelChange& change,
464 const TabStripSelectionChange& selection) override;
465
466 // views::ContextMenuController:
467 void ShowContextMenuForViewImpl(views::View* source,
468 const gfx::Point& point,
469 ui::MenuSourceType source_type) override;
470
471 // ui::SimpleMenuModel::Delegate:
472 void ExecuteCommand(int command_id, int event_flags) override;
473
474 void MaybeStartFlyingLink(WindowOpenDisposition disposition);
475
476 views::InkDropContainerView* ink_drop_container_;
477 views::Label* appearing_label_;
478 views::Label* disappearing_label_;
479 views::View* border_view_;
480 std::unique_ptr<TabCounterAnimator> animator_;
481 views::Throbber* throbber_;
482
483 std::unique_ptr<ui::SimpleMenuModel> menu_model_;
484 std::unique_ptr<views::MenuRunner> menu_runner_;
485 std::unique_ptr<InteractionTracker> interaction_tracker_;
486
487 TabStripModel* const tab_strip_model_;
488 BrowserView* const browser_view_;
489 BrowserView::OnLinkOpeningFromGestureSubscription
490 link_opened_from_gesture_subscription_;
491 };
492
WebUITabCounterButton(PressedCallback pressed_callback,BrowserView * browser_view)493 WebUITabCounterButton::WebUITabCounterButton(PressedCallback pressed_callback,
494 BrowserView* browser_view)
495 : Button(std::move(pressed_callback)),
496 tab_strip_model_(browser_view->browser()->tab_strip_model()),
497 browser_view_(browser_view) {
498 // Not focusable by default, only for accessibility.
499 SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
500 }
501
502 WebUITabCounterButton::~WebUITabCounterButton() = default;
503
UpdateTooltip(int num_tabs)504 void WebUITabCounterButton::UpdateTooltip(int num_tabs) {
505 SetTooltipText(base::i18n::MessageFormatter::FormatWithNumberedArgs(
506 l10n_util::GetStringUTF16(IDS_TOOLTIP_WEBUI_TAB_STRIP_TAB_COUNTER),
507 num_tabs));
508 }
509
UpdateColors()510 void WebUITabCounterButton::UpdateColors() {
511 const ui::ThemeProvider* theme_provider = GetThemeProvider();
512 const SkColor toolbar_color =
513 theme_provider ? theme_provider->GetColor(ThemeProperties::COLOR_TOOLBAR)
514 : gfx::kPlaceholderColor;
515 appearing_label_->SetBackgroundColor(toolbar_color);
516 disappearing_label_->SetBackgroundColor(toolbar_color);
517
518 const SkColor normal_text_color =
519 theme_provider
520 ? theme_provider->GetColor(ThemeProperties::COLOR_TOOLBAR_BUTTON_ICON)
521 : gfx::kPlaceholderColor;
522 const SkColor current_text_color =
523 GetProperty(kHasInProductHelpPromoKey)
524 ? GetFeaturePromoHighlightColorForToolbar(theme_provider)
525 : normal_text_color;
526
527 appearing_label_->SetEnabledColor(current_text_color);
528 disappearing_label_->SetEnabledColor(current_text_color);
529 border_view_->SetBorder(views::CreateRoundedRectBorder(
530 2,
531 views::LayoutProvider::Get()->GetCornerRadiusMetric(
532 views::EMPHASIS_MEDIUM),
533 current_text_color));
534 }
535
Init()536 void WebUITabCounterButton::Init() {
537 SetProperty(
538 views::kFlexBehaviorKey,
539 views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
540 views::MaximumFlexSizeRule::kPreferred)
541 .WithOrder(1));
542
543 const int button_height = GetLayoutConstant(TOOLBAR_BUTTON_HEIGHT);
544 SetPreferredSize(gfx::Size(button_height, button_height));
545
546 ink_drop_container_ =
547 AddChildView(std::make_unique<views::InkDropContainerView>());
548 ink_drop_container_->SetBoundsRect(GetLocalBounds());
549
550 throbber_ = AddChildView(std::make_unique<views::Throbber>());
551
552 border_view_ = AddChildView(std::make_unique<views::View>());
553
554 appearing_label_ =
555 border_view_->AddChildView(std::make_unique<NumberLabel>());
556 disappearing_label_ =
557 border_view_->AddChildView(std::make_unique<NumberLabel>());
558
559 animator_ = std::make_unique<TabCounterAnimator>(
560 appearing_label_, disappearing_label_, border_view_, throbber_);
561
562 set_context_menu_controller(this);
563 menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
564 menu_model_->AddItemWithIcon(
565 WEBUI_TAB_COUNTER_CXMENU_CLOSE_TAB,
566 l10n_util::GetStringUTF16(
567 IDS_WEBUI_TAB_STRIP_TAB_COUNTER_CXMENU_CLOSE_TAB),
568 ui::ImageModel::FromImageSkia(gfx::CreateVectorIcon(
569 vector_icons::kCloseIcon, gfx::kFaviconSize, SK_ColorGRAY)));
570 menu_model_->AddSeparator(ui::MenuSeparatorType::NORMAL_SEPARATOR);
571 menu_model_->AddItemWithIcon(
572 WEBUI_TAB_COUNTER_CXMENU_NEW_TAB,
573 l10n_util::GetStringUTF16(IDS_WEBUI_TAB_STRIP_TAB_COUNTER_CXMENU_NEW_TAB),
574 ui::ImageModel::FromImageSkia(
575 gfx::CreateVectorIcon(kAddIcon, gfx::kFaviconSize, SK_ColorGRAY)));
576 menu_runner_ = std::make_unique<views::MenuRunner>(
577 menu_model_.get(), views::MenuRunner::HAS_MNEMONICS |
578 views::MenuRunner::CONTEXT_MENU |
579 views::MenuRunner::FIXED_ANCHOR);
580
581 tab_strip_model_->AddObserver(this);
582 const int tab_count = tab_strip_model_->count();
583 UpdateTooltip(tab_count);
584 appearing_label_->SetText(GetTabCounterLabelText(tab_count));
585 }
586
AddedToWidget()587 void WebUITabCounterButton::AddedToWidget() {
588 interaction_tracker_ = std::make_unique<InteractionTracker>(GetWidget());
589 link_opened_from_gesture_subscription_ =
590 browser_view_->AddOnLinkOpeningFromGestureCallback(
591 base::BindRepeating(&WebUITabCounterButton::MaybeStartFlyingLink,
592 base::Unretained(this)));
593 }
594
AfterPropertyChange(const void * key,int64_t old_value)595 void WebUITabCounterButton::AfterPropertyChange(const void* key,
596 int64_t old_value) {
597 if (key != kHasInProductHelpPromoKey)
598 return;
599 UpdateColors();
600 }
601
AddLayerBeneathView(ui::Layer * new_layer)602 void WebUITabCounterButton::AddLayerBeneathView(ui::Layer* new_layer) {
603 ink_drop_container_->AddLayerBeneathView(new_layer);
604 }
605
RemoveLayerBeneathView(ui::Layer * old_layer)606 void WebUITabCounterButton::RemoveLayerBeneathView(ui::Layer* old_layer) {
607 ink_drop_container_->RemoveLayerBeneathView(old_layer);
608 }
609
OnThemeChanged()610 void WebUITabCounterButton::OnThemeChanged() {
611 views::Button::OnThemeChanged();
612 UpdateColors();
613 ConfigureInkDropForToolbar(this);
614 }
615
Layout()616 void WebUITabCounterButton::Layout() {
617 const gfx::Rect view_bounds = GetLocalBounds();
618
619 // Position views from the outside in (beacuse it's easier).
620 // Start with the throbber.
621 const int throbber_height = GetLayoutConstant(LOCATION_BAR_HEIGHT);
622 gfx::Rect throbber_rect = view_bounds;
623 throbber_rect.ClampToCenteredSize(
624 gfx::Size(throbber_height, throbber_height));
625 throbber_->SetBoundsRect(throbber_rect);
626
627 // Next is the rounded rect border around the counter.
628 constexpr gfx::Size kDesiredBorderSize(22, 22);
629 gfx::Rect border_bounds = view_bounds;
630 border_bounds.ClampToCenteredSize(kDesiredBorderSize);
631 border_view_->SetBoundsRect(border_bounds);
632
633 // Finally is the numbers themselves, which nest inside the label view.
634 appearing_label_->SetBoundsRect(gfx::Rect(kDesiredBorderSize));
635 disappearing_label_->SetBoundsRect(
636 gfx::Rect(gfx::Point(0, -kOffscreenLabelDistance), kDesiredBorderSize));
637
638 // Adjust label positions for animation.
639 animator_->LayoutIfAnimating();
640 }
641
MaybeStartFlyingLink(WindowOpenDisposition disposition)642 void WebUITabCounterButton::MaybeStartFlyingLink(
643 WindowOpenDisposition disposition) {
644 if (disposition == WindowOpenDisposition::NEW_BACKGROUND_TAB &&
645 interaction_tracker_ &&
646 interaction_tracker_->last_interaction_location().has_value())
647 animator_->StartFlyingLinkFrom(
648 interaction_tracker_->last_interaction_location().value());
649 }
650
OnTabStripModelChanged(TabStripModel * tab_strip_model,const TabStripModelChange & change,const TabStripSelectionChange & selection)651 void WebUITabCounterButton::OnTabStripModelChanged(
652 TabStripModel* tab_strip_model,
653 const TabStripModelChange& change,
654 const TabStripSelectionChange& selection) {
655 const int num_tabs = tab_strip_model->count();
656 UpdateTooltip(num_tabs);
657 animator_->Animate(num_tabs,
658 ShouldChangeStartThrobber(tab_strip_model, change));
659 }
660
ShowContextMenuForViewImpl(views::View * source,const gfx::Point & point,ui::MenuSourceType source_type)661 void WebUITabCounterButton::ShowContextMenuForViewImpl(
662 views::View* source,
663 const gfx::Point& point,
664 ui::MenuSourceType source_type) {
665 menu_runner_->RunMenuAt(GetWidget(), nullptr,
666 border_view_->GetBoundsInScreen(),
667 views::MenuAnchorPosition::kTopRight, source_type);
668 }
669
ExecuteCommand(int command_id,int event_flags)670 void WebUITabCounterButton::ExecuteCommand(int command_id, int event_flags) {
671 switch (command_id) {
672 case WEBUI_TAB_COUNTER_CXMENU_CLOSE_TAB: {
673 tab_strip_model_->CloseWebContentsAt(
674 tab_strip_model_->active_index(),
675 TabStripModel::CLOSE_USER_GESTURE |
676 TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
677 break;
678 }
679 case WEBUI_TAB_COUNTER_CXMENU_NEW_TAB:
680 tab_strip_model_->delegate()->AddTabAt(GURL(), -1, true);
681 break;
682 default:
683 NOTREACHED();
684 }
685 }
686
687 } // namespace
688
CreateWebUITabCounterButton(views::Button::PressedCallback pressed_callback,BrowserView * browser_view)689 std::unique_ptr<views::View> CreateWebUITabCounterButton(
690 views::Button::PressedCallback pressed_callback,
691 BrowserView* browser_view) {
692 auto tab_counter = std::make_unique<WebUITabCounterButton>(
693 std::move(pressed_callback), browser_view);
694
695 tab_counter->Init();
696
697 return tab_counter;
698 }
699