Revert "Delete tab pulsing feature."

This reverts commit 1dd90008554cac4a0a1637f7ec1d45aeb16eb254.

Reason for revert: breaks linux-jumbo-rel (failure log: https://logs.chromium.org/logs/chromium/buildbucket/cr-buildbucket.appspot.com/8924308573690053872/+/steps/compile/0/stdout)

Original change's description:
> Delete tab pulsing feature.
> 
> Previously, when hovering tab context menu items "Close tabs to the
> right" and "Close other tabs", the affected tabs would pulse. This patch
> deletes that feature. This caused SimpleMenuModel::CommandIdHighlighted
> and transitively MenuModel::HighlightChangedTo to become unused. This
> also caused TabStrip::animation_container_ to become effectively unused,
> since pulsing was the only animation that needed to be coordinated
> between tabs.
> 
> Bug: 921243
> Change-Id: Iae4ef39d979fa5f5a96ffc465275f25385f5949f
> Reviewed-on: https://chromium-review.googlesource.com/c/1407958
> Commit-Queue: Bret Sepulveda <bsep@chromium.org>
> Reviewed-by: Drew Wilson <atwilson@chromium.org>
> Reviewed-by: Scott Violet <sky@chromium.org>
> Reviewed-by: Avi Drissman <avi@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#622657}

TBR=avi@chromium.org,sky@chromium.org,atwilson@chromium.org,bsep@chromium.org

Change-Id: I96285c3f0b2601cc59c98b5d8abde97fd04afdf4
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: 921243
Reviewed-on: https://chromium-review.googlesource.com/c/1409886
Reviewed-by: Kevin Marshall <kmarshall@chromium.org>
Commit-Queue: Kevin Marshall <kmarshall@chromium.org>
Cr-Commit-Position: refs/heads/master@{#622668}
diff --git a/chrome/browser/status_icons/status_icon_menu_model.cc b/chrome/browser/status_icons/status_icon_menu_model.cc
index 6deae7c..2d46f71 100644
--- a/chrome/browser/status_icons/status_icon_menu_model.cc
+++ b/chrome/browser/status_icons/status_icon_menu_model.cc
@@ -25,6 +25,12 @@
 };
 
 ////////////////////////////////////////////////////////////////////////////////
+// StatusIconMenuModel::Delegate, public:
+
+void StatusIconMenuModel::Delegate::CommandIdHighlighted(int command_id) {
+}
+
+////////////////////////////////////////////////////////////////////////////////
 // StatusIconMenuModel, public:
 
 StatusIconMenuModel::StatusIconMenuModel(Delegate* delegate)
@@ -163,6 +169,11 @@
 ////////////////////////////////////////////////////////////////////////////////
 // StatusIconMenuModel, private:
 
+void StatusIconMenuModel::CommandIdHighlighted(int command_id) {
+  if (delegate_)
+    delegate_->CommandIdHighlighted(command_id);
+}
+
 void StatusIconMenuModel::ExecuteCommand(int command_id, int event_flags) {
   if (delegate_)
     delegate_->ExecuteCommand(command_id, event_flags);
diff --git a/chrome/browser/status_icons/status_icon_menu_model.h b/chrome/browser/status_icons/status_icon_menu_model.h
index 556f531f..c1514ee 100644
--- a/chrome/browser/status_icons/status_icon_menu_model.h
+++ b/chrome/browser/status_icons/status_icon_menu_model.h
@@ -28,6 +28,10 @@
  public:
   class Delegate {
    public:
+    // Notifies the delegate that the item with the specified command id was
+    // visually highlighted within the menu.
+    virtual void CommandIdHighlighted(int command_id);
+
     // Performs the action associates with the specified command id.
     // The passed |event_flags| are the flags from the event which issued this
     // command and they can be examined to find modifier keys.
@@ -93,6 +97,7 @@
 
  private:
   // Overridden from ui::SimpleMenuModel::Delegate:
+  void CommandIdHighlighted(int command_id) override;
   void ExecuteCommand(int command_id, int event_flags) override;
 
   struct ItemState;
diff --git a/chrome/browser/ui/tabs/tab_strip_model.cc b/chrome/browser/ui/tabs/tab_strip_model.cc
index 7dc8104..67bc9fb 100644
--- a/chrome/browser/ui/tabs/tab_strip_model.cc
+++ b/chrome/browser/ui/tabs/tab_strip_model.cc
@@ -1200,6 +1200,27 @@
   }
 }
 
+std::vector<int> TabStripModel::GetIndicesClosedByCommand(
+    int index,
+    ContextMenuCommand id) const {
+  DCHECK(ContainsIndex(index));
+  DCHECK(id == CommandCloseTabsToRight || id == CommandCloseOtherTabs);
+  bool is_selected = IsTabSelected(index);
+  int last_unclosed_tab = -1;
+  if (id == CommandCloseTabsToRight) {
+    last_unclosed_tab =
+        is_selected ? selection_model_.selected_indices().back() : index;
+  }
+
+  // NOTE: callers expect the vector to be sorted in descending order.
+  std::vector<int> indices;
+  for (int i = count() - 1; i > last_unclosed_tab; --i) {
+    if (i != index && !IsTabPinned(i) && (!is_selected || !IsTabSelected(i)))
+      indices.push_back(i);
+  }
+  return indices;
+}
+
 bool TabStripModel::WillContextMenuMute(int index) {
   std::vector<int> indices = GetIndicesForCommand(index);
   return !chrome::AreAllTabsMuted(*this, indices);
@@ -1316,27 +1337,6 @@
   return selection_model_.selected_indices();
 }
 
-std::vector<int> TabStripModel::GetIndicesClosedByCommand(
-    int index,
-    ContextMenuCommand id) const {
-  DCHECK(ContainsIndex(index));
-  DCHECK(id == CommandCloseTabsToRight || id == CommandCloseOtherTabs);
-  bool is_selected = IsTabSelected(index);
-  int last_unclosed_tab = -1;
-  if (id == CommandCloseTabsToRight) {
-    last_unclosed_tab =
-        is_selected ? selection_model_.selected_indices().back() : index;
-  }
-
-  // NOTE: callers expect the vector to be sorted in descending order.
-  std::vector<int> indices;
-  for (int i = count() - 1; i > last_unclosed_tab; --i) {
-    if (i != index && !IsTabPinned(i) && (!is_selected || !IsTabSelected(i)))
-      indices.push_back(i);
-  }
-  return indices;
-}
-
 bool TabStripModel::IsNewTabAtEndOfTabStrip(WebContents* contents) const {
   const GURL& url = contents->GetLastCommittedURL();
   return url.SchemeIs(content::kChromeUIScheme) &&
diff --git a/chrome/browser/ui/tabs/tab_strip_model.h b/chrome/browser/ui/tabs/tab_strip_model.h
index aeca460e..7dfc763 100644
--- a/chrome/browser/ui/tabs/tab_strip_model.h
+++ b/chrome/browser/ui/tabs/tab_strip_model.h
@@ -12,7 +12,6 @@
 #include <vector>
 
 #include "base/containers/span.h"
-#include "base/gtest_prod_util.h"
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
@@ -379,6 +378,12 @@
   void ExecuteContextMenuCommand(int context_index,
                                  ContextMenuCommand command_id);
 
+  // Returns a vector of indices of the tabs that will close when executing the
+  // command |id| for the tab at |index|. The returned indices are sorted in
+  // descending order.
+  std::vector<int> GetIndicesClosedByCommand(int index,
+                                             ContextMenuCommand id) const;
+
   // Returns true if 'CommandToggleTabAudioMuted' will mute. |index| is the
   // index supplied to |ExecuteContextMenuCommand|.
   bool WillContextMenuMute(int index);
@@ -419,8 +424,6 @@
   bool ShouldResetOpenerOnActiveTabChange(content::WebContents* contents) const;
 
  private:
-  FRIEND_TEST_ALL_PREFIXES(TabStripModelTest, GetIndicesClosedByCommand);
-
   class WebContentsData;
   struct DetachedWebContents;
   struct DetachNotifications;
@@ -457,12 +460,6 @@
   // increasing order.
   std::vector<int> GetIndicesForCommand(int index) const;
 
-  // Returns a vector of indices of the tabs that will close when executing the
-  // command |id| for the tab at |index|. The returned indices are sorted in
-  // descending order.
-  std::vector<int> GetIndicesClosedByCommand(int index,
-                                             ContextMenuCommand id) const;
-
   // Returns true if the specified WebContents is a New Tab at the end of
   // the tabstrip. We check for this because opener relationships are _not_
   // forgotten for the New Tab page opened as a result of a New Tab gesture
diff --git a/chrome/browser/ui/tabs/tab_strip_model_unittest.cc b/chrome/browser/ui/tabs/tab_strip_model_unittest.cc
index d5a3afb55..fc006c5 100644
--- a/chrome/browser/ui/tabs/tab_strip_model_unittest.cc
+++ b/chrome/browser/ui/tabs/tab_strip_model_unittest.cc
@@ -427,6 +427,20 @@
     return actual;
   }
 
+  std::string GetIndicesClosedByCommandAsString(
+      const TabStripModel& model,
+      int index,
+      TabStripModel::ContextMenuCommand id) const {
+    std::vector<int> indices = model.GetIndicesClosedByCommand(index, id);
+    std::string result;
+    for (size_t i = 0; i < indices.size(); ++i) {
+      if (i != 0)
+        result += " ";
+      result += base::IntToString(indices[i]);
+    }
+    return result;
+  }
+
   void PrepareTabstripForSelectionTest(TabStripModel* model,
                                        int tab_count,
                                        int pinned_count,
@@ -1328,46 +1342,34 @@
   TabStripModel tabstrip(&delegate, profile());
   EXPECT_TRUE(tabstrip.empty());
 
-  const auto indicesClosedAsString =
-      [&tabstrip](int index, TabStripModel::ContextMenuCommand id) {
-        std::vector<int> indices =
-            tabstrip.GetIndicesClosedByCommand(index, id);
-        std::string result;
-        for (size_t i = 0; i < indices.size(); ++i) {
-          if (i != 0)
-            result += " ";
-          result += base::IntToString(indices[i]);
-        }
-        return result;
-      };
-
   for (int i = 0; i < 5; ++i)
     tabstrip.AppendWebContents(CreateWebContents(), true);
 
   EXPECT_EQ("4 3 2 1",
-            indicesClosedAsString(0, TabStripModel::CommandCloseTabsToRight));
-  EXPECT_EQ("4 3 2",
-            indicesClosedAsString(1, TabStripModel::CommandCloseTabsToRight));
+            GetIndicesClosedByCommandAsString(
+                tabstrip, 0, TabStripModel::CommandCloseTabsToRight));
+  EXPECT_EQ("4 3 2", GetIndicesClosedByCommandAsString(
+                         tabstrip, 1, TabStripModel::CommandCloseTabsToRight));
 
-  EXPECT_EQ("4 3 2 1",
-            indicesClosedAsString(0, TabStripModel::CommandCloseOtherTabs));
-  EXPECT_EQ("4 3 2 0",
-            indicesClosedAsString(1, TabStripModel::CommandCloseOtherTabs));
+  EXPECT_EQ("4 3 2 1", GetIndicesClosedByCommandAsString(
+                           tabstrip, 0, TabStripModel::CommandCloseOtherTabs));
+  EXPECT_EQ("4 3 2 0", GetIndicesClosedByCommandAsString(
+                           tabstrip, 1, TabStripModel::CommandCloseOtherTabs));
 
   // Pin the first two tabs. Pinned tabs shouldn't be closed by the close other
   // commands.
   tabstrip.SetTabPinned(0, true);
   tabstrip.SetTabPinned(1, true);
 
-  EXPECT_EQ("4 3 2",
-            indicesClosedAsString(0, TabStripModel::CommandCloseTabsToRight));
-  EXPECT_EQ("4 3",
-            indicesClosedAsString(2, TabStripModel::CommandCloseTabsToRight));
+  EXPECT_EQ("4 3 2", GetIndicesClosedByCommandAsString(
+                         tabstrip, 0, TabStripModel::CommandCloseTabsToRight));
+  EXPECT_EQ("4 3", GetIndicesClosedByCommandAsString(
+                       tabstrip, 2, TabStripModel::CommandCloseTabsToRight));
 
-  EXPECT_EQ("4 3 2",
-            indicesClosedAsString(0, TabStripModel::CommandCloseOtherTabs));
-  EXPECT_EQ("4 3",
-            indicesClosedAsString(2, TabStripModel::CommandCloseOtherTabs));
+  EXPECT_EQ("4 3 2", GetIndicesClosedByCommandAsString(
+                         tabstrip, 0, TabStripModel::CommandCloseOtherTabs));
+  EXPECT_EQ("4 3", GetIndicesClosedByCommandAsString(
+                       tabstrip, 2, TabStripModel::CommandCloseOtherTabs));
 
   tabstrip.CloseAllTabs();
   EXPECT_TRUE(tabstrip.empty());
diff --git a/chrome/browser/ui/toolbar/back_forward_menu_model.cc b/chrome/browser/ui/toolbar/back_forward_menu_model.cc
index 66543de..39be1df 100644
--- a/chrome/browser/ui/toolbar/back_forward_menu_model.cc
+++ b/chrome/browser/ui/toolbar/back_forward_menu_model.cc
@@ -156,6 +156,9 @@
   return nullptr;
 }
 
