blob: 9b411f34abb33196ce29f11a300418cdda70547d [file] [log] [blame]
// Copyright 2013 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/toolbar/browser_actions_container.h"
#include <utility>
#include "base/compiler_specific.h"
#include "base/memory/ptr_util.h"
#include "chrome/browser/extensions/extension_message_bubble_controller.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_bar.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_model.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/extensions/browser_action_drag_data.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/toolbar/app_menu_button.h"
#include "chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/common/extensions/command.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "extensions/common/feature_switch.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/dragdrop/drag_utils.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/nine_image_painter_factory.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/views/bubble/bubble_dialog_delegate.h"
#include "ui/views/controls/resize_area.h"
#include "ui/views/painter.h"
#include "ui/views/widget/widget.h"
namespace {
// Returns the ToolbarView for the given |browser|.
ToolbarView* GetToolbarView(Browser* browser) {
return BrowserView::GetBrowserViewForBrowser(browser)->toolbar();
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// BrowserActionsContainer::DropPosition
struct BrowserActionsContainer::DropPosition {
DropPosition(size_t row, size_t icon_in_row);
// The (0-indexed) row into which the action will be dropped.
size_t row;
// The (0-indexed) icon in the row before the action will be dropped.
size_t icon_in_row;
};
BrowserActionsContainer::DropPosition::DropPosition(
size_t row, size_t icon_in_row)
: row(row), icon_in_row(icon_in_row) {
}
////////////////////////////////////////////////////////////////////////////////
// BrowserActionsContainer
BrowserActionsContainer::BrowserActionsContainer(
Browser* browser,
BrowserActionsContainer* main_container)
: toolbar_actions_bar_(new ToolbarActionsBar(
this,
browser,
main_container ?
main_container->toolbar_actions_bar_.get() : nullptr)),
browser_(browser),
main_container_(main_container),
resize_area_(NULL),
added_to_view_(false),
resize_starting_width_(-1),
resize_amount_(0),
animation_target_size_(0),
active_bubble_(nullptr) {
set_id(VIEW_ID_BROWSER_ACTION_TOOLBAR);
if (!ShownInsideMenu()) {
resize_animation_.reset(new gfx::SlideAnimation(this));
resize_area_ = new views::ResizeArea(this);
AddChildView(resize_area_);
const int kInfoImages[] = IMAGE_GRID(IDR_TOOLBAR_ACTION_HIGHLIGHT);
info_highlight_painter_.reset(
views::Painter::CreateImageGridPainter(kInfoImages));
const int kWarningImages[] = IMAGE_GRID(IDR_DEVELOPER_MODE_HIGHLIGHT);
warning_highlight_painter_.reset(
views::Painter::CreateImageGridPainter(kWarningImages));
}
}
BrowserActionsContainer::~BrowserActionsContainer() {
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_);
toolbar_actions_bar_->DeleteActions();
// All views should be removed as part of ToolbarActionsBar::DeleteActions().
DCHECK(toolbar_action_views_.empty());
}
std::string BrowserActionsContainer::GetIdAt(size_t index) const {
return toolbar_action_views_[index]->view_controller()->GetId();
}
ToolbarActionView* BrowserActionsContainer::GetViewForId(
const std::string& id) {
for (const auto& view : toolbar_action_views_) {
if (view->view_controller()->GetId() == id)
return view.get();
}
return nullptr;
}
void BrowserActionsContainer::RefreshToolbarActionViews() {
toolbar_actions_bar_->Update();
}
size_t BrowserActionsContainer::VisibleBrowserActions() const {
size_t visible_actions = 0;
for (const auto& view : toolbar_action_views_) {
if (view->visible())
++visible_actions;
}
return visible_actions;
}
size_t BrowserActionsContainer::VisibleBrowserActionsAfterAnimation() const {
if (!animating())
return VisibleBrowserActions();
return toolbar_actions_bar_->WidthToIconCount(animation_target_size_);
}
bool BrowserActionsContainer::ShownInsideMenu() const {
return main_container_ != nullptr;
}
void BrowserActionsContainer::OnToolbarActionViewDragDone() {
toolbar_actions_bar_->OnDragEnded();
}
views::MenuButton* BrowserActionsContainer::GetOverflowReferenceView() {
return static_cast<views::MenuButton*>(
GetToolbarView(browser_)->app_menu_button());
}
void BrowserActionsContainer::AddViewForAction(
ToolbarActionViewController* view_controller,
size_t index) {
ToolbarActionView* view = new ToolbarActionView(view_controller, this);
toolbar_action_views_.insert(toolbar_action_views_.begin() + index,
base::WrapUnique(view));
AddChildViewAt(view, index);
}
void BrowserActionsContainer::RemoveViewForAction(
ToolbarActionViewController* action) {
for (ToolbarActionViews::iterator iter = toolbar_action_views_.begin();
iter != toolbar_action_views_.end(); ++iter) {
if ((*iter)->view_controller() == action) {
toolbar_action_views_.erase(iter);
break;
}
}
}
void BrowserActionsContainer::RemoveAllViews() {
toolbar_action_views_.clear();
}
void BrowserActionsContainer::Redraw(bool order_changed) {
if (!added_to_view_) {
// We don't want to redraw before the view has been fully added to the
// hierarchy.
return;
}
// Don't allow resizing if the bar is highlighting.
if (resize_area_)
resize_area_->SetEnabled(!toolbar_actions_bar()->is_highlighting());
std::vector<ToolbarActionViewController*> actions =
toolbar_actions_bar_->GetActions();
if (order_changed) {
// Run through the views and compare them to the desired order. If something
// is out of place, find the correct spot for it.
for (int i = 0; i < static_cast<int>(actions.size()) - 1; ++i) {
if (actions[i] != toolbar_action_views_[i]->view_controller()) {
// Find where the correct view is (it's guaranteed to be after our
// current index, since everything up to this point is correct).
int j = i + 1;
while (actions[i] != toolbar_action_views_[j]->view_controller())
++j;
std::swap(toolbar_action_views_[i], toolbar_action_views_[j]);
// Also move the view in the child views vector.
ReorderChildView(toolbar_action_views_[i].get(), i);
}
}
}
Layout();
}
void BrowserActionsContainer::ResizeAndAnimate(gfx::Tween::Type tween_type,
int target_width) {
if (resize_animation_ && !toolbar_actions_bar_->suppress_animation()) {
if (!ShownInsideMenu()) {
// Make sure we don't try to animate to wider than the allowed width.
int max_width = GetToolbarView(browser_)->GetMaxBrowserActionsWidth();
if (target_width > max_width)
target_width = GetWidthForMaxWidth(max_width);
}
// Animate! We have to set the animation_target_size_ after calling Reset(),
// because that could end up calling AnimationEnded which clears the value.
resize_animation_->Reset();
resize_starting_width_ = width();
resize_animation_->SetTweenType(tween_type);
animation_target_size_ = target_width;
resize_animation_->Show();
} else {
animation_target_size_ = target_width;
AnimationEnded(resize_animation_.get());
}
}
int BrowserActionsContainer::GetWidth(GetWidthTime get_width_time) const {
return get_width_time == GET_WIDTH_AFTER_ANIMATION &&
animation_target_size_ > 0
? animation_target_size_
: width();
}
bool BrowserActionsContainer::IsAnimating() const {
return animating();
}
void BrowserActionsContainer::StopAnimating() {
animation_target_size_ = width();
resize_animation_->Reset();
}
void BrowserActionsContainer::ShowToolbarActionBubble(
std::unique_ptr<ToolbarActionsBarBubbleDelegate> controller) {
// The container shouldn't be asked to show a bubble if it's animating.
DCHECK(!animating());
DCHECK(!active_bubble_);
views::View* anchor_view = nullptr;
if (!controller->GetAnchorActionId().empty()) {
ToolbarActionView* action_view =
GetViewForId(controller->GetAnchorActionId());
if (action_view) {
anchor_view =
action_view->visible() ? action_view : GetOverflowReferenceView();
} else {
anchor_view = BrowserView::GetBrowserViewForBrowser(browser_)
->toolbar()
->app_menu_button();
}
} else {
anchor_view = this;
}
ToolbarActionsBarBubbleViews* bubble =
new ToolbarActionsBarBubbleViews(anchor_view, std::move(controller));
active_bubble_ = bubble;
views::BubbleDialogDelegateView::CreateBubble(bubble);
bubble->GetWidget()->AddObserver(this);
bubble->Show();
}
void BrowserActionsContainer::OnWidgetClosing(views::Widget* widget) {
ClearActiveBubble(widget);
}
void BrowserActionsContainer::OnWidgetDestroying(views::Widget* widget) {
ClearActiveBubble(widget);
}
int BrowserActionsContainer::GetWidthForMaxWidth(int max_width) const {
int preferred_width = GetPreferredSize().width();
if (preferred_width > max_width) {
// If we can't even show the minimum width, just throw in the towel (and
// show nothing).
if (max_width < toolbar_actions_bar_->GetMinimumWidth())
return 0;
preferred_width = toolbar_actions_bar_->IconCountToWidth(
toolbar_actions_bar_->WidthToIconCount(max_width));
}
return preferred_width;
}
gfx::Size BrowserActionsContainer::GetPreferredSize() const {
if (ShownInsideMenu())
return toolbar_actions_bar_->GetPreferredSize();
// If there are no actions to show, then don't show the container at all.
if (toolbar_action_views_.empty())
return gfx::Size();
// When resizing, preferred width is the starting width - resize amount.
// Otherwise, use the normal preferred width.
int preferred_width = resize_starting_width_ == -1 ?
toolbar_actions_bar_->GetPreferredSize().width() :
resize_starting_width_ - resize_amount_;
// In either case, clamp it within the max/min bounds.
preferred_width = std::min(
std::max(toolbar_actions_bar_->GetMinimumWidth(), preferred_width),
toolbar_actions_bar_->GetMaximumWidth());
return gfx::Size(preferred_width, ToolbarActionsBar::IconHeight());
}
int BrowserActionsContainer::GetHeightForWidth(int width) const {
if (ShownInsideMenu())
toolbar_actions_bar_->SetOverflowRowWidth(width);
return GetPreferredSize().height();
}
gfx::Size BrowserActionsContainer::GetMinimumSize() const {
return gfx::Size(toolbar_actions_bar_->GetMinimumWidth(),
ToolbarActionsBar::IconHeight());
}
void BrowserActionsContainer::Layout() {
if (toolbar_actions_bar_->suppress_layout())
return;
if (toolbar_action_views_.empty()) {
SetVisible(false);
return;
}
SetVisible(true);
if (resize_area_)
resize_area_->SetBounds(0, 0, platform_settings().item_spacing, height());
// The range of visible icons, from start_index (inclusive) to end_index
// (exclusive).
size_t start_index = toolbar_actions_bar_->GetStartIndexInBounds();
size_t end_index = toolbar_actions_bar_->GetEndIndexInBounds();
// Now draw the icons for the actions in the available space. Once all the
// variables are in place, the layout works equally well for the main and
// overflow container.
for (size_t i = 0u; i < toolbar_action_views_.size(); ++i) {
ToolbarActionView* view = toolbar_action_views_[i].get();
if (i < start_index || i >= end_index) {
view->SetVisible(false);
} else {
view->SetBoundsRect(toolbar_actions_bar_->GetFrameForIndex(i));
view->SetVisible(true);
}
}
}
bool BrowserActionsContainer::GetDropFormats(
int* formats,
std::set<ui::Clipboard::FormatType>* format_types) {
return BrowserActionDragData::GetDropFormats(format_types);
}
bool BrowserActionsContainer::AreDropTypesRequired() {
return BrowserActionDragData::AreDropTypesRequired();
}
bool BrowserActionsContainer::CanDrop(const OSExchangeData& data) {
return BrowserActionDragData::CanDrop(data, browser_->profile());
}
int BrowserActionsContainer::OnDragUpdated(
const ui::DropTargetEvent& event) {
size_t row_index = 0;
size_t before_icon_in_row = 0;
// If there are no visible actions (such as when dragging an icon to an empty
// overflow/main container), then 0, 0 for row, column is correct.
if (VisibleBrowserActions() != 0) {
// Figure out where to display the indicator. This is a complex calculation:
// First, we subtract out the padding to the left of the icon area. If
// we're right-to-left, we also mirror the event.x() so that our
// calculations are consistent with left-to-right.
int offset_into_icon_area =
GetMirroredXInView(event.x()) -
GetLayoutConstant(TOOLBAR_STANDARD_SPACING);
// Next, figure out what row we're on. This only matters for overflow mode,
// but the calculation is the same for both.
row_index = event.y() / ToolbarActionsBar::IconHeight();
// Sanity check - we should never be on a different row in the main
// container.
DCHECK(ShownInsideMenu() || row_index == 0);
// Next, we determine which icon to place the indicator in front of. We want
// to place the indicator in front of icon n when the cursor is between the
// midpoints of icons (n - 1) and n. To do this we take the offset into the
// icon area and transform it as follows:
//
// Real icon area:
// 0 a * b c
// | | | |
// |[IC|ON] [IC|ON] [IC|ON]
// We want to be before icon 0 for 0 < x <= a, icon 1 for a < x <= b, etc.
// Here the "*" represents the offset into the icon area, and since it's
// between a and b, we want to return "1".
//
// Transformed "icon area":
// 0 a * b c
// | | | |
// |[ICON] |[ICON] |[ICON] |
// If we shift both our offset and our divider points later by half an icon
// plus one spacing unit, then it becomes very easy to calculate how many
// divider points we've passed, because they're the multiples of "one icon
// plus padding".
int before_icon_unclamped =
(offset_into_icon_area + (ToolbarActionsBar::IconWidth(false) / 2) +
platform_settings().item_spacing) / ToolbarActionsBar::IconWidth(true);
// We need to figure out how many icons are visible on the relevant row.
// In the main container, this will just be the visible actions.
int visible_icons_on_row = VisibleBrowserActionsAfterAnimation();
if (ShownInsideMenu()) {
int icons_per_row = platform_settings().icons_per_overflow_menu_row;
// If this is the final row of the overflow, then this is the remainder of
// visible icons. Otherwise, it's a full row (kIconsPerRow).
visible_icons_on_row =
row_index ==
static_cast<size_t>(visible_icons_on_row / icons_per_row) ?
visible_icons_on_row % icons_per_row : icons_per_row;
}
// Because the user can drag outside the container bounds, we need to clamp
// to the valid range. Note that the maximum allowable value is (num icons),
// not (num icons - 1), because we represent the indicator being past the
// last icon as being "before the (last + 1) icon".
before_icon_in_row =
std::min(std::max(before_icon_unclamped, 0), visible_icons_on_row);
}
if (!drop_position_.get() ||
!(drop_position_->row == row_index &&
drop_position_->icon_in_row == before_icon_in_row)) {
drop_position_.reset(new DropPosition(row_index, before_icon_in_row));
SchedulePaint();
}
return ui::DragDropTypes::DRAG_MOVE;
}
void BrowserActionsContainer::OnDragExited() {
drop_position_.reset();
SchedulePaint();
}
int BrowserActionsContainer::OnPerformDrop(
const ui::DropTargetEvent& event) {
BrowserActionDragData data;
if (!data.Read(event.data()))
return ui::DragDropTypes::DRAG_NONE;
// Make sure we have the same view as we started with.
DCHECK_EQ(GetIdAt(data.index()), data.id());
size_t i = drop_position_->row *
platform_settings().icons_per_overflow_menu_row +
drop_position_->icon_in_row;
if (ShownInsideMenu())
i += main_container_->VisibleBrowserActionsAfterAnimation();
// |i| now points to the item to the right of the drop indicator*, which is
// correct when dragging an icon to the left. When dragging to the right,
// however, we want the icon being dragged to get the index of the item to
// the left of the drop indicator, so we subtract one.
// * Well, it can also point to the end, but not when dragging to the left. :)
if (i > data.index())
--i;
ToolbarActionsBar::DragType drag_type = ToolbarActionsBar::DRAG_TO_SAME;
if (!toolbar_action_views_[data.index()]->visible())
drag_type = ShownInsideMenu() ? ToolbarActionsBar::DRAG_TO_OVERFLOW :
ToolbarActionsBar::DRAG_TO_MAIN;
toolbar_actions_bar_->OnDragDrop(data.index(), i, drag_type);
OnDragExited(); // Perform clean up after dragging.
return ui::DragDropTypes::DRAG_MOVE;
}
void BrowserActionsContainer::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->role = ui::AX_ROLE_GROUP;
node_data->SetName(l10n_util::GetStringUTF8(IDS_ACCNAME_EXTENSIONS));
}
void BrowserActionsContainer::WriteDragDataForView(View* sender,
const gfx::Point& press_pt,
OSExchangeData* data) {
toolbar_actions_bar_->OnDragStarted();
DCHECK(data);
auto it =
std::find_if(toolbar_action_views_.cbegin(), toolbar_action_views_.cend(),
[sender](const std::unique_ptr<ToolbarActionView>& ptr) {
return ptr.get() == sender;
});
DCHECK(it != toolbar_action_views_.cend());
ToolbarActionViewController* view_controller = (*it)->view_controller();
gfx::Size size(ToolbarActionsBar::IconWidth(false),
ToolbarActionsBar::IconHeight());
drag_utils::SetDragImageOnDataObject(
view_controller->GetIcon(GetCurrentWebContents(), size).AsImageSkia(),
press_pt.OffsetFromOrigin(),
data);
// Fill in the remaining info.
BrowserActionDragData drag_data(view_controller->GetId(),
it - toolbar_action_views_.cbegin());
drag_data.Write(browser_->profile(), data);
}
int BrowserActionsContainer::GetDragOperationsForView(View* sender,
const gfx::Point& p) {
return ui::DragDropTypes::DRAG_MOVE;
}
bool BrowserActionsContainer::CanStartDragForView(View* sender,
const gfx::Point& press_pt,
const gfx::Point& p) {
// We don't allow dragging while we're highlighting.
return !toolbar_actions_bar_->is_highlighting();
}
void BrowserActionsContainer::OnResize(int resize_amount, bool done_resizing) {
// We don't allow resize while the toolbar is highlighting a subset of
// actions, since this is a temporary and entirely browser-driven sequence in
// order to warn the user about potentially dangerous items.
// We also don't allow resize when the bar is already animating, since we
// don't want two competing size changes.
if (toolbar_actions_bar_->is_highlighting() || animating())
return;
// If this is the start of the resize gesture, initialize the starting
// width.
if (resize_starting_width_ == -1)
resize_starting_width_ = width();
if (!done_resizing) {
resize_amount_ = resize_amount;
parent()->Layout();
return;
}
// Up until now we've only been modifying the resize_amount, but now it is
// time to set the container size to the size we have resized to, and then
// animate to the nearest icon count size if necessary (which may be 0).
int ending_width =
std::min(std::max(toolbar_actions_bar_->GetMinimumWidth(),
resize_starting_width_ - resize_amount),
toolbar_actions_bar_->GetMaximumWidth());
resize_starting_width_ = -1;
toolbar_actions_bar_->OnResizeComplete(ending_width);
}
void BrowserActionsContainer::AnimationProgressed(
const gfx::Animation* animation) {
DCHECK_EQ(resize_animation_.get(), animation);
resize_amount_ = static_cast<int>(resize_animation_->GetCurrentValue() *
(resize_starting_width_ - animation_target_size_));
parent()->Layout();
}
void BrowserActionsContainer::AnimationCanceled(
const gfx::Animation* animation) {
AnimationEnded(animation);
}
void BrowserActionsContainer::AnimationEnded(const gfx::Animation* animation) {
animation_target_size_ = 0;
resize_amount_ = 0;
resize_starting_width_ = -1;
parent()->Layout();
toolbar_actions_bar_->OnAnimationEnded();
}
content::WebContents* BrowserActionsContainer::GetCurrentWebContents() {
return browser_->tab_strip_model()->GetActiveWebContents();
}
void BrowserActionsContainer::OnPaint(gfx::Canvas* canvas) {
// If the views haven't been initialized yet, wait for the next call to
// paint (one will be triggered by entering highlight mode).
if (toolbar_actions_bar_->is_highlighting() &&
!toolbar_action_views_.empty() && !ShownInsideMenu()) {
ToolbarActionsModel::HighlightType highlight_type =
toolbar_actions_bar_->highlight_type();
views::Painter* painter =
highlight_type == ToolbarActionsModel::HIGHLIGHT_INFO
? info_highlight_painter_.get()
: warning_highlight_painter_.get();
views::Painter::PaintPainterAt(canvas, painter, GetLocalBounds());
}
// TODO(sky/glen): Instead of using a drop indicator, animate the icons while
// dragging (like we do for tab dragging).
if (drop_position_.get()) {
// The two-pixel width drop indicator.
static const int kDropIndicatorWidth = 2;
// Convert back to a pixel offset into the container. First find the X
// coordinate of the drop icon.
const int drop_icon_x = GetLayoutConstant(TOOLBAR_STANDARD_SPACING) +
(drop_position_->icon_in_row * ToolbarActionsBar::IconWidth(true));
// Next, find the space before the drop icon.
const int space_before_drop_icon = platform_settings().item_spacing;
// Now place the drop indicator halfway between this and the end of the
// previous icon. If there is an odd amount of available space between the
// two icons (or the icon and the address bar) after subtracting the drop
// indicator width, this calculation puts the extra pixel on the left side
// of the indicator, since when the indicator is between the address bar and
// the first icon, it looks better closer to the icon.
const int drop_indicator_x = drop_icon_x -
((space_before_drop_icon + kDropIndicatorWidth) / 2);
const int row_height = ToolbarActionsBar::IconHeight();
const int drop_indicator_y = row_height * drop_position_->row;
gfx::Rect indicator_bounds(drop_indicator_x,
drop_indicator_y,
kDropIndicatorWidth,
row_height);
indicator_bounds.set_x(GetMirroredXForRect(indicator_bounds));
// Color of the drop indicator.
static const SkColor kDropIndicatorColor = SK_ColorBLACK;
canvas->FillRect(indicator_bounds, kDropIndicatorColor);
}
}
void BrowserActionsContainer::ViewHierarchyChanged(
const ViewHierarchyChangedDetails& details) {
if (!toolbar_actions_bar_->enabled())
return;
if (details.is_add && details.child == this) {
// Initial toolbar button creation and placement in the widget hierarchy.
// We do this here instead of in the constructor because adding views
// calls Layout on the Toolbar, which needs this object to be constructed
// before its Layout function is called.
toolbar_actions_bar_->CreateActions();
added_to_view_ = true;
}
}
void BrowserActionsContainer::ClearActiveBubble(views::Widget* widget) {
DCHECK(active_bubble_);
DCHECK_EQ(active_bubble_->GetWidget(), widget);
widget->RemoveObserver(this);
active_bubble_ = nullptr;
toolbar_actions_bar_->OnBubbleClosed();
}