1 // Copyright 2014 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/toolbar/toolbar_actions_bar.h"
6 
7 #include <algorithm>
8 #include <set>
9 #include <string>
10 #include <utility>
11 
12 #include "base/auto_reset.h"
13 #include "base/bind.h"
14 #include "base/location.h"
15 #include "base/numerics/ranges.h"
16 #include "base/single_thread_task_runner.h"
17 #include "base/threading/thread_task_runner_handle.h"
18 #include "base/time/time.h"
19 #include "chrome/browser/extensions/extension_message_bubble_controller.h"
20 #include "chrome/browser/profiles/profile.h"
21 #include "chrome/browser/ui/browser.h"
22 #include "chrome/browser/ui/browser_window.h"
23 #include "chrome/browser/ui/extensions/extension_action_view_controller.h"
24 #include "chrome/browser/ui/extensions/extension_message_bubble_bridge.h"
25 #include "chrome/browser/ui/extensions/settings_api_bubble_helpers.h"
26 #include "chrome/browser/ui/layout_constants.h"
27 #include "chrome/browser/ui/tabs/tab_strip_model.h"
28 #include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
29 #include "chrome/browser/ui/toolbar/toolbar_actions_bar_delegate.h"
30 #include "chrome/browser/ui/toolbar/toolbar_actions_bar_observer.h"
31 #include "chrome/browser/ui/ui_features.h"
32 #include "chrome/common/pref_names.h"
33 #include "components/crx_file/id_util.h"
34 #include "components/pref_registry/pref_registry_syncable.h"
35 #include "extensions/browser/extension_system.h"
36 #include "extensions/browser/extension_util.h"
37 #include "extensions/browser/runtime_data.h"
38 #include "extensions/common/extension.h"
39 #include "ui/base/resource/resource_bundle.h"
40 #include "ui/base/ui_base_features.h"
41 #include "ui/gfx/image/image_skia.h"
42 
43 namespace {
44 
45 using WeakToolbarActions = std::vector<ToolbarActionViewController*>;
46 
47 enum DimensionType { WIDTH, HEIGHT };
48 
49 // Takes a reference vector |reference| of length n, where n is less than or
50 // equal to the length of |to_sort|, and rearranges |to_sort| so that
51 // |to_sort|'s first n elements match the n elements of |reference| (the order
52 // of any remaining elements in |to_sort| is unspecified).
53 // |equal| is used to compare the elements of |to_sort| and |reference|.
54 // This allows us to sort a vector to match another vector of two different
55 // types without needing to construct a more cumbersome comparator class.
56 // |FunctionType| should equate to (something similar to)
57 // bool Equal(const Type1&, const Type2&), but we can't enforce this
58 // because of MSVC compilation limitations.
59 template <typename Type1, typename Type2, typename FunctionType>
SortContainer(std::vector<std::unique_ptr<Type1>> * to_sort,const std::vector<Type2> & reference,FunctionType equal)60 void SortContainer(std::vector<std::unique_ptr<Type1>>* to_sort,
61                    const std::vector<Type2>& reference,
62                    FunctionType equal) {
63   CHECK_GE(to_sort->size(), reference.size())
64       << "|to_sort| must contain all elements in |reference|.";
65   if (reference.empty())
66     return;
67   // Run through the each element and compare it to the reference. If something
68   // is out of place, find the correct spot for it.
69   for (size_t i = 0; i < reference.size() - 1; ++i) {
70     if (!equal(to_sort->at(i).get(), reference[i])) {
71       // Find the correct index (it's guaranteed to be after our current
72       // index, since everything up to this point is correct), and swap.
73       size_t j = i + 1;
74       while (!equal(to_sort->at(j).get(), reference[i])) {
75         ++j;
76         DCHECK_LT(j, to_sort->size())
77             << "Item in |reference| not found in |to_sort|.";
78       }
79       std::swap(to_sort->at(i), to_sort->at(j));
80     }
81   }
82 }
83 
84 // How long to wait until showing an extension message bubble.
85 int g_extension_bubble_appearance_wait_time_in_seconds = 5;
86 
87 }  // namespace
88 
89 // static
90 bool ToolbarActionsBar::disable_animations_for_testing_ = false;
91 
PlatformSettings()92 ToolbarActionsBar::PlatformSettings::PlatformSettings()
93     : item_spacing(GetLayoutConstant(TOOLBAR_STANDARD_SPACING)),
94       icons_per_overflow_menu_row(1) {}
95 
ToolbarActionsBar(ToolbarActionsBarDelegate * delegate,Browser * browser,ToolbarActionsBar * main_bar)96 ToolbarActionsBar::ToolbarActionsBar(ToolbarActionsBarDelegate* delegate,
97                                      Browser* browser,
98                                      ToolbarActionsBar* main_bar)
99     : delegate_(delegate),
100       browser_(browser),
101       model_(ToolbarActionsModel::Get(browser_->profile())),
102       main_bar_(main_bar),
103       platform_settings_(),
104       popup_owner_(nullptr),
105       model_observer_(this),
106       suppress_layout_(false),
107       suppress_animation_(true),
108       should_check_extension_bubble_(!main_bar),
109       popped_out_action_(nullptr),
110       is_popped_out_sticky_(false),
111       is_showing_bubble_(false) {
112   if (model_)  // |model_| can be null in unittests.
113     model_observer_.Add(model_);
114 
115   DCHECK(!base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu));
116 
117   browser_->tab_strip_model()->AddObserver(this);
118 }
119 
~ToolbarActionsBar()120 ToolbarActionsBar::~ToolbarActionsBar() {
121   // We don't just call DeleteActions() here because it makes assumptions about
122   // the order of deletion between the views and the ToolbarActionsBar.
123   DCHECK(toolbar_actions_.empty())
124       << "Must call DeleteActions() before destruction.";
125 
126   // Make sure we don't listen to any more model changes during
127   // ToolbarActionsBar destruction.
128   model_observer_.RemoveAll();
129 
130   for (ToolbarActionsBarObserver& observer : observers_)
131     observer.OnToolbarActionsBarDestroyed();
132 }
133 
134 // static
FromBrowserWindow(BrowserWindow * window)135 ToolbarActionsBar* ToolbarActionsBar::FromBrowserWindow(BrowserWindow* window) {
136   DCHECK(!base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu));
137   // The ToolbarActionsBar is the only implementation of the ExtensionsContainer
138   // if the ExtensionsMenu feature is disabled.
139   return static_cast<ToolbarActionsBar*>(window->GetExtensionsContainer());
140 }
141 
142 // static
RegisterProfilePrefs(user_prefs::PrefRegistrySyncable * registry)143 void ToolbarActionsBar::RegisterProfilePrefs(
144     user_prefs::PrefRegistrySyncable* registry) {
145   registry->RegisterBooleanPref(
146       prefs::kToolbarIconSurfacingBubbleAcknowledged, false,
147       user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
148   registry->RegisterInt64Pref(prefs::kToolbarIconSurfacingBubbleLastShowTime,
149                               0);
150 }
151 
152 // static
GetIconAreaSize()153 gfx::Size ToolbarActionsBar::GetIconAreaSize() {
154   return gfx::Size(28, 28);
155 }
156 
GetViewSize() const157 gfx::Size ToolbarActionsBar::GetViewSize() const {
158   gfx::Rect rect(GetIconAreaSize());
159   rect.Inset(-GetIconAreaInsets());
160   return rect.size();
161 }
162 
GetFullSize() const163 gfx::Size ToolbarActionsBar::GetFullSize() const {
164   // If there are no actions to show (and this isn't an overflow container),
165   // then don't show the container at all.
166   if (toolbar_actions_.empty() && !in_overflow_mode())
167     return gfx::Size();
168 
169   int num_icons = GetIconCount();
170   int num_rows = 1;
171 
172   if (in_overflow_mode()) {
173     // In overflow, we always have a preferred size of a full row (even if we
174     // don't use it), and always of at least one row. The parent may decide to
175     // show us even when empty, e.g. as a drag target for dragging in icons from
176     // the main container.
177     num_icons = platform_settings_.icons_per_overflow_menu_row;
178     const int icon_count = GetEndIndexInBounds() - GetStartIndexInBounds();
179     num_rows += (std::max(0, icon_count - 1) / num_icons);
180   }
181 
182   return gfx::Size(IconCountToWidth(num_icons), IconCountToWidth(num_rows));
183 }
184 
GetMinimumWidth() const185 int ToolbarActionsBar::GetMinimumWidth() const {
186   return platform_settings_.item_spacing;
187 }
188 
GetMaximumWidth() const189 int ToolbarActionsBar::GetMaximumWidth() const {
190   return IconCountToWidth(toolbar_actions_.size());
191 }
192 
IconCountToWidth(size_t icons) const193 int ToolbarActionsBar::IconCountToWidth(size_t icons) const {
194   if (icons == 0)
195     return 0;
196   return icons * GetViewSize().width() +
197          (icons - 1) * GetLayoutConstant(TOOLBAR_ELEMENT_PADDING);
198 }
199 
WidthToIconCountUnclamped(int pixels) const200 size_t ToolbarActionsBar::WidthToIconCountUnclamped(int pixels) const {
201   const int element_padding = GetLayoutConstant(TOOLBAR_ELEMENT_PADDING);
202   return std::max(
203       (pixels + element_padding) / (GetViewSize().width() + element_padding),
204       0);
205 }
206 
WidthToIconCount(int pixels) const207 size_t ToolbarActionsBar::WidthToIconCount(int pixels) const {
208   return std::min(WidthToIconCountUnclamped(pixels), toolbar_actions_.size());
209 }
210 
GetIconCount() const211 size_t ToolbarActionsBar::GetIconCount() const {
212   if (!model_)
213     return 0;
214 
215   int pop_out_modifier = 0;
216   // If there is a popped out action, it could affect the number of visible
217   // icons - but only if it wouldn't otherwise be visible.
218   if (popped_out_action_) {
219     size_t popped_out_index = 0;
220     for (; popped_out_index < toolbar_actions_.size(); ++popped_out_index) {
221       if (toolbar_actions_[popped_out_index].get() == popped_out_action_)
222         break;
223     }
224 
225     pop_out_modifier = popped_out_index >= model_->visible_icon_count() ? 1 : 0;
226   }
227 
228   // We purposefully do not account for any "popped out" actions in overflow
229   // mode. This is because the popup cannot be showing while the overflow menu
230   // is open, so there's no concern there. Also, if the user has a popped out
231   // action, and immediately opens the overflow menu, we *want* the action there
232   // (since it will close the popup, but do so asynchronously, and we don't
233   // want to "slide" the action back in.
234   size_t visible_icons =
235       in_overflow_mode()
236           ? toolbar_actions_.size() - model_->visible_icon_count()
237           : model_->visible_icon_count() + pop_out_modifier;
238 
239 #if DCHECK_IS_ON()
240   // Good time for some sanity checks: We should never try to display more
241   // icons than we have, and we should always have a view per item in the model.
242   // (The only exception is if this is in initialization.)
243   if (!toolbar_actions_.empty() && !suppress_layout_ &&
244       model_->actions_initialized()) {
245     DCHECK_LE(visible_icons, toolbar_actions_.size());
246     DCHECK_EQ(model_->action_ids().size(), toolbar_actions_.size());
247   }
248 #endif
249 
250   return visible_icons;
251 }
252 
GetStartIndexInBounds() const253 size_t ToolbarActionsBar::GetStartIndexInBounds() const {
254   return in_overflow_mode() ? main_bar_->GetEndIndexInBounds() : 0;
255 }
256 
GetEndIndexInBounds() const257 size_t ToolbarActionsBar::GetEndIndexInBounds() const {
258   // The end index for the main bar is however many icons can fit with the given
259   // width. We take the width-after-animation here so that we don't have to
260   // constantly adjust both this and the overflow as the size changes (the
261   // animations are small and fast enough that this doesn't cause problems).
262   return in_overflow_mode()
263              ? toolbar_actions_.size()
264              : WidthToIconCount(delegate_->GetWidth(
265                    ToolbarActionsBarDelegate::GET_WIDTH_AFTER_ANIMATION));
266 }
267 
NeedsOverflow() const268 bool ToolbarActionsBar::NeedsOverflow() const {
269   DCHECK(!in_overflow_mode());
270   // We need an overflow view if either the end index is less than the number of
271   // icons, if a drag is in progress with the redesign turned on (since the
272   // user can drag an icon into the app menu), or if there is a non-sticky
273   // popped out action (because the action will pop back into overflow when the
274   // menu opens).
275   return GetEndIndexInBounds() != toolbar_actions_.size() ||
276          is_drag_in_progress() ||
277          (popped_out_action_ && !is_popped_out_sticky_);
278 }
279 
GetFrameForIndex(size_t index) const280 gfx::Rect ToolbarActionsBar::GetFrameForIndex(size_t index) const {
281   size_t start_index = GetStartIndexInBounds();
282 
283   // If the index is for an action that is before range we show (i.e., is for
284   // a button that's on the main bar, and this is the overflow), send back an
285   // empty rect.
286   if (index < start_index)
287     return gfx::Rect();
288 
289   const size_t relative_index = index - start_index;
290   const int icons_per_overflow_row =
291       platform_settings().icons_per_overflow_menu_row;
292   const size_t row_index =
293       in_overflow_mode() ? relative_index / icons_per_overflow_row : 0;
294   const size_t index_in_row = in_overflow_mode()
295                                   ? relative_index % icons_per_overflow_row
296                                   : relative_index;
297 
298   const auto size = GetViewSize();
299   const int element_padding = GetLayoutConstant(TOOLBAR_ELEMENT_PADDING);
300   return gfx::Rect(gfx::Point(index_in_row * (size.width() + element_padding),
301                               row_index * (size.height() + element_padding)),
302                    size);
303 }
304 
GetActions() const305 std::vector<ToolbarActionViewController*> ToolbarActionsBar::GetActions()
306     const {
307   std::vector<ToolbarActionViewController*> actions;
308   for (const auto& action : toolbar_actions_)
309     actions.push_back(action.get());
310 
311   // If there is an action that should be popped out, and it's not visible by
312   // default, make it the final action in the list.
313   if (popped_out_action_) {
314     size_t index =
315         std::find(actions.begin(), actions.end(), popped_out_action_) -
316         actions.begin();
317     DCHECK_NE(actions.size(), index);
318     size_t visible = GetIconCount();
319     if (index >= visible) {
320       size_t rindex = actions.size() - index - 1;
321       std::rotate(actions.rbegin() + rindex, actions.rbegin() + rindex + 1,
322                   actions.rend() - visible + 1);
323     }
324   }
325 
326   return actions;
327 }
328 
CreateActions()329 void ToolbarActionsBar::CreateActions() {
330   CHECK(toolbar_actions_.empty());
331   // If the model isn't initialized, wait for it.
332   if (!model_ || !model_->actions_initialized())
333     return;
334 
335   {
336     // We don't redraw the view while creating actions.
337     base::AutoReset<bool> layout_resetter(&suppress_layout_, true);
338 
339     // Get the toolbar actions.
340     toolbar_actions_ =
341         model_->CreateActions(browser_, GetMainBar(), in_overflow_mode());
342     if (!toolbar_actions_.empty())
343       ReorderActions();
344 
345     for (size_t i = 0; i < toolbar_actions_.size(); ++i)
346       delegate_->AddViewForAction(toolbar_actions_[i].get(), i);
347   }
348 
349   // Once the actions are created, we should animate the changes.
350   suppress_animation_ = false;
351 
352   // CreateActions() can be called multiple times, so we need to make sure we
353   // haven't already shown the bubble.
354   // Extension bubbles can also highlight a subset of actions, so don't show the
355   // bubble if the toolbar is already highlighting a different set.
356   if (should_check_extension_bubble_ && !is_highlighting()) {
357     should_check_extension_bubble_ = false;
358     // CreateActions() can be called as part of the browser window set up, which
359     // we need to let finish before showing the actions.
360     base::ThreadTaskRunnerHandle::Get()->PostTask(
361         FROM_HERE, base::BindOnce(&ToolbarActionsBar::MaybeShowExtensionBubble,
362                                   weak_ptr_factory_.GetWeakPtr()));
363   }
364 }
365 
DeleteActions()366 void ToolbarActionsBar::DeleteActions() {
367   HideActivePopup();
368   delegate_->RemoveAllViews();
369   toolbar_actions_.clear();
370 }
371 
Update()372 void ToolbarActionsBar::Update() {
373   if (toolbar_actions_.empty())
374     return;  // Nothing to do.
375 
376   {
377     // Don't layout until the end.
378     base::AutoReset<bool> layout_resetter(&suppress_layout_, true);
379     for (const auto& action : toolbar_actions_)
380       action->UpdateState();
381   }
382 
383   ReorderActions();  // Also triggers a draw.
384 }
385 
ShowToolbarActionPopupForAPICall(const std::string & action_id)386 bool ToolbarActionsBar::ShowToolbarActionPopupForAPICall(
387     const std::string& action_id) {
388   // Don't override another popup, and only show in the active window.
389   if (popup_owner() || !browser_->window()->IsActive())
390     return false;
391 
392   ToolbarActionViewController* action = GetActionForId(action_id);
393   // Since this was triggered by an API call, we never want to grant activeTab
394   // to the extension.
395   constexpr bool kGrantActiveTab = false;
396   return action && action->ExecuteAction(
397                        kGrantActiveTab,
398                        ToolbarActionViewController::InvocationSource::kApi);
399 }
400 
SetOverflowRowWidth(int width)401 void ToolbarActionsBar::SetOverflowRowWidth(int width) {
402   DCHECK(in_overflow_mode());
403   // This uses the unclamped icon count to allow the in-menu bar to span the
404   // menu width.
405   platform_settings_.icons_per_overflow_menu_row =
406       std::max(WidthToIconCountUnclamped(width), static_cast<size_t>(1));
407 }
408 
OnResizeComplete(int width)409 void ToolbarActionsBar::OnResizeComplete(int width) {
410   DCHECK(!in_overflow_mode());  // The user can't resize the overflow container.
411   size_t resized_count = WidthToIconCount(width);
412   // Save off the desired number of visible icons. We do this now instead of
413   // at the end of the animation so that even if the browser is shut down
414   // while animating, the right value will be restored on next run.
415   model_->SetVisibleIconCount(resized_count);
416 }
417 
OnDragStarted(size_t index_of_dragged_item)418 void ToolbarActionsBar::OnDragStarted(size_t index_of_dragged_item) {
419   if (in_overflow_mode()) {
420     main_bar_->OnDragStarted(index_of_dragged_item);
421     return;
422   }
423   DCHECK(!is_drag_in_progress());
424   index_of_dragged_item_ = index_of_dragged_item;
425 }
426 
OnDragEnded()427 void ToolbarActionsBar::OnDragEnded() {
428   // All drag-and-drop commands should go to the main bar.
429   if (in_overflow_mode()) {
430     main_bar_->OnDragEnded();
431     return;
432   }
433 
434   DCHECK(is_drag_in_progress());
435   index_of_dragged_item_.reset();
436   for (ToolbarActionsBarObserver& observer : observers_)
437     observer.OnToolbarActionDragDone();
438 }
439 
OnDragDrop(int dragged_index,int dropped_index,DragType drag_type)440 void ToolbarActionsBar::OnDragDrop(int dragged_index,
441                                    int dropped_index,
442                                    DragType drag_type) {
443   if (in_overflow_mode()) {
444     // All drag-and-drop commands should go to the main bar.
445     main_bar_->OnDragDrop(dragged_index, dropped_index, drag_type);
446     return;
447   }
448 
449   int delta = 0;
450   if (drag_type == DRAG_TO_OVERFLOW)
451     delta = -1;
452   else if (drag_type == DRAG_TO_MAIN &&
453            dragged_index >= static_cast<int>(model_->visible_icon_count()))
454     delta = 1;
455   model_->MoveActionIcon(toolbar_actions_[dragged_index]->GetId(),
456                          dropped_index);
457   if (delta)
458     model_->SetVisibleIconCount(model_->visible_icon_count() + delta);
459 }
460 
IndexOfDraggedItem() const461 const base::Optional<size_t> ToolbarActionsBar::IndexOfDraggedItem() const {
462   DCHECK(!in_overflow_mode());
463   return index_of_dragged_item_;
464 }
465 
OnAnimationEnded()466 void ToolbarActionsBar::OnAnimationEnded() {
467   // Notify the observers now, since showing a bubble or popup could potentially
468   // cause another animation to start.
469   for (ToolbarActionsBarObserver& observer : observers_)
470     observer.OnToolbarActionsBarAnimationEnded();
471 
472   // Check if we were waiting for animation to complete to either show a
473   // message bubble, or to show a popup.
474   if (pending_bubble_controller_) {
475     ShowToolbarActionBubble(std::move(pending_bubble_controller_));
476   } else if (!popped_out_closure_.is_null()) {
477     popped_out_closure_.Run();
478     popped_out_closure_.Reset();
479   }
480 }
481 
OnBubbleClosed()482 void ToolbarActionsBar::OnBubbleClosed() {
483   is_showing_bubble_ = false;
484 }
485 
IsActionVisibleOnToolbar(const ToolbarActionViewController * action) const486 bool ToolbarActionsBar::IsActionVisibleOnToolbar(
487     const ToolbarActionViewController* action) const {
488   if (in_overflow_mode())
489     return main_bar_->IsActionVisibleOnToolbar(action);
490 
491   if (action == popped_out_action_)
492     return true;
493 
494   size_t visible_icon_count = std::min(toolbar_actions_.size(), GetIconCount());
495   for (size_t index = 0; index < visible_icon_count; ++index)
496     if (toolbar_actions_[index].get() == action)
497       return true;
498 
499   return false;
500 }
501 
502 extensions::ExtensionContextMenuModel::ButtonVisibility
GetActionVisibility(const ToolbarActionViewController * action) const503 ToolbarActionsBar::GetActionVisibility(
504     const ToolbarActionViewController* action) const {
505   extensions::ExtensionContextMenuModel::ButtonVisibility visibility =
506       extensions::ExtensionContextMenuModel::PINNED;
507 
508   if (GetPoppedOutAction() == action) {
509     visibility = extensions::ExtensionContextMenuModel::TRANSITIVELY_VISIBLE;
510   } else if (!IsActionVisibleOnToolbar(action)) {
511     visibility = extensions::ExtensionContextMenuModel::UNPINNED;
512   }
513   return visibility;
514 }
515 
PopOutAction(ToolbarActionViewController * controller,bool is_sticky,const base::Closure & closure)516 void ToolbarActionsBar::PopOutAction(ToolbarActionViewController* controller,
517                                      bool is_sticky,
518                                      const base::Closure& closure) {
519   DCHECK(!in_overflow_mode()) << "Only the main bar can pop out actions.";
520   DCHECK(!popped_out_action_) << "Only one action can be popped out at a time!";
521   bool needs_redraw = !IsActionVisibleOnToolbar(controller);
522   popped_out_action_ = controller;
523   is_popped_out_sticky_ = is_sticky;
524   if (needs_redraw) {
525     // We suppress animation for this draw, because we need the action to get
526     // into position immediately, since it's about to show its popup.
527     base::AutoReset<bool> layout_resetter(&suppress_animation_, false);
528     delegate_->Redraw(true);
529   }
530 
531   ResizeDelegate(gfx::Tween::LINEAR);
532   if (!delegate_->IsAnimating()) {
533     // Don't call the closure re-entrantly.
534     base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, closure);
535   } else {
536     popped_out_closure_ = closure;
537   }
538 }
539 
GetPoppedOutAction() const540 ToolbarActionViewController* ToolbarActionsBar::GetPoppedOutAction() const {
541   return popped_out_action_;
542 }
543 
UndoPopOut()544 void ToolbarActionsBar::UndoPopOut() {
545   DCHECK(!in_overflow_mode()) << "Only the main bar can pop out actions.";
546   DCHECK(popped_out_action_);
547   ToolbarActionViewController* controller = popped_out_action_;
548   popped_out_action_ = nullptr;
549   is_popped_out_sticky_ = false;
550   popped_out_closure_.Reset();
551   if (!IsActionVisibleOnToolbar(controller))
552     delegate_->Redraw(true);
553   ResizeDelegate(gfx::Tween::LINEAR);
554 }
555 
SetPopupOwner(ToolbarActionViewController * popup_owner)556 void ToolbarActionsBar::SetPopupOwner(
557     ToolbarActionViewController* popup_owner) {
558   // We should never be setting a popup owner when one already exists, and
559   // never unsetting one when one wasn't set.
560   DCHECK((!popup_owner_ && popup_owner) || (popup_owner_ && !popup_owner));
561   popup_owner_ = popup_owner;
562 }
563 
HideActivePopup()564 void ToolbarActionsBar::HideActivePopup() {
565   if (popup_owner_)
566     popup_owner_->HidePopup();
567   DCHECK(!popup_owner_);
568 }
569 
AddObserver(ToolbarActionsBarObserver * observer)570 void ToolbarActionsBar::AddObserver(ToolbarActionsBarObserver* observer) {
571   observers_.AddObserver(observer);
572 }
573 
RemoveObserver(ToolbarActionsBarObserver * observer)574 void ToolbarActionsBar::RemoveObserver(ToolbarActionsBarObserver* observer) {
575   observers_.RemoveObserver(observer);
576 }
577 
ShowToolbarActionBubble(std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble)578 void ToolbarActionsBar::ShowToolbarActionBubble(
579     std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble) {
580   DCHECK(!in_overflow_mode());
581   if (delegate_->IsAnimating()) {
582     // If the toolbar is animating, we can't effectively anchor the bubble,
583     // so wait until animation stops.
584     pending_bubble_controller_ = std::move(bubble);
585   } else if (bubble->ShouldShow()) {
586     // We check ShouldShow() above since we show the bubble asynchronously, and
587     // it might no longer have been valid.
588 
589     // If needed, close the overflow menu before showing the bubble.
590     ToolbarActionViewController* controller =
591         GetActionForId(bubble->GetAnchorActionId());
592     bool close_overflow_menu =
593         controller && !IsActionVisibleOnToolbar(controller);
594     if (close_overflow_menu)
595       delegate_->CloseOverflowMenuIfOpen();
596 
597     is_showing_bubble_ = true;
598     delegate_->ShowToolbarActionBubble(std::move(bubble));
599   }
600 }
601 
ShowToolbarActionBubbleAsync(std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble)602 void ToolbarActionsBar::ShowToolbarActionBubbleAsync(
603     std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble) {
604   base::ThreadTaskRunnerHandle::Get()->PostTask(
605       FROM_HERE,
606       base::BindOnce(&ToolbarActionsBar::ShowToolbarActionBubble,
607                      weak_ptr_factory_.GetWeakPtr(), std::move(bubble)));
608 }
609 
CloseOverflowMenuIfOpen()610 bool ToolbarActionsBar::CloseOverflowMenuIfOpen() {
611   return delegate_->CloseOverflowMenuIfOpen();
612 }
613 
MaybeShowExtensionBubble()614 void ToolbarActionsBar::MaybeShowExtensionBubble() {
615   std::unique_ptr<extensions::ExtensionMessageBubbleController> controller =
616       model_->GetExtensionMessageBubbleController(browser_);
617   if (!controller)
618     return;
619 
620   DCHECK(controller->ShouldShow());
621   controller->HighlightExtensionsIfNecessary();  // Safe to call multiple times.
622 
623   // Not showing the bubble right away (during startup) has a few benefits:
624   // We don't have to worry about focus being lost due to the Omnibox (or to
625   // other things that want focus at startup). This allows Esc to work to close
626   // the bubble and also solves the keyboard accessibility problem that comes
627   // with focus being lost (we don't have a good generic mechanism of injecting
628   // bubbles into the focus cycle). Another benefit of delaying the show is
629   // that fade-in works (the fade-in isn't apparent if the the bubble appears at
630   // startup).
631   std::unique_ptr<ToolbarActionsBarBubbleDelegate> delegate(
632       new ExtensionMessageBubbleBridge(std::move(controller)));
633   base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
634       FROM_HERE,
635       base::BindOnce(&ToolbarActionsBar::ShowToolbarActionBubble,
636                      weak_ptr_factory_.GetWeakPtr(), std::move(delegate)),
637       base::TimeDelta::FromSeconds(
638           g_extension_bubble_appearance_wait_time_in_seconds));
639 }
640 
GetMainBar()641 ToolbarActionsBar* ToolbarActionsBar::GetMainBar() {
642   return main_bar_ ? main_bar_ : this;
643 }
644 
645 // static
set_extension_bubble_appearance_wait_time_for_testing(int time_in_seconds)646 void ToolbarActionsBar::set_extension_bubble_appearance_wait_time_for_testing(
647     int time_in_seconds) {
648   g_extension_bubble_appearance_wait_time_in_seconds = time_in_seconds;
649 }
650 
GetIconAreaInsets() const651 gfx::Insets ToolbarActionsBar::GetIconAreaInsets() const {
652   return GetLayoutInsets(TOOLBAR_ACTION_VIEW);
653 }
654 
OnToolbarActionAdded(const ToolbarActionsModel::ActionId & action_id,int index)655 void ToolbarActionsBar::OnToolbarActionAdded(
656     const ToolbarActionsModel::ActionId& action_id,
657     int index) {
658   CHECK(model_->actions_initialized());
659   CHECK(GetActionForId(action_id) == nullptr)
660       << "Asked to add a toolbar action view for an action that already "
661          "exists";
662 
663   toolbar_actions_.insert(
664       toolbar_actions_.begin() + index,
665       model_->CreateActionForId(browser_, GetMainBar(), in_overflow_mode(),
666                                 action_id));
667   delegate_->AddViewForAction(toolbar_actions_[index].get(), index);
668 
669   // We may need to resize (e.g. to show the new icon). We don't need to check
670   // if an extension is upgrading here, because ResizeDelegate() checks to see
671   // if the container is already the proper size, and because if the action is
672   // newly incognito enabled, even though it's a reload, it's a new extension to
673   // this toolbar.
674   ResizeDelegate(gfx::Tween::LINEAR);
675 }
676 
OnToolbarActionLoadFailed()677 void ToolbarActionsBar::OnToolbarActionLoadFailed() {
678   // When an extension is re-uploaded, it is first unloaded from Chrome. At this
679   // point, the extension's icon is initially removed from the toolbar, leaving
680   // an empty slot in the toolbar. Then the (newer version of the) extension is
681   // loaded, and its icon populates the empty slot.
682   //
683   // If the extension failed to load, then the empty slot should be removed and
684   // hence we resize the toolbar.
685   ResizeDelegate(gfx::Tween::EASE_OUT);
686 }
687 
OnToolbarActionRemoved(const ToolbarActionsModel::ActionId & action_id)688 void ToolbarActionsBar::OnToolbarActionRemoved(
689     const ToolbarActionsModel::ActionId& action_id) {
690   auto iter = toolbar_actions_.begin();
691   while (iter != toolbar_actions_.end() && (*iter)->GetId() != action_id)
692     ++iter;
693 
694   if (iter == toolbar_actions_.end())
695     return;
696 
697   // The action should outlive the UI element (which is owned by the delegate),
698   // so we can't delete it just yet. But we should remove it from the list of
699   // actions so that any width calculations are correct.
700   std::unique_ptr<ToolbarActionViewController> removed_action =
701       std::move(*iter);
702   toolbar_actions_.erase(iter);
703 
704   // If we kill the view before we undo the popout, highlights and pop-ups can
705   // get left in weird states, so undo the popout first.
706   if (popped_out_action_ == removed_action.get())
707     UndoPopOut();
708   delegate_->RemoveViewForAction(removed_action.get());
709   removed_action.reset();
710 
711   // If the extension is being upgraded we don't want the bar to shrink
712   // because the icon is just going to get re-added to the same location.
713   // There is an exception if this is an off-the-record profile, and the
714   // extension is no longer incognito-enabled.
715   if (!extensions::ExtensionSystem::Get(browser_->profile())
716            ->runtime_data()
717            ->IsBeingUpgraded(action_id) ||
718       (browser_->profile()->IsOffTheRecord() &&
719        !extensions::util::IsIncognitoEnabled(action_id, browser_->profile()))) {
720     if (toolbar_actions_.size() > model_->visible_icon_count()) {
721       // If we have more icons than we can show, then we must not be changing
722       // the container size (since we either removed an icon from the main
723       // area and one from the overflow list will have shifted in, or we
724       // removed an entry directly from the overflow list).
725       delegate_->Redraw(false);
726     } else {
727       // Either we went from overflow to no-overflow, or we shrunk the no-
728       // overflow container by 1.  Either way the size changed, so animate.
729       ResizeDelegate(gfx::Tween::EASE_OUT);
730     }
731   }
732 }
733 
OnToolbarActionMoved(const ToolbarActionsModel::ActionId & action_id,int index)734 void ToolbarActionsBar::OnToolbarActionMoved(
735     const ToolbarActionsModel::ActionId& action_id,
736     int index) {
737   DCHECK(index >= 0 && index < static_cast<int>(toolbar_actions_.size()));
738   // Unfortunately, |index| doesn't really mean a lot to us, because this
739   // window's toolbar could be different (if actions are popped out). Just
740   // do a full reorder.
741   ReorderActions();
742 }
743 
OnToolbarActionUpdated(const ToolbarActionsModel::ActionId & action_id)744 void ToolbarActionsBar::OnToolbarActionUpdated(
745     const ToolbarActionsModel::ActionId& action_id) {
746   ToolbarActionViewController* action = GetActionForId(action_id);
747   // There might not be a view in cases where we are highlighting or if we
748   // haven't fully initialized the actions.
749   if (action)
750     action->UpdateState();
751 }
752 
OnToolbarVisibleCountChanged()753 void ToolbarActionsBar::OnToolbarVisibleCountChanged() {
754   ResizeDelegate(gfx::Tween::EASE_OUT);
755 }
756 
ResizeDelegate(gfx::Tween::Type tween_type)757 void ToolbarActionsBar::ResizeDelegate(gfx::Tween::Type tween_type) {
758   int desired_width = GetFullSize().width();
759   if (desired_width !=
760       delegate_->GetWidth(ToolbarActionsBarDelegate::GET_WIDTH_CURRENT)) {
761     delegate_->ResizeAndAnimate(tween_type, desired_width);
762   } else if (delegate_->IsAnimating()) {
763     // It's possible that we're right where we're supposed to be in terms of
764     // width, but that we're also currently resizing. If this is the case, end
765     // the current animation with the current width.
766     delegate_->StopAnimating();
767   } else {
768     // We may already be at the right size (this can happen frequently with
769     // overflow, where we have a fixed width, and in tests, where we skip
770     // animations). If this is the case, we still need to Redraw(), because the
771     // icons within the toolbar may have changed (e.g. if we removed one
772     // action and added a different one in quick succession).
773     delegate_->Redraw(false);
774   }
775 }
776 
OnToolbarHighlightModeChanged(bool is_highlighting)777 void ToolbarActionsBar::OnToolbarHighlightModeChanged(bool is_highlighting) {
778   if (!model_->actions_initialized())
779     return;
780 
781   {
782     base::AutoReset<bool> layout_resetter(&suppress_layout_, true);
783     base::AutoReset<bool> animation_resetter(&suppress_animation_, true);
784     std::set<std::string> model_action_ids;
785     for (const auto& model_action_id : model_->action_ids()) {
786       model_action_ids.insert(model_action_id);
787 
788       bool found = false;
789       for (size_t i = 0; i < toolbar_actions_.size(); ++i) {
790         if (toolbar_actions_[i]->GetId() == model_action_id) {
791           found = true;
792           break;
793         }
794       }
795 
796       if (!found) {
797         toolbar_actions_.push_back(model_->CreateActionForId(
798             browser_, GetMainBar(), in_overflow_mode(), model_action_id));
799         delegate_->AddViewForAction(toolbar_actions_.back().get(),
800                                     toolbar_actions_.size() - 1);
801       }
802     }
803 
804     for (auto iter = toolbar_actions_.begin();
805          iter != toolbar_actions_.end();) {
806       if (model_action_ids.count((*iter)->GetId()) == 0) {
807         delegate_->RemoveViewForAction(iter->get());
808         iter = toolbar_actions_.erase(iter);
809       } else {
810         ++iter;
811       }
812     }
813   }
814 
815   ReorderActions();
816 }
817 
OnToolbarModelInitialized()818 void ToolbarActionsBar::OnToolbarModelInitialized() {
819   // We shouldn't have any actions before the model is initialized.
820   CHECK(toolbar_actions_.empty());
821   CreateActions();
822   ResizeDelegate(gfx::Tween::EASE_OUT);
823 }
824 
OnToolbarPinnedActionsChanged()825 void ToolbarActionsBar::OnToolbarPinnedActionsChanged() {
826   NOTREACHED();
827 }
828 
OnTabStripModelChanged(TabStripModel * tab_strip_model,const TabStripModelChange & change,const TabStripSelectionChange & selection)829 void ToolbarActionsBar::OnTabStripModelChanged(
830     TabStripModel* tab_strip_model,
831     const TabStripModelChange& change,
832     const TabStripSelectionChange& selection) {
833   if (tab_strip_model->empty() || !selection.active_tab_changed())
834     return;
835 
836   extensions::MaybeShowExtensionControlledNewTabPage(browser_,
837                                                      selection.new_contents);
838 }
839 
ReorderActions()840 void ToolbarActionsBar::ReorderActions() {
841   if (toolbar_actions_.empty())
842     return;
843 
844   // First, reset the order to that of the model.
845   auto compare = [](ToolbarActionViewController* const& action,
846                     const ToolbarActionsModel::ActionId& action_id) {
847     return action->GetId() == action_id;
848   };
849   SortContainer(&toolbar_actions_, model_->action_ids(), compare);
850 
851   // Our visible browser actions may have changed - re-Layout() and check the
852   // size (if we aren't suppressing the layout).
853   if (!suppress_layout_) {
854     ResizeDelegate(gfx::Tween::EASE_OUT);
855     delegate_->Redraw(true);
856   }
857 }
858 
GetActionForId(const std::string & action_id)859 ToolbarActionViewController* ToolbarActionsBar::GetActionForId(
860     const std::string& action_id) {
861   for (const auto& action : toolbar_actions_) {
862     if (action->GetId() == action_id)
863       return action.get();
864   }
865   return nullptr;
866 }
867 
GetCurrentWebContents()868 content::WebContents* ToolbarActionsBar::GetCurrentWebContents() {
869   return browser_->tab_strip_model()->GetActiveWebContents();
870 }
871