+void BackForwardMenuModel::HighlightChangedTo(int index) {
+}
+
 void BackForwardMenuModel::ActivatedAt(int index) {
   ActivatedAt(index, 0);
 }
diff --git a/chrome/browser/ui/toolbar/back_forward_menu_model.h b/chrome/browser/ui/toolbar/back_forward_menu_model.h
index 5139414..48fd16ac 100644
--- a/chrome/browser/ui/toolbar/back_forward_menu_model.h
+++ b/chrome/browser/ui/toolbar/back_forward_menu_model.h
@@ -66,6 +66,7 @@
   ui::ButtonMenuItemModel* GetButtonMenuItemAt(int index) const override;
   bool IsEnabledAt(int index) const override;
   MenuModel* GetSubmenuModelAt(int index) const override;
+  void HighlightChangedTo(int index) override;
   void ActivatedAt(int index) override;
   void ActivatedAt(int index, int event_flags) override;
   void MenuWillShow() override;
diff --git a/chrome/browser/ui/views/menu_model_adapter_test.cc b/chrome/browser/ui/views/menu_model_adapter_test.cc
index 9021259..7e52499 100644
--- a/chrome/browser/ui/views/menu_model_adapter_test.cc
+++ b/chrome/browser/ui/views/menu_model_adapter_test.cc
@@ -73,6 +73,8 @@
 
   ui::MenuModel* GetSubmenuModelAt(int index) const override { return nullptr; }
 
