blob: e77f642ee92b72ef6fedff42270f1af0f8d25850 [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 "base/compiler_specific.h"
#include "base/stl_util.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/extension_view_host.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_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/extensions/extension_keybinding_registry_views.h"
#include "chrome/browser/ui/views/extensions/extension_popup.h"
#include "chrome/browser/ui/views/toolbar/browser_actions_container_observer.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/common/extensions/command.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/runtime_data.h"
#include "extensions/common/feature_switch.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "grit/ui_resources.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/ax_view_state.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/base/theme_provider.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/controls/button/label_button_border.h"
#include "ui/views/controls/button/menu_button.h"
#include "ui/views/controls/resize_area.h"
#include "ui/views/metrics.h"
#include "ui/views/painter.h"
#include "ui/views/widget/widget.h"
using extensions::Extension;
namespace {
// Horizontal spacing between most items in the container, as well as after the
// last item or chevron (if visible).
const int kItemSpacing = ToolbarView::kStandardSpacing;
// Horizontal spacing before the chevron (if visible).
const int kChevronSpacing = kItemSpacing - 2;
// The maximum number of icons to show per row when in overflow mode (showing
// icons in the application menu).
// TODO(devlin): Compute the right number of icons to show, depending on the
// menu width.
#if defined(OS_LINUX)
const int kIconsPerMenuRow = 8; // The menu on Linux is wider.
#else
const int kIconsPerMenuRow = 7;
#endif
// A version of MenuButton with almost empty insets to fit properly on the
// toolbar.
class ChevronMenuButton : public views::MenuButton {
public:
ChevronMenuButton(views::ButtonListener* listener,
const base::string16& text,
views::MenuButtonListener* menu_button_listener,
bool show_menu_marker)
: views::MenuButton(listener,
text,
menu_button_listener,
show_menu_marker) {
}
virtual ~ChevronMenuButton() {}
virtual scoped_ptr<views::LabelButtonBorder> CreateDefaultBorder() const
OVERRIDE {
// The chevron resource was designed to not have any insets.
scoped_ptr<views::LabelButtonBorder> border =
views::MenuButton::CreateDefaultBorder();
border->set_insets(gfx::Insets());
return border.Pass();
}
private:
DISALLOW_COPY_AND_ASSIGN(ChevronMenuButton);
};
} // 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
// static
bool BrowserActionsContainer::disable_animations_during_testing_ = false;
BrowserActionsContainer::BrowserActionsContainer(
Browser* browser,
View* owner_view,
BrowserActionsContainer* main_container)
: profile_(browser->profile()),
browser_(browser),
owner_view_(owner_view),
main_container_(main_container),
popup_owner_(NULL),
model_(NULL),
container_width_(0),
resize_area_(NULL),
chevron_(NULL),
overflow_menu_(NULL),
suppress_chevron_(false),
resize_amount_(0),
animation_target_size_(0),
task_factory_(this),
show_menu_task_factory_(this) {
set_id(VIEW_ID_BROWSER_ACTION_TOOLBAR);
model_ = extensions::ExtensionToolbarModel::Get(browser->profile());
if (model_)
model_->AddObserver(this);
bool overflow_experiment =
extensions::FeatureSwitch::extension_action_redesign()->IsEnabled();
DCHECK(!in_overflow_mode() || overflow_experiment);
if (!in_overflow_mode()) {
extension_keybinding_registry_.reset(new ExtensionKeybindingRegistryViews(
browser->profile(),
owner_view->GetFocusManager(),
extensions::ExtensionKeybindingRegistry::ALL_EXTENSIONS,
this));
resize_animation_.reset(new gfx::SlideAnimation(this));
resize_area_ = new views::ResizeArea(this);
AddChildView(resize_area_);
// 'Main' mode doesn't need a chevron overflow when overflow is shown inside
// the Chrome menu.
if (!overflow_experiment) {
chevron_ = new ChevronMenuButton(NULL, base::string16(), this, false);
chevron_->EnableCanvasFlippingForRTLUI(true);
chevron_->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS_CHEVRON));
chevron_->SetVisible(false);
AddChildView(chevron_);
}
}
}
BrowserActionsContainer::~BrowserActionsContainer() {
FOR_EACH_OBSERVER(BrowserActionsContainerObserver,
observers_,
OnBrowserActionsContainerDestroyed());
if (overflow_menu_)
overflow_menu_->set_observer(NULL);
if (model_)
model_->RemoveObserver(this);
StopShowFolderDropMenuTimer();
HideActivePopup();
DeleteBrowserActionViews();
}
void BrowserActionsContainer::Init() {
LoadImages();
// We wait to set the container width until now so that the chevron images
// will be loaded. The width calculation needs to know the chevron size.
if (model_ && model_->extensions_initialized())
SetContainerWidth();
}
BrowserActionView* BrowserActionsContainer::GetBrowserActionView(
ExtensionAction* action) {
for (BrowserActionViews::iterator i(browser_action_views_.begin());
i != browser_action_views_.end(); ++i) {
if ((*i)->extension_action() == action)
return *i;
}
return NULL;
}
void BrowserActionsContainer::RefreshBrowserActionViews() {
for (size_t i = 0; i < browser_action_views_.size(); ++i)
browser_action_views_[i]->UpdateState();
}
void BrowserActionsContainer::CreateBrowserActionViews() {
DCHECK(browser_action_views_.empty());
if (!model_)
return;
const extensions::ExtensionList& toolbar_items = model_->toolbar_items();
for (extensions::ExtensionList::const_iterator i(toolbar_items.begin());
i != toolbar_items.end(); ++i) {
if (!ShouldDisplayBrowserAction(i->get()))
continue;
BrowserActionView* view = new BrowserActionView(i->get(), browser_, this);
browser_action_views_.push_back(view);
AddChildView(view);
}
}
void BrowserActionsContainer::DeleteBrowserActionViews() {
HideActivePopup();
if (overflow_menu_)
overflow_menu_->NotifyBrowserActionViewsDeleting();
STLDeleteElements(&browser_action_views_);
}
size_t BrowserActionsContainer::VisibleBrowserActions() const {
size_t visible_actions = 0;
for (size_t i = 0; i < browser_action_views_.size(); ++i) {
if (browser_action_views_[i]->visible())
++visible_actions;
}
return visible_actions;
}
size_t BrowserActionsContainer::VisibleBrowserActionsAfterAnimation() const {
if (!animating())
return VisibleBrowserActions();
return WidthToIconCount(animation_target_size_);
}
void BrowserActionsContainer::ExecuteExtensionCommand(
const extensions::Extension* extension,
const extensions::Command& command) {
// Global commands are handled by the ExtensionCommandsGlobalRegistry
// instance.
DCHECK(!command.global());
extension_keybinding_registry_->ExecuteCommand(extension->id(),
command.accelerator());
}
bool BrowserActionsContainer::ShownInsideMenu() const {
return in_overflow_mode();
}
void BrowserActionsContainer::OnBrowserActionViewDragDone() {
// We notify here as well as in OnPerformDrop because the dragged view is
// removed in OnPerformDrop, so it will never get its OnDragDone() call.
// TODO(devlin): we should see about fixing that.
FOR_EACH_OBSERVER(BrowserActionsContainerObserver,
observers_,
OnBrowserActionDragDone());
}
views::View* BrowserActionsContainer::GetOverflowReferenceView() {
// We should only need an overflow reference when using the traditional
// chevron overflow.
DCHECK(chevron_);
return chevron_;
}
void BrowserActionsContainer::SetPopupOwner(BrowserActionView* popup_owner) {
// We should never be setting a popup owner when one already exists.
DCHECK(!popup_owner_ || !popup_owner);
popup_owner_ = popup_owner;
}
void BrowserActionsContainer::HideActivePopup() {
if (popup_owner_)
popup_owner_->view_controller()->HidePopup();
}
void BrowserActionsContainer::AddObserver(
BrowserActionsContainerObserver* observer) {
observers_.AddObserver(observer);
}
void BrowserActionsContainer::RemoveObserver(
BrowserActionsContainerObserver* observer) {
observers_.RemoveObserver(observer);
}
gfx::Size BrowserActionsContainer::GetPreferredSize() const {
size_t icon_count = browser_action_views_.size() -
(in_overflow_mode() ? main_container_->VisibleBrowserActions() : 0);
// If there are no actions to show, or we are in overflow mode and the main
// container is already showing them all, then no further work is required.
if (icon_count == 0)
return gfx::Size();
if (in_overflow_mode()) {
// When in overflow, y is multiline, so the pixel count is IconHeight()
// times the number of rows needed.
return gfx::Size(
IconCountToWidth(kIconsPerMenuRow, false),
(((icon_count - 1) / kIconsPerMenuRow) + 1) * IconHeight());
}
// We calculate the size of the view by taking the current width and
// subtracting resize_amount_ (the latter represents how far the user is
// resizing the view or, if animating the snapping, how far to animate it).
// But we also clamp it to a minimum size and the maximum size, so that the
// container can never shrink too far or take up more space than it needs.
// In other words: MinimumNonemptyWidth() < width() - resize < ClampTo(MAX).
int preferred_width = std::min(
std::max(MinimumNonemptyWidth(), container_width_ - resize_amount_),
IconCountToWidth(-1, false));
return gfx::Size(preferred_width, IconHeight());
}
gfx::Size BrowserActionsContainer::GetMinimumSize() const {
int min_width = std::min(MinimumNonemptyWidth(), IconCountToWidth(-1, false));
return gfx::Size(min_width, IconHeight());
}
void BrowserActionsContainer::Layout() {
if (browser_action_views_.empty()) {
SetVisible(false);
return;
}
SetVisible(true);
if (resize_area_)
resize_area_->SetBounds(0, 0, kItemSpacing, height());
// If the icons don't all fit, show the chevron (unless suppressed).
int max_x = GetPreferredSize().width();
if ((IconCountToWidth(-1, false) > max_x) && !suppress_chevron_ && chevron_) {
chevron_->SetVisible(true);
gfx::Size chevron_size(chevron_->GetPreferredSize());
max_x -=
ToolbarView::kStandardSpacing + chevron_size.width() + kChevronSpacing;
chevron_->SetBounds(
width() - ToolbarView::kStandardSpacing - chevron_size.width(),
0,
chevron_size.width(),
chevron_size.height());
} else if (chevron_) {
chevron_->SetVisible(false);
}
// Now draw the icons for the browser actions in the available space.
int icon_width = IconWidth(false);
if (in_overflow_mode()) {
for (size_t i = 0;
i < main_container_->VisibleBrowserActionsAfterAnimation(); ++i) {
// Ensure that any browser actions shown in the main view are hidden in
// the overflow view.
browser_action_views_[i]->SetVisible(false);
}
for (size_t i = main_container_->VisibleBrowserActionsAfterAnimation();
i < browser_action_views_.size(); ++i) {
BrowserActionView* view = browser_action_views_[i];
size_t index = i - main_container_->VisibleBrowserActionsAfterAnimation();
int row_index = static_cast<int>(index) / kIconsPerMenuRow;
int x = kItemSpacing + (index * IconWidth(true)) -
(row_index * IconWidth(true) * kIconsPerMenuRow);
gfx::Rect rect_bounds(
x, IconHeight() * row_index, icon_width, IconHeight());
view->SetBoundsRect(rect_bounds);
view->SetVisible(true);
}
} else {
for (BrowserActionViews::const_iterator it = browser_action_views_.begin();
it < browser_action_views_.end(); ++it) {
BrowserActionView* view = *it;
int x = ToolbarView::kStandardSpacing +
((it - browser_action_views_.begin()) * IconWidth(true));
view->SetVisible(x + icon_width <= max_x);
if (view->visible())
view->SetBounds(x, 0, icon_width, IconHeight());
}
}
}
bool BrowserActionsContainer::GetDropFormats(
int* formats,
std::set<OSExchangeData::CustomFormat>* custom_formats) {
return BrowserActionDragData::GetDropFormats(custom_formats);
}
bool BrowserActionsContainer::AreDropTypesRequired() {
return BrowserActionDragData::AreDropTypesRequired();
}
bool BrowserActionsContainer::CanDrop(const OSExchangeData& data) {
return BrowserActionDragData::CanDrop(data, profile_);
}
void BrowserActionsContainer::OnDragEntered(
const ui::DropTargetEvent& event) {
}
int BrowserActionsContainer::OnDragUpdated(
const ui::DropTargetEvent& event) {
// First check if we are above the chevron (overflow) menu.
if (GetEventHandlerForPoint(event.location()) == chevron_) {
if (!show_menu_task_factory_.HasWeakPtrs() && !overflow_menu_)
StartShowFolderDropMenuTimer();
return ui::DragDropTypes::DRAG_MOVE;
}
StopShowFolderDropMenuTimer();
// Figure out where to display the indicator. This is a complex calculation:
// First, we figure out how much space is to the left of the icon area, so we
// can calculate the true offset into the icon area. The easiest way to do
// this is to just find where the first icon starts.
int width_before_icons =
browser_action_views_[GetFirstVisibleIconIndex()]->x();
// If we're right-to-left, we flip the mirror the event.x() so that our
// calculations are consistent with left-to-right.
int offset_into_icon_area =
GetMirroredXInView(event.x()) - width_before_icons;
// Next, figure out what row we're on. This only matters for overflow mode,
// but the calculation is the same for both.
size_t row_index = event.y() / IconHeight();
// Sanity check - we should never be on a different row in the main container.
DCHECK(in_overflow_mode() || 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 + (IconWidth(false) / 2) +
kItemSpacing) / 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 (in_overflow_mode()) {
// 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 / kIconsPerMenuRow) ?
visible_icons_on_row % kIconsPerMenuRow :
kIconsPerMenuRow;
}
// 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".
size_t 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() {
StopShowFolderDropMenuTimer();
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(browser_action_views_[data.index()]->extension()->id(),
data.id());
DCHECK(model_);
size_t i =
drop_position_->row * kIconsPerMenuRow + drop_position_->icon_in_row;
if (in_overflow_mode())
i += GetFirstVisibleIconIndex();
// |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;
if (profile_->IsOffTheRecord())
i = model_->IncognitoIndexToOriginal(i);
model_->MoveBrowserAction(
browser_action_views_[data.index()]->extension(), i);
OnDragExited(); // Perform clean up after dragging.
FOR_EACH_OBSERVER(BrowserActionsContainerObserver,
observers_,
OnBrowserActionDragDone());
return ui::DragDropTypes::DRAG_MOVE;
}
void BrowserActionsContainer::GetAccessibleState(
ui::AXViewState* state) {
state->role = ui::AX_ROLE_GROUP;
state->name = l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS);
}
void BrowserActionsContainer::OnMenuButtonClicked(views::View* source,
const gfx::Point& point) {
if (source == chevron_) {
overflow_menu_ =
new BrowserActionOverflowMenuController(this,
browser_,
chevron_,
browser_action_views_,
VisibleBrowserActions(),
false);
overflow_menu_->set_observer(this);
overflow_menu_->RunMenu(GetWidget());
}
}
void BrowserActionsContainer::WriteDragDataForView(View* sender,
const gfx::Point& press_pt,
OSExchangeData* data) {
DCHECK(data);
for (size_t i = 0; i < browser_action_views_.size(); ++i) {
BrowserActionView* view = browser_action_views_[i];
if (view == sender) {
// Set the dragging image for the icon.
gfx::ImageSkia badge(view->GetIconWithBadge());
drag_utils::SetDragImageOnDataObject(badge,
press_pt.OffsetFromOrigin(),
data);
// Fill in the remaining info.
BrowserActionDragData drag_data(view->extension()->id(), i);
drag_data.Write(profile_, data);
break;
}
}
}
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 !model_->is_highlighting();
}
void BrowserActionsContainer::OnResize(int resize_amount, bool done_resizing) {
if (!done_resizing) {
resize_amount_ = resize_amount;
OnBrowserActionVisibilityChanged();
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 max_width = IconCountToWidth(-1, false);
container_width_ =
std::min(std::max(0, container_width_ - resize_amount), max_width);
SaveDesiredSizeAndAnimate(gfx::Tween::EASE_OUT,
WidthToIconCount(container_width_));
}
void BrowserActionsContainer::AnimationProgressed(
const gfx::Animation* animation) {
DCHECK_EQ(resize_animation_.get(), animation);
resize_amount_ = static_cast<int>(resize_animation_->GetCurrentValue() *
(container_width_ - animation_target_size_));
OnBrowserActionVisibilityChanged();
}
void BrowserActionsContainer::AnimationEnded(const gfx::Animation* animation) {
container_width_ = animation_target_size_;
animation_target_size_ = 0;
resize_amount_ = 0;
suppress_chevron_ = false;
OnBrowserActionVisibilityChanged();
FOR_EACH_OBSERVER(BrowserActionsContainerObserver,
observers_,
OnBrowserActionsContainerAnimationEnded());
}
void BrowserActionsContainer::NotifyMenuDeleted(
BrowserActionOverflowMenuController* controller) {
DCHECK_EQ(overflow_menu_, controller);
overflow_menu_ = NULL;
}
content::WebContents* BrowserActionsContainer::GetCurrentWebContents() {
return browser_->tab_strip_model()->GetActiveWebContents();
}
void BrowserActionsContainer::OnBrowserActionVisibilityChanged() {
SetVisible(!browser_action_views_.empty());
if (owner_view_) {
owner_view_->Layout();
owner_view_->SchedulePaint();
}
}
extensions::ActiveTabPermissionGranter*
BrowserActionsContainer::GetActiveTabPermissionGranter() {
content::WebContents* web_contents =
browser_->tab_strip_model()->GetActiveWebContents();
if (!web_contents)
return NULL;
return extensions::TabHelper::FromWebContents(web_contents)->
active_tab_permission_granter();
}
void BrowserActionsContainer::MoveBrowserAction(const std::string& extension_id,
size_t new_index) {
const Extension* extension = extensions::ExtensionRegistry::Get(profile_)->
enabled_extensions().GetByID(extension_id);
model_->MoveBrowserAction(extension, new_index);
SchedulePaint();
}
size_t BrowserActionsContainer::GetFirstVisibleIconIndex() const {
return in_overflow_mode() ? model_->GetVisibleIconCount() : 0;
}
ExtensionPopup* BrowserActionsContainer::TestGetPopup() {
return popup_owner_ ? popup_owner_->view_controller()->popup() : NULL;
}
void BrowserActionsContainer::TestSetIconVisibilityCount(size_t icons) {
model_->SetVisibleIconCount(icons);
chevron_->SetVisible(icons < browser_action_views_.size());
container_width_ = IconCountToWidth(icons, chevron_->visible());
Layout();
SchedulePaint();
}
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 (model_->is_highlighting() && !browser_action_views_.empty()) {
views::Painter::PaintPainterAt(
canvas, highlight_painter_.get(), 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.
int drop_icon_x = browser_action_views_[GetFirstVisibleIconIndex()]->x() +
(drop_position_->icon_in_row * IconWidth(true));
// Next, find the space before the drop icon. This will either be
// kItemSpacing or ToolbarView::kStandardSpacing, depending on whether this
// is the first icon.
// NOTE: Right now, these are the same. But let's do this right for if they
// ever aren't.
int space_before_drop_icon = drop_position_->icon_in_row == 0 ?
ToolbarView::kStandardSpacing : kItemSpacing;
// 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.
int drop_indicator_x = drop_icon_x -
((space_before_drop_icon + kDropIndicatorWidth) / 2);
int row_height = IconHeight();
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::OnThemeChanged() {
LoadImages();
}
void BrowserActionsContainer::ViewHierarchyChanged(
const ViewHierarchyChangedDetails& details) {
// No extensions (e.g., incognito).
if (!model_)
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 AddBrowserAction
// calls Layout on the Toolbar, which needs this object to be constructed
// before its Layout function is called.
CreateBrowserActionViews();
}
}
// static
int BrowserActionsContainer::IconWidth(bool include_padding) {
static bool initialized = false;
static int icon_width = 0;
if (!initialized) {
initialized = true;
icon_width = ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
IDR_BROWSER_ACTION)->width();
}
return icon_width + (include_padding ? kItemSpacing : 0);
}
// static
int BrowserActionsContainer::IconHeight() {
static bool initialized = false;
static int icon_height = 0;
if (!initialized) {
initialized = true;
icon_height = ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
IDR_BROWSER_ACTION)->height();
}
return icon_height;
}
void BrowserActionsContainer::BrowserActionAdded(const Extension* extension,
int index) {
#if defined(DEBUG)
for (size_t i = 0; i < browser_action_views_.size(); ++i) {
DCHECK(browser_action_views_[i]->extension() != extension) <<
"Asked to add a browser action view for an extension that already "
"exists.";
}
#endif
CloseOverflowMenu();
if (!ShouldDisplayBrowserAction(extension))
return;
size_t visible_actions = VisibleBrowserActionsAfterAnimation();
// Add the new browser action to the vector and the view hierarchy.
if (profile_->IsOffTheRecord())
index = model_->OriginalIndexToIncognito(index);
BrowserActionView* view = new BrowserActionView(extension, browser_, this);
browser_action_views_.insert(browser_action_views_.begin() + index, view);
AddChildViewAt(view, index);
// If we are still initializing the container, don't bother animating.
if (!model_->extensions_initialized())
return;
// Enlarge the container if it was already at maximum size and we're not in
// the middle of upgrading.
if ((model_->GetVisibleIconCount() < 0) &&
!extensions::ExtensionSystem::Get(profile_)->runtime_data()->
IsBeingUpgraded(extension)) {
suppress_chevron_ = true;
SaveDesiredSizeAndAnimate(gfx::Tween::LINEAR, visible_actions + 1);
} else {
// Just redraw the (possibly modified) visible icon set.
OnBrowserActionVisibilityChanged();
}
}
void BrowserActionsContainer::BrowserActionRemoved(const Extension* extension) {
CloseOverflowMenu();
size_t visible_actions = VisibleBrowserActionsAfterAnimation();
for (BrowserActionViews::iterator i(browser_action_views_.begin());
i != browser_action_views_.end(); ++i) {
if ((*i)->extension() == extension) {
delete *i;
browser_action_views_.erase(i);
// If the extension is being upgraded we don't want the bar to shrink
// because the icon is just going to get re-added to the same location.
if (extensions::ExtensionSystem::Get(profile_)->runtime_data()->
IsBeingUpgraded(extension))
return;
if (browser_action_views_.size() > visible_actions) {
// If we have more icons than we can show, then we must not be changing
// the container size (since we either removed an icon from the main
// area and one from the overflow list will have shifted in, or we
// removed an entry directly from the overflow list).
OnBrowserActionVisibilityChanged();
} else {
// Either we went from overflow to no-overflow, or we shrunk the no-
// overflow container by 1. Either way the size changed, so animate.
if (chevron_)
chevron_->SetVisible(false);
SaveDesiredSizeAndAnimate(gfx::Tween::EASE_OUT,
browser_action_views_.size());
}
return; // We have found the action to remove, bail out.
}
}
}
void BrowserActionsContainer::BrowserActionMoved(const Extension* extension,
int index) {
if (!ShouldDisplayBrowserAction(extension))
return;
if (profile_->IsOffTheRecord())
index = model_->OriginalIndexToIncognito(index);
DCHECK(index >= 0 && index < static_cast<int>(browser_action_views_.size()));
DeleteBrowserActionViews();
CreateBrowserActionViews();
Layout();
SchedulePaint();
}
bool BrowserActionsContainer::BrowserActionShowPopup(
const Extension* extension) {
return ShowPopupForExtension(extension, false, false);
}
void BrowserActionsContainer::VisibleCountChanged() {
SetContainerWidth();
}
void BrowserActionsContainer::HighlightModeChanged(bool is_highlighting) {
// The visual highlighting is done in OnPaint(). It's a bit of a pain that
// we delete and recreate everything here, but that's how it's done in
// BrowserActionMoved(), too. If we want to optimize it, we could move the
// existing icons, instead of deleting it all.
DeleteBrowserActionViews();
CreateBrowserActionViews();
SaveDesiredSizeAndAnimate(gfx::Tween::LINEAR, browser_action_views_.size());
}
void BrowserActionsContainer::LoadImages() {
ui::ThemeProvider* tp = GetThemeProvider();
if (!tp || !chevron_)
return;
chevron_->SetImage(views::Button::STATE_NORMAL,
*tp->GetImageSkiaNamed(IDR_BROWSER_ACTIONS_OVERFLOW));
const int kImages[] = IMAGE_GRID(IDR_DEVELOPER_MODE_HIGHLIGHT);
highlight_painter_.reset(views::Painter::CreateImageGridPainter(kImages));
}
void BrowserActionsContainer::SetContainerWidth() {
// The slave only draws the overflow (what isn't visible in the other
// container).
int visible_actions = in_overflow_mode() ?
model_->toolbar_items().size() - model_->GetVisibleIconCount() :
model_->GetVisibleIconCount();
if (visible_actions < 0) // All icons should be visible.
visible_actions = model_->toolbar_items().size();
if (chevron_) {
chevron_->SetVisible(
static_cast<size_t>(visible_actions) < model_->toolbar_items().size());
}
container_width_ =
IconCountToWidth(visible_actions, chevron_ && chevron_->visible());
}
void BrowserActionsContainer::CloseOverflowMenu() {
if (overflow_menu_)
overflow_menu_->CancelMenu();
}
void BrowserActionsContainer::StopShowFolderDropMenuTimer() {
show_menu_task_factory_.InvalidateWeakPtrs();
}
void BrowserActionsContainer::StartShowFolderDropMenuTimer() {
base::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
base::Bind(&BrowserActionsContainer::ShowDropFolder,
show_menu_task_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(views::GetMenuShowDelay()));
}
void BrowserActionsContainer::ShowDropFolder() {
DCHECK(!overflow_menu_);
drop_position_.reset();
overflow_menu_ =
new BrowserActionOverflowMenuController(this,
browser_,
chevron_,
browser_action_views_,
VisibleBrowserActions(),
true);
overflow_menu_->set_observer(this);
overflow_menu_->RunMenu(GetWidget());
}
int BrowserActionsContainer::IconCountToWidth(int icons,
bool display_chevron) const {
if (icons < 0)
icons = browser_action_views_.size();
if ((icons == 0) && !display_chevron)
return ToolbarView::kStandardSpacing;
int icons_size =
(icons == 0) ? 0 : ((icons * IconWidth(true)) - kItemSpacing);
int chevron_size = chevron_ && display_chevron ?
(kChevronSpacing + chevron_->GetPreferredSize().width()) : 0;
return ToolbarView::kStandardSpacing + icons_size + chevron_size +
ToolbarView::kStandardSpacing;
}
size_t BrowserActionsContainer::WidthToIconCount(int pixels) const {
// Check for widths large enough to show the entire icon set.
if (pixels >= IconCountToWidth(-1, false))
return browser_action_views_.size();
// We need to reserve space for the resize area, chevron, and the spacing on
// either side of the chevron.
int available_space = pixels - ToolbarView::kStandardSpacing -
(chevron_ ? chevron_->GetPreferredSize().width() : 0) -
kChevronSpacing - ToolbarView::kStandardSpacing;
// Now we add an extra between-item padding value so the space can be divided
// evenly by (size of icon with padding).
return static_cast<size_t>(
std::max(0, available_space + kItemSpacing) / IconWidth(true));
}
int BrowserActionsContainer::MinimumNonemptyWidth() const {
if (!chevron_)
return ToolbarView::kStandardSpacing;
return (ToolbarView::kStandardSpacing * 2) + kChevronSpacing +
chevron_->GetPreferredSize().width();
}
void BrowserActionsContainer::SaveDesiredSizeAndAnimate(
gfx::Tween::Type tween_type,
size_t num_visible_icons) {
// Save off the desired number of visible icons. We do this now instead of at
// the end of the animation so that even if the browser is shut down while
// animating, the right value will be restored on next run.
// NOTE: Don't save the icon count in incognito because there may be fewer
// icons in that mode. The result is that the container in a normal window is
// always at least as wide as in an incognito window.
if (!profile_->IsOffTheRecord())
model_->SetVisibleIconCount(num_visible_icons);
int target_size = IconCountToWidth(num_visible_icons,
num_visible_icons < browser_action_views_.size());
if (resize_animation_ && !disable_animations_during_testing_) {
// 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_animation_->SetTweenType(tween_type);
animation_target_size_ = target_size;
resize_animation_->Show();
} else {
animation_target_size_ = target_size;
AnimationEnded(resize_animation_.get());
}
}
bool BrowserActionsContainer::ShouldDisplayBrowserAction(
const Extension* extension) {
// Only display incognito-enabled extensions while in incognito mode.
return !profile_->IsOffTheRecord() ||
extensions::util::IsIncognitoEnabled(extension->id(), profile_);
}
bool BrowserActionsContainer::ShowPopupForExtension(
const extensions::Extension* extension,
bool grant_tab_permissions,
bool can_override) {
// If the popup cannot override other views, then no other popups can be
// showing, and it must be shown in the active widow with a visible toolbar.
// TODO(justinlin): Remove toolbar check when http://crbug.com/308645 is
// fixed.
if (!can_override &&
(popup_owner_ ||
!browser_->window()->IsActive() ||
!browser_->window()->IsToolbarVisible())) {
return false;
}
for (BrowserActionViews::iterator iter = browser_action_views_.begin();
iter != browser_action_views_.end(); ++iter) {
BrowserActionView* view = (*iter);
if (view->extension() == extension)
return view->view_controller()->ExecuteAction(
ExtensionPopup::SHOW, grant_tab_permissions);
}
return false;
}