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