+  void HighlightChangedTo(int index) override {}
+
   void ActivatedAt(int index) override {}
 
   void SetMenuModelDelegate(ui::MenuModelDelegate* delegate) override {}
diff --git a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
index 29f9919..e268ea4 100644
--- a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
+++ b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
@@ -92,8 +92,11 @@
 class BrowserTabStripController::TabContextMenuContents
     : public ui::SimpleMenuModel::Delegate {
  public:
-  TabContextMenuContents(Tab* tab, BrowserTabStripController* controller)
-      : tab_(tab), controller_(controller) {
+  TabContextMenuContents(Tab* tab,
+                         BrowserTabStripController* controller)
+      : tab_(tab),
+        controller_(controller),
+        last_command_(TabStripModel::CommandFirst) {
     model_.reset(new TabMenuModel(
         this, controller->model_,
         controller->tabstrip_->GetModelIndexOfTab(tab)));
@@ -102,6 +105,11 @@
         views::MenuRunner::HAS_MNEMONICS | views::MenuRunner::CONTEXT_MENU));
   }
 
+  ~TabContextMenuContents() override {
+    if (controller_)
+      controller_->tabstrip_->StopAllHighlighting();
+  }
+
   void Cancel() {
     controller_ = NULL;
   }
@@ -128,14 +136,25 @@
                                                             accelerator) :
         false;
   }
