blob: 382885bcc82de1c474182036e14f80c350e125f8 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/extensions/extensions_toolbar_container.h"
#include "base/numerics/ranges.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
#include "chrome/browser/ui/views/extensions/browser_action_drag_data.h"
#include "chrome/browser/ui/views/extensions/extensions_menu_view.h"
#include "chrome/browser/ui/views/extensions/extensions_toolbar_button.h"
#include "chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.h"
#include "ui/views/layout/animating_layout_manager.h"
#include "ui/views/widget/widget_observer.h"
struct ExtensionsToolbarContainer::DropInfo {
DropInfo(ToolbarActionsModel::ActionId action_id, size_t index);
// The id for the action being dragged.
ToolbarActionsModel::ActionId action_id;
// The (0-indexed) icon before the action will be dropped.
size_t index;
};
ExtensionsToolbarContainer::DropInfo::DropInfo(
ToolbarActionsModel::ActionId action_id,
size_t index)
: action_id(action_id), index(index) {}
ExtensionsToolbarContainer::ExtensionsToolbarContainer(Browser* browser)
: ToolbarIconContainerView(/*uses_highlight=*/true),
browser_(browser),
model_(ToolbarActionsModel::Get(browser_->profile())),
model_observer_(this),
extensions_button_(new ExtensionsToolbarButton(browser_, this)) {
model_observer_.Add(model_);
// Do not flip the Extensions icon in RTL.
extensions_button_->EnableCanvasFlippingForRTLUI(false);
AddMainButton(extensions_button_);
CreateActions();
}
ExtensionsToolbarContainer::~ExtensionsToolbarContainer() {
if (active_bubble_)
active_bubble_->GetWidget()->Close();
// We should synchronously receive the OnWidgetClosing() event, so we should
// always have cleared the active bubble by now.
DCHECK(!active_bubble_);
}
void ExtensionsToolbarContainer::UpdateAllIcons() {
extensions_button_->UpdateIcon();
for (const auto& action : actions_)
action->UpdateState();
}
ToolbarActionView* ExtensionsToolbarContainer::GetViewForId(
const std::string& id) {
auto it = icons_.find(id);
if (it == icons_.end())
return nullptr;
return it->second.get();
}
ToolbarActionViewController* ExtensionsToolbarContainer::GetActionForId(
const std::string& action_id) {
for (const auto& action : actions_) {
if (action->GetId() == action_id)
return action.get();
}
return nullptr;
}
ToolbarActionViewController* ExtensionsToolbarContainer::GetPoppedOutAction()
const {
return popped_out_action_;
}
bool ExtensionsToolbarContainer::IsActionVisibleOnToolbar(
const ToolbarActionViewController* action) const {
return model_->IsActionPinned(action->GetId()) ||
action == popped_out_action_ ||
(active_bubble_ &&
action->GetId() == active_bubble_->GetAnchorActionId());
}
void ExtensionsToolbarContainer::UndoPopOut() {
DCHECK(popped_out_action_);
ToolbarActionViewController* const popped_out_action = popped_out_action_;
popped_out_action_ = nullptr;
// Note that we only hide this view if it was not pinned while being popped
// out.
icons_[popped_out_action->GetId()]->SetVisible(
IsActionVisibleOnToolbar(popped_out_action));
}
void ExtensionsToolbarContainer::SetPopupOwner(
ToolbarActionViewController* popup_owner) {
// We should never be setting a popup owner when one already exists, and
// never unsetting one when one wasn't set.
DCHECK((popup_owner_ != nullptr) ^ (popup_owner != nullptr));
popup_owner_ = popup_owner;
}
void ExtensionsToolbarContainer::HideActivePopup() {
if (popup_owner_)
popup_owner_->HidePopup();
DCHECK(!popup_owner_);
}
bool ExtensionsToolbarContainer::CloseOverflowMenuIfOpen() {
if (ExtensionsMenuView::IsShowing()) {
ExtensionsMenuView::Hide();
return true;
}
return false;
}
void ExtensionsToolbarContainer::PopOutAction(
ToolbarActionViewController* action,
bool is_sticky,
const base::Closure& closure) {
// TODO(pbos): Highlight popout differently.
DCHECK(!popped_out_action_);
popped_out_action_ = action;
icons_[popped_out_action_->GetId()]->SetVisible(true);
ReorderViews();
static_cast<views::AnimatingLayoutManager*>(GetLayoutManager())
->RunOrQueueAction(closure);
}
void ExtensionsToolbarContainer::ShowToolbarActionBubble(
std::unique_ptr<ToolbarActionsBarBubbleDelegate> controller) {
auto iter = icons_.find(controller->GetAnchorActionId());
views::View* const anchor_view = iter != icons_.end()
? static_cast<View*>(iter->second.get())
: extensions_button_;
anchor_view->SetVisible(true);
static_cast<views::AnimatingLayoutManager*>(GetLayoutManager())
->RunOrQueueAction(
base::BindOnce(&ExtensionsToolbarContainer::ShowActiveBubble,
weak_ptr_factory_.GetWeakPtr(), anchor_view,
base::Passed(std::move(controller))));
}
void ExtensionsToolbarContainer::ShowToolbarActionBubbleAsync(
std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&ExtensionsToolbarContainer::ShowToolbarActionBubble,
weak_ptr_factory_.GetWeakPtr(), std::move(bubble)));
}
void ExtensionsToolbarContainer::OnToolbarActionAdded(
const ToolbarActionsModel::ActionId& action_id,
int index) {
CreateActionForId(action_id);
ReorderViews();
}
void ExtensionsToolbarContainer::OnToolbarActionRemoved(
const ToolbarActionsModel::ActionId& action_id) {
// TODO(pbos): Handle extension upgrades, see ToolbarActionsBar. Arguably this
// could be handled inside the model and be invisible to the container when
// permissions are unchanged.
// Delete the icon first so it unregisters it from the action.
icons_.erase(action_id);
base::EraseIf(
actions_,
[action_id](const std::unique_ptr<ToolbarActionViewController>& item) {
return item->GetId() == action_id;
});
}
void ExtensionsToolbarContainer::OnToolbarActionMoved(
const ToolbarActionsModel::ActionId& action_id,
int index) {}
void ExtensionsToolbarContainer::OnToolbarActionLoadFailed() {}
void ExtensionsToolbarContainer::OnToolbarActionUpdated(
const ToolbarActionsModel::ActionId& action_id) {
ToolbarActionViewController* action = GetActionForId(action_id);
if (action)
action->UpdateState();
}
void ExtensionsToolbarContainer::OnToolbarVisibleCountChanged() {}
void ExtensionsToolbarContainer::OnToolbarHighlightModeChanged(
bool is_highlighting) {}
void ExtensionsToolbarContainer::OnToolbarModelInitialized() {
CreateActions();
}
void ExtensionsToolbarContainer::OnToolbarPinnedActionsChanged() {
for (auto& it : icons_)
it.second->SetVisible(IsActionVisibleOnToolbar(GetActionForId(it.first)));
ReorderViews();
}
void ExtensionsToolbarContainer::ReorderViews() {
const auto& pinned_action_ids = model_->pinned_action_ids();
for (size_t i = 0; i < pinned_action_ids.size(); ++i)
ReorderChildView(icons_[pinned_action_ids[i]].get(), i);
if (drop_info_.get())
ReorderChildView(icons_[drop_info_->action_id].get(), drop_info_->index);
// Popped out actions should be at the end.
if (popped_out_action_)
ReorderChildView(icons_[popped_out_action_->GetId()].get(), -1);
// The extension button is always last.
ReorderChildView(extensions_button_, -1);
}
void ExtensionsToolbarContainer::CreateActions() {
DCHECK(icons_.empty());
DCHECK(actions_.empty());
// If the model isn't initialized, wait for it.
if (!model_->actions_initialized())
return;
for (auto& action_id : model_->action_ids())
CreateActionForId(action_id);
ReorderViews();
}
void ExtensionsToolbarContainer::CreateActionForId(
const ToolbarActionsModel::ActionId& action_id) {
actions_.push_back(
model_->CreateActionForId(browser_, this, false, action_id));
auto icon = std::make_unique<ToolbarActionView>(actions_.back().get(), this);
icon->set_owned_by_client();
icon->SetVisible(IsActionVisibleOnToolbar(actions_.back().get()));
icon->AddButtonObserver(this);
icon->AddObserver(this);
AddChildView(icon.get());
icons_[action_id] = std::move(icon);
}
content::WebContents* ExtensionsToolbarContainer::GetCurrentWebContents() {
return browser_->tab_strip_model()->GetActiveWebContents();
}
bool ExtensionsToolbarContainer::ShownInsideMenu() const {
return false;
}
void ExtensionsToolbarContainer::OnToolbarActionViewDragDone() {}
views::LabelButton* ExtensionsToolbarContainer::GetOverflowReferenceView()
const {
return extensions_button_;
}
gfx::Size ExtensionsToolbarContainer::GetToolbarActionSize() {
gfx::Rect rect(gfx::Size(28, 28));
rect.Inset(-GetLayoutInsets(TOOLBAR_ACTION_VIEW));
return rect.size();
}
void ExtensionsToolbarContainer::WriteDragDataForView(
View* sender,
const gfx::Point& press_pt,
ui::OSExchangeData* data) {
DCHECK(data);
auto it = std::find_if(model_->pinned_action_ids().cbegin(),
model_->pinned_action_ids().cend(),
[this, sender](const std::string& action_id) {
return GetViewForId(action_id) == sender;
});
DCHECK(it != model_->pinned_action_ids().cend());
size_t index = it - model_->pinned_action_ids().cbegin();
ToolbarActionView* extension_view = GetViewForId(*it);
data->provider().SetDragImage(GetExtensionIcon(extension_view),
press_pt.OffsetFromOrigin());
// Fill in the remaining info.
BrowserActionDragData drag_data(extension_view->view_controller()->GetId(),
index);
drag_data.Write(browser_->profile(), data);
}
int ExtensionsToolbarContainer::GetDragOperationsForView(View* sender,
const gfx::Point& p) {
return ui::DragDropTypes::DRAG_MOVE;
}
bool ExtensionsToolbarContainer::CanStartDragForView(View* sender,
const gfx::Point& press_pt,
const gfx::Point& p) {
return true;
}
bool ExtensionsToolbarContainer::GetDropFormats(
int* formats,
std::set<ui::ClipboardFormatType>* format_types) {
return BrowserActionDragData::GetDropFormats(format_types);
}
bool ExtensionsToolbarContainer::AreDropTypesRequired() {
return BrowserActionDragData::AreDropTypesRequired();
}
bool ExtensionsToolbarContainer::CanDrop(const OSExchangeData& data) {
return BrowserActionDragData::CanDrop(data, browser_->profile());
}
int ExtensionsToolbarContainer::OnDragUpdated(
const ui::DropTargetEvent& event) {
BrowserActionDragData data;
if (!data.Read(event.data()))
return ui::DragDropTypes::DRAG_NONE;
size_t before_icon = 0;
// Figure out where to display the icon during dragging transition.
// First, since we want to update the dragged extension's position from before
// an icon to after it when the event passes the midpoint between two icons.
// This will convert the event coordinate into the index of the icon we want
// to display the dragged extension before. We also mirror the event.x() so
// that our calculations are consistent with left-to-right.
const int offset_into_icon_area = GetMirroredXInView(event.x());
const int before_icon_unclamped = WidthToIconCount(offset_into_icon_area);
int visible_icons = model_->pinned_action_ids().size();
// Because the user can drag outside the container bounds, we need to clamp
// to the valid range. Note that the maximum allowable value is
// |visible_icons|, not (|visible_icons| - 1), because we represent the
// dragged extension being past the last icon as being "before the (last + 1)
// icon".
before_icon = base::ClampToRange(before_icon_unclamped, 0, visible_icons);
if (!drop_info_.get() || drop_info_->index != before_icon) {
drop_info_ = std::make_unique<DropInfo>(data.id(), before_icon);
SetExtensionIconVisibility(drop_info_->action_id, false);
ReorderViews();
}
return ui::DragDropTypes::DRAG_MOVE;
}
void ExtensionsToolbarContainer::OnDragExited() {
const ToolbarActionsModel::ActionId dragged_extension_id =
drop_info_->action_id;
drop_info_.reset();
ReorderViews();
static_cast<views::AnimatingLayoutManager*>(GetLayoutManager())
->RunOrQueueAction(base::BindOnce(
&ExtensionsToolbarContainer::SetExtensionIconVisibility,
weak_ptr_factory_.GetWeakPtr(), dragged_extension_id, true));
}
int ExtensionsToolbarContainer::OnPerformDrop(
const ui::DropTargetEvent& event) {
BrowserActionDragData data;
if (!data.Read(event.data()))
return ui::DragDropTypes::DRAG_NONE;
model_->MovePinnedAction(drop_info_->action_id, drop_info_->index);
OnDragExited(); // Perform clean up after dragging.
return ui::DragDropTypes::DRAG_MOVE;
}
void ExtensionsToolbarContainer::OnWidgetClosing(views::Widget* widget) {
ClearActiveBubble(widget);
}
void ExtensionsToolbarContainer::OnWidgetDestroying(views::Widget* widget) {
ClearActiveBubble(widget);
}
void ExtensionsToolbarContainer::ClearActiveBubble(views::Widget* widget) {
DCHECK(active_bubble_);
DCHECK_EQ(active_bubble_->GetWidget(), widget);
ToolbarActionViewController* const action =
GetActionForId(active_bubble_->GetAnchorActionId());
// TODO(pbos): Note that this crashes if a bubble anchors to the menu and not
// to an extension that gets popped out. This should be fixed, but a test
// should first be added to make sure that it's covered.
CHECK(action);
active_bubble_ = nullptr;
widget->RemoveObserver(this);
// Note that we only hide this view if it's not visible for other reasons
// than displaying the bubble.
icons_[action->GetId()]->SetVisible(IsActionVisibleOnToolbar(action));
}
size_t ExtensionsToolbarContainer::WidthToIconCount(int x_offset) {
const int element_padding = GetLayoutConstant(TOOLBAR_ELEMENT_PADDING);
size_t unclamped_count =
std::max((x_offset + element_padding) /
(GetToolbarActionSize().width() + element_padding),
0);
return std::min(unclamped_count, actions_.size());
}
gfx::ImageSkia ExtensionsToolbarContainer::GetExtensionIcon(
ToolbarActionView* extension_view) {
return extension_view->view_controller()
->GetIcon(GetCurrentWebContents(), GetToolbarActionSize())
.AsImageSkia();
}
void ExtensionsToolbarContainer::SetExtensionIconVisibility(
ToolbarActionsModel::ActionId id,
bool visible) {
auto it = std::find_if(model_->pinned_action_ids().cbegin(),
model_->pinned_action_ids().cend(),
[this, id](const std::string& action_id) {
return GetViewForId(action_id) == GetViewForId(id);
});
ToolbarActionView* extension_view = GetViewForId(*it);
extension_view->SetImage(
views::Button::STATE_NORMAL,
visible ? GetExtensionIcon(extension_view) : gfx::ImageSkia());
}
void ExtensionsToolbarContainer::ShowActiveBubble(
views::View* anchor_view,
std::unique_ptr<ToolbarActionsBarBubbleDelegate> controller) {
active_bubble_ = new ToolbarActionsBarBubbleViews(
anchor_view, anchor_view != extensions_button_, std::move(controller));
views::BubbleDialogDelegateView::CreateBubble(active_bubble_)
->AddObserver(this);
active_bubble_->Show();
}