+  void CommandIdHighlighted(int command_id) override {
+    controller_->StopHighlightTabsForCommand(last_command_, tab_);
+    last_command_ = static_cast<TabStripModel::ContextMenuCommand>(command_id);
+    controller_->StartHighlightTabsForCommand(last_command_, tab_);
+  }
   void ExecuteCommand(int command_id, int event_flags) override {
     // Executing the command destroys |this|, and can also end up destroying
     // |controller_|. So stop the highlights before executing the command.
+    controller_->tabstrip_->StopAllHighlighting();
     controller_->ExecuteCommandForTab(
         static_cast<TabStripModel::ContextMenuCommand>(command_id),
         tab_);
   }
 
+  void MenuClosed(ui::SimpleMenuModel* /*source*/) override {
+    if (controller_)
+      controller_->tabstrip_->StopAllHighlighting();
+  }
+
  private:
   std::unique_ptr<TabMenuModel> model_;
   std::unique_ptr<views::MenuRunner> menu_runner_;
@@ -146,6 +165,10 @@
   // A pointer back to our hosting controller, for command state information.
   BrowserTabStripController* controller_;
 
+  // The last command that was selected, so that we can start/stop highlighting
+  // appropriately as the user moves through the menu.
+  TabStripModel::ContextMenuCommand last_command_;
+
   DISALLOW_COPY_AND_ASSIGN(TabContextMenuContents);
 };
 
@@ -584,6 +607,33 @@
       TabRendererDataFromModel(web_contents, model_index, EXISTING_TAB));
 }
 
+void BrowserTabStripController::StartHighlightTabsForCommand(
+    TabStripModel::ContextMenuCommand command_id,
+    Tab* tab) {
+  if (command_id == TabStripModel::CommandCloseOtherTabs ||
+      command_id == TabStripModel::CommandCloseTabsToRight) {
+    int model_index = tabstrip_->GetModelIndexOfTab(tab);
+    if (IsValidIndex(model_index)) {
+      std::vector<int> indices =
+          model_->GetIndicesClosedByCommand(model_index, command_id);
+      for (std::vector<int>::const_iterator i(indices.begin());
+           i != indices.end(); ++i) {
+        tabstrip_->StartHighlight(*i);
+      }
+    }
+  }
+}
+
+void BrowserTabStripController::StopHighlightTabsForCommand(
+    TabStripModel::ContextMenuCommand command_id,
+    Tab* tab) {
+  if (command_id == TabStripModel::CommandCloseTabsToRight ||
+      command_id == TabStripModel::CommandCloseOtherTabs) {
+    // Just tell all Tabs to stop pulsing - it's safe.
+    tabstrip_->StopAllHighlighting();
+  }
+}
+
 void BrowserTabStripController::AddTab(WebContents* contents,
                                        int index,
                                        bool is_active) {
diff --git a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.h b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.h
index 9dfcf326..d72fb97 100644
--- a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.h
+++ b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.h
@@ -126,6 +126,13 @@
   // Invokes tabstrip_->SetTabData.
   void SetTabDataAt(content::WebContents* web_contents, int model_index);
 
+  void StartHighlightTabsForCommand(
+      TabStripModel::ContextMenuCommand command_id,
+      Tab* tab);
+  void StopHighlightTabsForCommand(
+      TabStripModel::ContextMenuCommand command_id,
+      Tab* tab);
+
   // Adds a tab.
   void AddTab(content::WebContents* contents, int index, bool is_active);
 
diff --git a/chrome/browser/ui/views/tabs/tab.cc b/chrome/browser/ui/views/tabs/tab.cc
index 7efd7a00..46554c2 100644
--- a/chrome/browser/ui/views/tabs/tab.cc
+++ b/chrome/browser/ui/views/tabs/tab.cc
@@ -55,6 +55,7 @@
 #include "ui/base/resource/resource_bundle.h"
 #include "ui/base/theme_provider.h"
 #include "ui/compositor/clip_recorder.h"
+#include "ui/gfx/animation/animation_container.h"
 #include "ui/gfx/animation/tween.h"
 #include "ui/gfx/canvas.h"
 #include "ui/gfx/color_analysis.h"
@@ -118,8 +119,10 @@
 // static
 const char Tab::kViewClassName[] = "Tab";
 
-Tab::Tab(TabController* controller)
+Tab::Tab(TabController* controller, gfx::AnimationContainer* container)
     : controller_(controller),
+      pulse_animation_(this),
+      animation_container_(container),
       title_(new views::Label()),
       title_animation_(this),
       hover_controller_(this) {
@@ -161,7 +164,14 @@
 
   set_context_menu_controller(this);
 
+  constexpr int kPulseDurationMs = 200;
+  pulse_animation_.SetSlideDuration(kPulseDurationMs);
+  pulse_animation_.SetContainer(animation_container_.get());
+
   title_animation_.SetDuration(base::TimeDelta::FromMilliseconds(100));
+  title_animation_.SetContainer(animation_container_.get());
+
+  hover_controller_.SetAnimationContainer(animation_container_.get());
 
   // Enable keyboard focus.
   SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
@@ -186,6 +196,11 @@
     return;
   }
 
+  // Ignore if the pulse animation is being performed on active tab because
+  // it repaints the same image. See PaintTab().
+  if (animation == &pulse_animation_ && IsActive())
+    return;
+
   SchedulePaint();
 }
 
@@ -738,6 +753,14 @@
   return icon_->ShowingLoadingAnimation();
 }
 
+void Tab::StartPulse() {
+  pulse_animation_.StartThrobbing(std::numeric_limits<int>::max());
+}
+
+void Tab::StopPulse() {
+  pulse_animation_.Stop();
+}
+
 void Tab::SetTabNeedsAttention(bool attention) {
   icon_->SetAttention(TabIcon::AttentionType::kTabWantsAttentionStatus,
                       attention);
@@ -825,7 +848,9 @@
     return is_selected ? (kSelectedTabThrobScale * opacity) : opacity;
   };
 
-  if (hover_controller_.ShouldDraw())
+  if (pulse_animation_.is_animating())
+    val += pulse_animation_.GetCurrentValue() * offset();
+  else if (hover_controller_.ShouldDraw())
     val += hover_controller_.GetAnimationValue() * offset();
 
   return val;
diff --git a/chrome/browser/ui/views/tabs/tab.h b/chrome/browser/ui/views/tabs/tab.h
index 1ceaec8..a9e3d7c 100644
--- a/chrome/browser/ui/views/tabs/tab.h
+++ b/chrome/browser/ui/views/tabs/tab.h
@@ -33,7 +33,9 @@
 
 namespace gfx {
 class Animation;
+class AnimationContainer;
 class LinearAnimation;
+class ThrobAnimation;
 }
 namespace views {
 class Label;
@@ -59,7 +61,7 @@
   static constexpr int kMinimumContentsWidthForCloseButtons = 68;
   static constexpr int kTouchMinimumContentsWidthForCloseButtons = 100;
 
-  explicit Tab(TabController* controller);
+  Tab(TabController* controller, gfx::AnimationContainer* container);
   ~Tab() override;
 
   // gfx::AnimationDelegate:
@@ -151,6 +153,10 @@
 
   bool ShowingLoadingAnimation() const;
 
+  // Starts/Stops a pulse animation.
+  void StartPulse();
+  void StopPulse();
+
   // Sets the visibility of the indicator shown when the tab needs to indicate
   // to the user that it needs their attention.
   void SetTabNeedsAttention(bool attention);
@@ -239,6 +245,11 @@
   // True if the tab has been detached.
   bool detached_ = false;
 
+  // Whole-tab throbbing "pulse" animation.
+  gfx::ThrobAnimation pulse_animation_;
+
+  scoped_refptr<gfx::AnimationContainer> animation_container_;
+
   TabIcon* icon_ = nullptr;
   AlertIndicator* alert_indicator_ = nullptr;
   TabCloseButton* close_button_ = nullptr;
diff --git a/chrome/browser/ui/views/tabs/tab_strip.cc b/chrome/browser/ui/views/tabs/tab_strip.cc
index 1c6cc0e..07b33cc 100644
--- a/chrome/browser/ui/views/tabs/tab_strip.cc
+++ b/chrome/browser/ui/views/tabs/tab_strip.cc
@@ -56,6 +56,7 @@
 #include "ui/base/resource/resource_bundle.h"
 #include "ui/display/display.h"
 #include "ui/display/screen.h"
+#include "ui/gfx/animation/animation_container.h"
 #include "ui/gfx/animation/throb_animation.h"
 #include "ui/gfx/animation/tween.h"
 #include "ui/gfx/canvas.h"
@@ -418,6 +419,15 @@
     tab_at(i)->Layout();
 }
 
+void TabStrip::StartHighlight(int model_index) {
+  tab_at(model_index)->StartPulse();
+}
+
+void TabStrip::StopAllHighlighting() {
+  for (int i = 0; i < tab_count(); ++i)
+    tab_at(i)->StopPulse();
+}
+
 void TabStrip::AddTabAt(int model_index, TabRendererData data, bool is_active) {
   const bool was_single_tab_mode = SingleTabMode();
 
@@ -427,7 +437,7 @@
     view_index = GetIndexOf(tab_at(model_index - 1)) + 1;
   }
 
-  Tab* tab = new Tab(this);
+  Tab* tab = new Tab(this, animation_container_.get());
   AddChildViewAt(tab, view_index);
   const bool pinned = data.pinned;
   tab->SetData(std::move(data));
diff --git a/chrome/browser/ui/views/tabs/tab_strip.h b/chrome/browser/ui/views/tabs/tab_strip.h
index ae80855..d7ec646c 100644
--- a/chrome/browser/ui/views/tabs/tab_strip.h
+++ b/chrome/browser/ui/views/tabs/tab_strip.h
@@ -23,6 +23,7 @@
 #include "chrome/browser/ui/views/tabs/tab_controller.h"
 #include "chrome/browser/ui/views/tabs/tab_strip.h"
 #include "ui/base/material_design/material_design_controller_observer.h"
+#include "ui/gfx/animation/animation_container.h"
 #include "ui/gfx/color_palette.h"
 #include "ui/gfx/geometry/point.h"
 #include "ui/gfx/geometry/rect.h"
@@ -139,6 +140,12 @@
   // Returns the bounds of the new tab button.
   gfx::Rect new_tab_button_bounds() const { return new_tab_button_bounds_; }
 
+  // Starts highlighting the tab at the specified index.
+  void StartHighlight(int model_index);
+
+  // Stops all tab higlighting.
+  void StopAllHighlighting();
+
   // Adds a tab at the specified index.
   void AddTabAt(int model_index, TabRendererData data, bool is_active);
 
@@ -676,6 +683,11 @@
   // Valid for the lifetime of a drag over us.
   std::unique_ptr<DropArrow> drop_arrow_;
 
+  // To ensure all tabs pulse at the same time they share the same animation
+  // container. This is that animation container.
+  scoped_refptr<gfx::AnimationContainer> animation_container_{
+      new gfx::AnimationContainer()};
+
   // MouseWatcher is used for two things:
   // . When a tab is closed to reset the layout.
   // . When a mouse is used and the layout dynamically adjusts and is currently
diff --git a/chrome/browser/ui/views/tabs/tab_unittest.cc b/chrome/browser/ui/views/tabs/tab_unittest.cc
index 449445d2..211f507d 100644
--- a/chrome/browser/ui/views/tabs/tab_unittest.cc
+++ b/chrome/browser/ui/views/tabs/tab_unittest.cc
@@ -412,7 +412,7 @@
   InitWidget(&widget);
 
   FakeTabController tab_controller;
-  Tab tab(&tab_controller);
+  Tab tab(&tab_controller, nullptr);
   widget.GetContentsView()->AddChildView(&tab);
   tab.SizeToPreferredSize();
 
@@ -446,7 +446,7 @@
   InitWidget(&widget);
 
   FakeTabController controller;
-  Tab tab(&controller);
+  Tab tab(&controller, nullptr);
   widget.GetContentsView()->AddChildView(&tab);
 
   SkBitmap bitmap;
@@ -498,7 +498,7 @@
   InitWidget(&widget);
 
   FakeTabController controller;
-  Tab tab(&controller);
+  Tab tab(&controller, nullptr);
   widget.GetContentsView()->AddChildView(&tab);
   tab.SizeToPreferredSize();
 
@@ -548,7 +548,7 @@
 // shouldn't change the insets of the close button.
 TEST_F(TabTest, CloseButtonLayout) {
   FakeTabController tab_controller;
-  Tab tab(&tab_controller);
+  Tab tab(&tab_controller, nullptr);
   tab.SetBounds(0, 0, 100, 50);
   LayoutTab(&tab);
   gfx::Insets close_button_insets = GetCloseButton(tab)->GetInsets();
@@ -569,7 +569,7 @@
   Widget widget;
   InitWidget(&widget);
   FakeTabController tab_controller;
-  Tab tab(&tab_controller);
+  Tab tab(&tab_controller, nullptr);
   widget.GetContentsView()->AddChildView(&tab);
 
   views::ImageButton* tab_close_button = GetCloseButton(tab);
@@ -590,7 +590,7 @@
   InitWidget(&widget);
 
   FakeTabController tab_controller;
-  Tab tab(&tab_controller);
+  Tab tab(&tab_controller, nullptr);
   widget.GetContentsView()->AddChildView(&tab);
   tab.SizeToPreferredSize();
 
@@ -701,7 +701,7 @@
 
 TEST_F(TabTest, TitleHiddenWhenSmall) {
   FakeTabController tab_controller;
-  Tab tab(&tab_controller);
+  Tab tab(&tab_controller, nullptr);
   tab.SetBounds(0, 0, 100, 50);
   EXPECT_GT(GetTitleWidth(tab), 0);
   tab.SetBounds(0, 0, 0, 50);
@@ -715,7 +715,7 @@
   for (bool is_active_tab : {false, true}) {
     FakeTabController controller;
     controller.set_active_tab(is_active_tab);
-    Tab tab(&controller);
+    Tab tab(&controller, nullptr);
     widget.GetContentsView()->AddChildView(&tab);
     tab.SizeToPreferredSize();
 
@@ -734,7 +734,7 @@
 
   FakeTabController controller;
   controller.set_active_tab(false);
-  Tab tab(&controller);
+  Tab tab(&controller, nullptr);
   widget.GetContentsView()->AddChildView(&tab);
   const int width = tab.tab_style()->GetContentsInsets().width() +
                     Tab::kMinimumContentsWidthForCloseButtons;
@@ -757,7 +757,7 @@
 
   FakeTabController controller;
   controller.set_active_tab(true);
-  Tab tab(&controller);
+  Tab tab(&controller, nullptr);
   widget.GetContentsView()->AddChildView(&tab);
   tab.SetBounds(0, 0, 200, 50);
   const views::View* close = GetCloseButton(tab);
@@ -777,7 +777,7 @@
   InitWidget(&widget);
 
   FakeTabController controller;
-  Tab tab(&controller);
+  Tab tab(&controller, nullptr);
   widget.GetContentsView()->AddChildView(&tab);
 
   tab.SizeToPreferredSize();
@@ -800,7 +800,7 @@
 
   FakeTabController controller;
   controller.set_active_tab(true);
-  Tab tab(&controller);
+  Tab tab(&controller, nullptr);
   widget.GetContentsView()->AddChildView(&tab);
   TabRendererData data;
   data.alert_state = TabAlertState::AUDIO_PLAYING;
@@ -845,7 +845,7 @@
   Widget widget;
   InitWidget(&widget);
   FakeTabController controller;
-  Tab tab(&controller);
+  Tab tab(&controller, nullptr);
   widget.GetContentsView()->AddChildView(&tab);
 
   for (const auto& colors : color_schemes) {
diff --git a/chrome/browser/ui/views/toolbar/toolbar_button_views_unittest.cc b/chrome/browser/ui/views/toolbar/toolbar_button_views_unittest.cc
index 11607fb..2b5f47e 100644
--- a/chrome/browser/ui/views/toolbar/toolbar_button_views_unittest.cc
+++ b/chrome/browser/ui/views/toolbar/toolbar_button_views_unittest.cc
@@ -51,6 +51,7 @@
   }
   bool IsEnabledAt(int index) const override { return false; }
   ui::MenuModel* GetSubmenuModelAt(int index) const override { return nullptr; }
+  void HighlightChangedTo(int index) override {}
   void ActivatedAt(int index) override {}
   void SetMenuModelDelegate(ui::MenuModelDelegate* delegate) override {}
   ui::MenuModelDelegate* GetMenuModelDelegate() const override {
diff --git a/ui/base/models/menu_model.h b/ui/base/models/menu_model.h
index bb53c04..fb958b18 100644
--- a/ui/base/models/menu_model.h
+++ b/ui/base/models/menu_model.h
@@ -112,6 +112,10 @@
   // Returns the model for the submenu at the specified index.
   virtual MenuModel* GetSubmenuModelAt(int index) const = 0;
 
+  // Called when the highlighted menu item changes to the item at the specified
+  // index.
+  virtual void HighlightChangedTo(int index) = 0;
+
   // Called when the item at the specified index has been activated.
   virtual void ActivatedAt(int index) = 0;
 
diff --git a/ui/base/models/simple_menu_model.cc b/ui/base/models/simple_menu_model.cc
index b9fbad7..4aa19f6 100644
--- a/ui/base/models/simple_menu_model.cc
+++ b/ui/base/models/simple_menu_model.cc
@@ -50,6 +50,9 @@
   return false;
 }
 
+void SimpleMenuModel::Delegate::CommandIdHighlighted(int command_id) {
+}
+
 void SimpleMenuModel::Delegate::OnMenuWillShow(SimpleMenuModel* /*source*/) {}
 
 void SimpleMenuModel::Delegate::MenuClosed(SimpleMenuModel* /*source*/) {
@@ -419,6 +422,11 @@
          items_[ValidateItemIndex(index)].visible;
 }
 
+void SimpleMenuModel::HighlightChangedTo(int index) {
+  if (delegate_)
+    delegate_->CommandIdHighlighted(GetCommandIdAt(index));
+}
+
 void SimpleMenuModel::ActivatedAt(int index) {
   ActivatedAt(index, 0);
 }
diff --git a/ui/base/models/simple_menu_model.h b/ui/base/models/simple_menu_model.h
index b9f7b9b..ac1e1a7 100644
--- a/ui/base/models/simple_menu_model.h
+++ b/ui/base/models/simple_menu_model.h
@@ -46,6 +46,10 @@
     virtual bool GetIconForCommandId(int command_id,
                                      gfx::Image* icon) const;
 
+    // Notifies the delegate that the item with the specified command id was
+    // visually highlighted within the menu.
+    virtual void CommandIdHighlighted(int command_id);
+
     // Performs the action associates with the specified command id.
     // The passed |event_flags| are the flags from the event which issued this
     // command and they can be examined to find modifier keys.
@@ -181,6 +185,7 @@
   ui::ButtonMenuItemModel* GetButtonMenuItemAt(int index) const override;
   bool IsEnabledAt(int index) const override;
   bool IsVisibleAt(int index) const override;
+  void HighlightChangedTo(int index) override;
   void ActivatedAt(int index) override;
   void ActivatedAt(int index, int event_flags) override;
   MenuModel* GetSubmenuModelAt(int index) const override;
diff --git a/ui/views/controls/combobox/combobox.cc b/ui/views/controls/combobox/combobox.cc
index 7863d712..62a28d66 100644
--- a/ui/views/controls/combobox/combobox.cc
+++ b/ui/views/controls/combobox/combobox.cc
@@ -185,6 +185,8 @@
     return model_->IsItemEnabledAt(index);
   }
 
+  void HighlightChangedTo(int index) override {}
+
   void ActivatedAt(int index) override {
     owner_->selected_index_ = index;
     owner_->OnPerformAction();
diff --git a/ui/views/controls/menu/menu_controller.cc b/ui/views/controls/menu/menu_controller.cc
index d813c85..8751786 100644
--- a/ui/views/controls/menu/menu_controller.cc
+++ b/ui/views/controls/menu/menu_controller.cc
@@ -1249,6 +1249,9 @@
         (selection_types & SELECTION_OPEN_SUBMENU) != 0);
   }
 
+  if (menu_item && menu_item->GetDelegate())
+    menu_item->GetDelegate()->SelectionChanged(menu_item);
+
   DCHECK(menu_item || (selection_types & SELECTION_EXIT) != 0);
 
   pending_state_.item = menu_item;
diff --git a/ui/views/controls/menu/menu_delegate.h b/ui/views/controls/menu/menu_delegate.h
index 234ef7e..69a8ed5 100644
--- a/ui/views/controls/menu/menu_delegate.h
+++ b/ui/views/controls/menu/menu_delegate.h
@@ -194,6 +194,10 @@
   // Views that are not MenuItemViews.
   virtual bool ShouldCloseOnDragComplete();
 
+  // Notification that the user has highlighted the specified item.
+  virtual void SelectionChanged(MenuItemView* menu) {
+  }
+
   // Notification the menu has closed. This will not be called if MenuRunner is
   // deleted during calls to ExecuteCommand().
   virtual void OnMenuClosed(MenuItemView* menu) {}
diff --git a/ui/views/controls/menu/menu_model_adapter.cc b/ui/views/controls/menu/menu_model_adapter.cc
index c91415cb..08132c52 100644
--- a/ui/views/controls/menu/menu_model_adapter.cc
+++ b/ui/views/controls/menu/menu_model_adapter.cc
@@ -220,6 +220,22 @@
   return false;
 }
 
+void MenuModelAdapter::SelectionChanged(MenuItemView* menu) {
+  // Ignore selection of the root menu.
+  if (menu == menu->GetRootMenuItem())
+    return;
+
+  const int id = menu->GetCommand();
+  ui::MenuModel* model = menu_model_;
+  int index = 0;
+  if (ui::MenuModel::GetModelAndIndexForCommandId(id, &model, &index)) {
+    model->HighlightChangedTo(index);
+    return;
+  }
+
+  NOTREACHED();
+}
+
 void MenuModelAdapter::WillShowMenu(MenuItemView* menu) {
   // Look up the menu model for this menu.
   const std::map<MenuItemView*, ui::MenuModel*>::const_iterator map_iterator =
diff --git a/ui/views/controls/menu/menu_model_adapter.h b/ui/views/controls/menu/menu_model_adapter.h
index a3598719..e52edfe 100644
--- a/ui/views/controls/menu/menu_model_adapter.h
+++ b/ui/views/controls/menu/menu_model_adapter.h
@@ -75,6 +75,7 @@
   bool IsCommandEnabled(int id) const override;
   bool IsCommandVisible(int id) const override;
   bool IsItemChecked(int id) const override;
+  void SelectionChanged(MenuItemView* menu) override;
   void WillShowMenu(MenuItemView* menu) override;
   void WillHideMenu(MenuItemView* menu) override;
   void OnMenuClosed(MenuItemView* menu) override;
diff --git a/ui/views/controls/menu/menu_model_adapter_unittest.cc b/ui/views/controls/menu/menu_model_adapter_unittest.cc
index 2d95b3cd..392f6c3e 100644
--- a/ui/views/controls/menu/menu_model_adapter_unittest.cc
+++ b/ui/views/controls/menu/menu_model_adapter_unittest.cc
@@ -82,6 +82,8 @@
     return items_[index].submenu;
   }
 
+  void HighlightChangedTo(int index) override {}
+
   void ActivatedAt(int index) override { set_last_activation(index); }
 
   void ActivatedAt(int index, int event_flags) override { ActivatedAt(index); }
@@ -339,6 +341,11 @@
   const int actionable_submenu_index = 5;
   CheckSubmenu(model, menu, &delegate, kRootIdBase + actionable_submenu_index,
                2, actionable_submenu_index, kActionableSubmenuIdBase);
+
+  // Check that selecting the root item is safe.  The MenuModel does
+  // not care about the root so MenuModelAdapter should do nothing
+  // (not hit the NOTREACHED check) when the root is selected.
+  static_cast<views::MenuDelegate*>(&delegate)->SelectionChanged(menu);
 }
 
 }  // namespace views