// Copyright (c) 2011 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/browser_actions_container.h"
#include "base/stl_util-inl.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/extensions/extension_browser_event_router.h"
#include "chrome/browser/extensions/extension_host.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_tabs_module.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/detachable_toolbar_view.h"
#include "chrome/browser/ui/views/extensions/browser_action_drag_data.h"
#include "chrome/browser/ui/views/extensions/extension_popup.h"
#include "chrome/browser/ui/views/toolbar_view.h"
#include "chrome/common/extensions/extension_action.h"
#include "chrome/common/extensions/extension_resource.h"
#include "chrome/common/pref_names.h"
#include "content/browser/renderer_host/render_view_host.h"
#include "content/browser/renderer_host/render_widget_host_view.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "content/common/notification_source.h"
#include "content/common/notification_type.h"
#include "grit/app_resources.h"
#include "grit/generated_resources.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/effects/SkGradientShader.h"
#include "ui/base/accessibility/accessible_view_state.h"
#include "ui/base/animation/slide_animation.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/theme_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/canvas_skia.h"
#include "views/controls/button/menu_button.h"
#include "views/controls/button/text_button.h"
#include "views/controls/menu/menu_2.h"
#include "views/drag_utils.h"
#include "views/metrics.h"
#include "views/window/window.h"
#include "grit/theme_resources.h"
// Horizontal spacing between most items in the container, as well as after the
// last item or chevron (if visible).
static const int kItemSpacing = ToolbarView::kStandardSpacing;
// Horizontal spacing before the chevron (if visible).
static const int kChevronSpacing = kItemSpacing - 2;
// static
bool BrowserActionsContainer::disable_animations_during_testing_ = false;
////////////////////////////////////////////////////////////////////////////////
// BrowserActionButton
BrowserActionButton::BrowserActionButton(const Extension* extension,
BrowserActionsContainer* panel)
: ALLOW_THIS_IN_INITIALIZER_LIST(
MenuButton(this, std::wstring(), NULL, false)),
browser_action_(extension->browser_action()),
extension_(extension),
ALLOW_THIS_IN_INITIALIZER_LIST(tracker_(this)),
showing_context_menu_(false),
panel_(panel) {
set_border(NULL);
set_alignment(TextButton::ALIGN_CENTER);
// No UpdateState() here because View hierarchy not setup yet. Our parent
// should call UpdateState() after creation.
registrar_.Add(this, NotificationType::EXTENSION_BROWSER_ACTION_UPDATED,
Source<ExtensionAction>(browser_action_));
}
void BrowserActionButton::Destroy() {
if (showing_context_menu_) {
context_menu_menu_->CancelMenu();
MessageLoop::current()->DeleteSoon(FROM_HERE, this);
} else {
delete this;
}
}
void BrowserActionButton::ViewHierarchyChanged(
bool is_add, View* parent, View* child) {
if (is_add && child == this) {
// The Browser Action API does not allow the default icon path to be
// changed at runtime, so we can load this now and cache it.
std::string relative_path = browser_action_->default_icon_path();
if (relative_path.empty())
return;
// LoadImage is not guaranteed to be synchronous, so we might see the
// callback OnImageLoaded execute immediately. It (through UpdateState)
// expects parent() to return the owner for this button, so this
// function is as early as we can start this request.
tracker_.LoadImage(extension_, extension_->GetResource(relative_path),
gfx::Size(Extension::kBrowserActionIconMaxSize,
Extension::kBrowserActionIconMaxSize),
ImageLoadingTracker::DONT_CACHE);
}
MenuButton::ViewHierarchyChanged(is_add, parent, child);
}
void BrowserActionButton::ButtonPressed(views::Button* sender,
const views::Event& event) {
panel_->OnBrowserActionExecuted(this, false);
}
void BrowserActionButton::OnImageLoaded(SkBitmap* image,
const ExtensionResource& resource,
int index) {
if (image)
default_icon_ = *image;
// Call back to UpdateState() because a more specific icon might have been set
// while the load was outstanding.
UpdateState();
}
void BrowserActionButton::UpdateState() {
int tab_id = panel_->GetCurrentTabId();
if (tab_id < 0)
return;
SkBitmap icon(browser_action()->GetIcon(tab_id));
if (icon.isNull())
icon = default_icon_;
if (!icon.isNull()) {
SkPaint paint;
paint.setXfermode(SkXfermode::Create(SkXfermode::kSrcOver_Mode));
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
SkBitmap bg;
rb.GetBitmapNamed(IDR_BROWSER_ACTION)->copyTo(&bg,
SkBitmap::kARGB_8888_Config);
SkCanvas bg_canvas(bg);
bg_canvas.drawBitmap(icon, SkIntToScalar((bg.width() - icon.width()) / 2),
SkIntToScalar((bg.height() - icon.height()) / 2), &paint);
SetIcon(bg);
SkBitmap bg_h;
rb.GetBitmapNamed(IDR_BROWSER_ACTION_H)->copyTo(&bg_h,
SkBitmap::kARGB_8888_Config);
SkCanvas bg_h_canvas(bg_h);
bg_h_canvas.drawBitmap(icon,
SkIntToScalar((bg_h.width() - icon.width()) / 2),
SkIntToScalar((bg_h.height() - icon.height()) / 2), &paint);
SetHoverIcon(bg_h);
SkBitmap bg_p;
rb.GetBitmapNamed(IDR_BROWSER_ACTION_P)->copyTo(&bg_p,
SkBitmap::kARGB_8888_Config);
SkCanvas bg_p_canvas(bg_p);
bg_p_canvas.drawBitmap(icon,
SkIntToScalar((bg_p.width() - icon.width()) / 2),
SkIntToScalar((bg_p.height() - icon.height()) / 2), &paint);
SetPushedIcon(bg_p);
}
// If the browser action name is empty, show the extension name instead.
string16 name = UTF8ToUTF16(browser_action()->GetTitle(tab_id));
if (name.empty())
name = UTF8ToUTF16(extension()->name());
SetTooltipText(UTF16ToWideHack(name));
parent()->SchedulePaint();
}
bool BrowserActionButton::IsPopup() {
int tab_id = panel_->GetCurrentTabId();
return (tab_id < 0) ? false : browser_action_->HasPopup(tab_id);
}
GURL BrowserActionButton::GetPopupUrl() {
int tab_id = panel_->GetCurrentTabId();
return (tab_id < 0) ? GURL() : browser_action_->GetPopupUrl(tab_id);
}
void BrowserActionButton::Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
DCHECK(type == NotificationType::EXTENSION_BROWSER_ACTION_UPDATED);
UpdateState();
// The browser action may have become visible/hidden so we need to make
// sure the state gets updated.
panel_->OnBrowserActionVisibilityChanged();
}
bool BrowserActionButton::Activate() {
if (!IsPopup())
return true;
panel_->OnBrowserActionExecuted(this, false);
// TODO(erikkay): Run a nested modal loop while the mouse is down to
// enable menu-like drag-select behavior.
// The return value of this method is returned via OnMousePressed.
// We need to return false here since we're handing off focus to another
// widget/view, and true will grab it right back and try to send events
// to us.
return false;
}
bool BrowserActionButton::OnMousePressed(const views::MouseEvent& event) {
if (!event.IsRightMouseButton()) {
return IsPopup() ?
MenuButton::OnMousePressed(event) : TextButton::OnMousePressed(event);
}
// Get the top left point of this button in screen coordinates.
gfx::Point point = gfx::Point(0, 0);
ConvertPointToScreen(this, &point);
// Make the menu appear below the button.
point.Offset(0, height());
ShowContextMenu(point, true);
return false;
}
void BrowserActionButton::OnMouseReleased(const views::MouseEvent& event) {
if (IsPopup() || showing_context_menu_) {
// TODO(erikkay) this never actually gets called (probably because of the
// loss of focus).
MenuButton::OnMouseReleased(event);
} else {
TextButton::OnMouseReleased(event);
}
}
void BrowserActionButton::OnMouseExited(const views::MouseEvent& event) {
if (IsPopup() || showing_context_menu_)
MenuButton::OnMouseExited(event);
else
TextButton::OnMouseExited(event);
}
bool BrowserActionButton::OnKeyReleased(const views::KeyEvent& event) {
return IsPopup() ?
MenuButton::OnKeyReleased(event) : TextButton::OnKeyReleased(event);
}
void BrowserActionButton::ShowContextMenu(const gfx::Point& p,
bool is_mouse_gesture) {
if (!extension()->ShowConfigureContextMenus())
return;
showing_context_menu_ = true;
SetButtonPushed();
// Reconstructs the menu every time because the menu's contents are dynamic.
context_menu_contents_ =
new ExtensionContextMenuModel(extension(), panel_->browser(), panel_);
context_menu_menu_.reset(new views::Menu2(context_menu_contents_.get()));
context_menu_menu_->RunContextMenuAt(p);
SetButtonNotPushed();
showing_context_menu_ = false;
}
void BrowserActionButton::SetButtonPushed() {
SetState(views::CustomButton::BS_PUSHED);
menu_visible_ = true;
}
void BrowserActionButton::SetButtonNotPushed() {
SetState(views::CustomButton::BS_NORMAL);
menu_visible_ = false;
}
BrowserActionButton::~BrowserActionButton() {
}
////////////////////////////////////////////////////////////////////////////////
// BrowserActionView
BrowserActionView::BrowserActionView(const Extension* extension,
BrowserActionsContainer* panel)
: panel_(panel) {
button_ = new BrowserActionButton(extension, panel);
button_->SetDragController(panel_);
AddChildView(button_);
button_->UpdateState();
}
BrowserActionView::~BrowserActionView() {
RemoveChildView(button_);
button_->Destroy();
}
gfx::Canvas* BrowserActionView::GetIconWithBadge() {
int tab_id = panel_->GetCurrentTabId();
SkBitmap icon = button_->extension()->browser_action()->GetIcon(tab_id);
if (icon.isNull())
icon = button_->default_icon();
gfx::Canvas* canvas = new gfx::CanvasSkia(icon.width(), icon.height(), false);
canvas->DrawBitmapInt(icon, 0, 0);
if (tab_id >= 0) {
gfx::Rect bounds(icon.width(), icon.height() + ToolbarView::kVertSpacing);
button_->extension()->browser_action()->PaintBadge(canvas, bounds, tab_id);
}
return canvas;
}
void BrowserActionView::Layout() {
// We can't rely on button_->GetPreferredSize() here because that's not set
// correctly until the first call to
// BrowserActionsContainer::RefreshBrowserActionViews(), whereas this can be
// called before that when the initial bounds are set (and then not after,
// since the bounds don't change). So instead of setting the height from the
// button's preferred size, we use IconHeight(), since that's how big the
// button should be regardless of what it's displaying.
button_->SetBounds(0, ToolbarView::kVertSpacing, width(),
BrowserActionsContainer::IconHeight());
}
void BrowserActionView::GetAccessibleState(ui::AccessibleViewState* state) {
state->name = l10n_util::GetStringUTF16(
IDS_ACCNAME_EXTENSIONS_BROWSER_ACTION);
state->role = ui::AccessibilityTypes::ROLE_GROUPING;
}
void BrowserActionView::PaintChildren(gfx::Canvas* canvas) {
View::PaintChildren(canvas);
ExtensionAction* action = button()->browser_action();
int tab_id = panel_->GetCurrentTabId();
if (tab_id >= 0)
action->PaintBadge(canvas, gfx::Rect(width(), height()), tab_id);
}
////////////////////////////////////////////////////////////////////////////////
// BrowserActionsContainer
BrowserActionsContainer::BrowserActionsContainer(Browser* browser,
View* owner_view)
: profile_(browser->profile()),
browser_(browser),
owner_view_(owner_view),
popup_(NULL),
popup_button_(NULL),
model_(NULL),
container_width_(0),
chevron_(NULL),
overflow_menu_(NULL),
suppress_chevron_(false),
resize_amount_(0),
animation_target_size_(0),
drop_indicator_position_(-1),
ALLOW_THIS_IN_INITIALIZER_LIST(task_factory_(this)),
ALLOW_THIS_IN_INITIALIZER_LIST(show_menu_task_factory_(this)) {
SetID(VIEW_ID_BROWSER_ACTION_TOOLBAR);
if (profile_->GetExtensionService()) {
model_ = profile_->GetExtensionService()->toolbar_model();
model_->AddObserver(this);
}
resize_animation_.reset(new ui::SlideAnimation(this));
resize_area_ = new views::ResizeArea(this);
AddChildView(resize_area_);
chevron_ = new views::MenuButton(NULL, std::wstring(), this, false);
chevron_->set_border(NULL);
chevron_->EnableCanvasFlippingForRTLUI(true);
chevron_->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS_CHEVRON));
chevron_->SetVisible(false);
AddChildView(chevron_);
}
BrowserActionsContainer::~BrowserActionsContainer() {
if (model_)
model_->RemoveObserver(this);
StopShowFolderDropMenuTimer();
HidePopup();
DeleteBrowserActionViews();
}
// Static.
void BrowserActionsContainer::RegisterUserPrefs(PrefService* prefs) {
prefs->RegisterIntegerPref(prefs::kBrowserActionContainerWidth, 0);
}
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_ &&
!profile_->GetPrefs()->HasPrefPath(prefs::kExtensionToolbarSize)) {
// Migration code to the new VisibleIconCount pref.
// TODO(mpcomplete): remove this after users are upgraded to 5.0.
int predefined_width =
profile_->GetPrefs()->GetInteger(prefs::kBrowserActionContainerWidth);
if (predefined_width != 0)
model_->SetVisibleIconCount(WidthToIconCount(predefined_width));
}
if (model_ && model_->extensions_initialized())
SetContainerWidth();
}
int BrowserActionsContainer::GetCurrentTabId() const {
TabContents* tab_contents = browser_->GetSelectedTabContents();
return tab_contents ? tab_contents->controller().session_id().id() : -1;
}
BrowserActionView* BrowserActionsContainer::GetBrowserActionView(
ExtensionAction* action) {
for (BrowserActionViews::iterator iter = browser_action_views_.begin();
iter != browser_action_views_.end(); ++iter) {
if ((*iter)->button()->browser_action() == action)
return *iter;
}
return NULL;
}
void BrowserActionsContainer::RefreshBrowserActionViews() {
for (size_t i = 0; i < browser_action_views_.size(); ++i)
browser_action_views_[i]->button()->UpdateState();
}
void BrowserActionsContainer::CreateBrowserActionViews() {
DCHECK(browser_action_views_.empty());
if (!model_)
return;
for (ExtensionList::iterator iter = model_->begin(); iter != model_->end();
++iter) {
if (!ShouldDisplayBrowserAction(*iter))
continue;
BrowserActionView* view = new BrowserActionView(*iter, this);
browser_action_views_.push_back(view);
AddChildView(view);
}
}
void BrowserActionsContainer::DeleteBrowserActionViews() {
if (!browser_action_views_.empty()) {
for (size_t i = 0; i < browser_action_views_.size(); ++i)
RemoveChildView(browser_action_views_[i]);
STLDeleteContainerPointers(browser_action_views_.begin(),
browser_action_views_.end());
browser_action_views_.clear();
}
}
void BrowserActionsContainer::OnBrowserActionVisibilityChanged() {
SetVisible(!browser_action_views_.empty());
owner_view_->Layout();
owner_view_->SchedulePaint();
}
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]->IsVisible())
++visible_actions;
}
return visible_actions;
}
void BrowserActionsContainer::OnBrowserActionExecuted(
BrowserActionButton* button,
bool inspect_with_devtools) {
ExtensionAction* browser_action = button->browser_action();
// Popups just display. No notification to the extension.
// TODO(erikkay): should there be?
if (!button->IsPopup()) {
ExtensionService* service = profile_->GetExtensionService();
service->browser_event_router()->BrowserActionExecuted(
profile_, browser_action->extension_id(), browser_);
return;
}
// If we're showing the same popup, just hide it and return.
bool same_showing = popup_ && button == popup_button_;
// Always hide the current popup, even if it's not the same.
// Only one popup should be visible at a time.
HidePopup();
if (same_showing)
return;
// We can get the execute event for browser actions that are not visible,
// since buttons can be activated from the overflow menu (chevron). In that
// case we show the popup as originating from the chevron.
View* reference_view = button->parent()->IsVisible() ? button : chevron_;
gfx::Point origin;
View::ConvertPointToScreen(reference_view, &origin);
gfx::Rect rect = reference_view->bounds();
rect.set_origin(origin);
BubbleBorder::ArrowLocation arrow_location = base::i18n::IsRTL() ?
BubbleBorder::TOP_LEFT : BubbleBorder::TOP_RIGHT;
popup_ = ExtensionPopup::Show(button->GetPopupUrl(), browser_, rect,
arrow_location, inspect_with_devtools,
this);
popup_button_ = button;
popup_button_->SetButtonPushed();
}
gfx::Size BrowserActionsContainer::GetPreferredSize() {
if (browser_action_views_.empty())
return gfx::Size(ToolbarView::kStandardSpacing, 0);
// 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: ContainerMinSize() < width() - resize < ClampTo(MAX).
int clamped_width = std::min(
std::max(ContainerMinSize(), container_width_ - resize_amount_),
IconCountToWidth(-1, false));
return gfx::Size(clamped_width, 0);
}
void BrowserActionsContainer::Layout() {
if (browser_action_views_.empty()) {
SetVisible(false);
return;
}
SetVisible(true);
resize_area_->SetBounds(0, ToolbarView::kVertSpacing, kItemSpacing,
IconHeight());
// 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_->SetVisible(true);
gfx::Size chevron_size(chevron_->GetPreferredSize());
max_x -=
ToolbarView::kStandardSpacing + chevron_size.width() + kChevronSpacing;
chevron_->SetBounds(
width() - ToolbarView::kStandardSpacing - chevron_size.width(),
ToolbarView::kVertSpacing, chevron_size.width(), chevron_size.height());
} else {
chevron_->SetVisible(false);
}
// Now draw the icons for the browser actions in the available space.
int icon_width = IconWidth(false);
for (size_t i = 0; i < browser_action_views_.size(); ++i) {
BrowserActionView* view = browser_action_views_[i];
int x = ToolbarView::kStandardSpacing + (i * IconWidth(true));
if (x + icon_width <= max_x) {
view->SetBounds(x, 0, icon_width, height());
view->SetVisible(true);
} else {
view->SetVisible(false);
}
}
}
bool BrowserActionsContainer::GetDropFormats(
int* formats,
std::set<OSExchangeData::CustomFormat>* custom_formats) {
custom_formats->insert(BrowserActionDragData::GetBrowserActionCustomFormat());
return true;
}
bool BrowserActionsContainer::AreDropTypesRequired() {
return true;
}
bool BrowserActionsContainer::CanDrop(const OSExchangeData& data) {
BrowserActionDragData drop_data;
return drop_data.Read(data) ? drop_data.IsFromProfile(profile_) : false;
}
void BrowserActionsContainer::OnDragEntered(
const views::DropTargetEvent& event) {
}
int BrowserActionsContainer::OnDragUpdated(
const views::DropTargetEvent& event) {
// First check if we are above the chevron (overflow) menu.
if (GetEventHandlerForPoint(event.location()) == chevron_) {
if (show_menu_task_factory_.empty() && !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.
int width_before_icons = ToolbarView::kStandardSpacing +
(base::i18n::IsRTL() ?
(chevron_->GetPreferredSize().width() + kChevronSpacing) : 0);
int offset_into_icon_area = event.x() - width_before_icons;
// 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);
// 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".
int before_icon = std::min(std::max(before_icon_unclamped, 0),
static_cast<int>(VisibleBrowserActions()));
// Now we convert back to a pixel offset into the container. We want to place
// the center of the drop indicator at the midpoint of the space before our
// chosen icon.
SetDropIndicator(width_before_icons + (before_icon * IconWidth(true)) -
(kItemSpacing / 2));
return ui::DragDropTypes::DRAG_MOVE;
}
void BrowserActionsContainer::OnDragExited() {
StopShowFolderDropMenuTimer();
drop_indicator_position_ = -1;
SchedulePaint();
}
int BrowserActionsContainer::OnPerformDrop(
const views::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()]->button()->extension()->id(),
data.id());
DCHECK(model_);
size_t i = 0;
for (; i < browser_action_views_.size(); ++i) {
int view_x = browser_action_views_[i]->GetMirroredBounds().x();
if (!browser_action_views_[i]->IsVisible() ||
(base::i18n::IsRTL() ? (view_x < drop_indicator_position_) :
(view_x >= drop_indicator_position_))) {
// We have reached the end of the visible icons or found one that has a
// higher x position than the drop point.
break;
}
}
// |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()]->button()->extension(), i);
OnDragExited(); // Perform clean up after dragging.
return ui::DragDropTypes::DRAG_MOVE;
}
void BrowserActionsContainer::GetAccessibleState(
ui::AccessibleViewState* state) {
state->role = ui::AccessibilityTypes::ROLE_GROUPING;
state->name = l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS);
}
void BrowserActionsContainer::RunMenu(View* source, const gfx::Point& pt) {
if (source == chevron_) {
overflow_menu_ = new BrowserActionOverflowMenuController(
this, chevron_, browser_action_views_, VisibleBrowserActions());
overflow_menu_->set_observer(this);
overflow_menu_->RunMenu(GetWindow()->GetNativeWindow(), false);
}
}
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) {
BrowserActionButton* button = browser_action_views_[i]->button();
if (button == sender) {
// Set the dragging image for the icon.
scoped_ptr<gfx::Canvas> canvas(
browser_action_views_[i]->GetIconWithBadge());
drag_utils::SetDragImageOnDataObject(*canvas, button->size(), press_pt,
data);
// Fill in the remaining info.
BrowserActionDragData drag_data(
browser_action_views_[i]->button()->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) {
return true;
}
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(ui::Tween::EASE_OUT,
WidthToIconCount(container_width_));
}
void BrowserActionsContainer::AnimationProgressed(
const ui::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 ui::Animation* animation) {
container_width_ = animation_target_size_;
animation_target_size_ = 0;
resize_amount_ = 0;
OnBrowserActionVisibilityChanged();
suppress_chevron_ = false;
}
void BrowserActionsContainer::NotifyMenuDeleted(
BrowserActionOverflowMenuController* controller) {
DCHECK(controller == overflow_menu_);
overflow_menu_ = NULL;
}
void BrowserActionsContainer::InspectPopup(ExtensionAction* action) {
OnBrowserActionExecuted(GetBrowserActionView(action)->button(), true);
}
void BrowserActionsContainer::ExtensionPopupIsClosing(ExtensionPopup* popup) {
// ExtensionPopup is ref-counted, so we don't need to delete it.
DCHECK_EQ(popup_, popup);
popup_ = NULL;
popup_button_->SetButtonNotPushed();
popup_button_ = NULL;
}
void BrowserActionsContainer::MoveBrowserAction(const std::string& extension_id,
size_t new_index) {
ExtensionService* service = profile_->GetExtensionService();
if (service) {
const Extension* extension = service->GetExtensionById(extension_id, false);
model_->MoveBrowserAction(extension, new_index);
SchedulePaint();
}
}
void BrowserActionsContainer::HidePopup() {
if (popup_)
popup_->Close();
}
void BrowserActionsContainer::TestExecuteBrowserAction(int index) {
BrowserActionButton* button = browser_action_views_[index]->button();
OnBrowserActionExecuted(button, false);
}
void BrowserActionsContainer::TestSetIconVisibilityCount(size_t icons) {
model_->SetVisibleIconCount(icons);
chevron_->SetVisible(icons < browser_action_views_.size());
container_width_ = IconCountToWidth(icons, chevron_->IsVisible());
Layout();
SchedulePaint();
}
void BrowserActionsContainer::OnPaint(gfx::Canvas* canvas) {
// TODO(sky/glen): Instead of using a drop indicator, animate the icons while
// dragging (like we do for tab dragging).
if (drop_indicator_position_ > -1) {
// The two-pixel width drop indicator.
static const int kDropIndicatorWidth = 2;
gfx::Rect indicator_bounds(
drop_indicator_position_ - (kDropIndicatorWidth / 2),
ToolbarView::kVertSpacing, kDropIndicatorWidth, IconHeight());
// Color of the drop indicator.
static const SkColor kDropIndicatorColor = SK_ColorBLACK;
canvas->FillRectInt(kDropIndicatorColor, indicator_bounds.x(),
indicator_bounds.y(), indicator_bounds.width(),
indicator_bounds.height());
}
}
void BrowserActionsContainer::OnThemeChanged() {
LoadImages();
}
void BrowserActionsContainer::ViewHierarchyChanged(bool is_add,
views::View* parent,
views::View* child) {
// No extensions (e.g., incognito).
if (!model_)
return;
if (is_add && 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 = ResourceBundle::GetSharedInstance().GetBitmapNamed(
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 = ResourceBundle::GetSharedInstance().GetBitmapNamed(
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]->button()->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 = VisibleBrowserActions();
// Add the new browser action to the vector and the view hierarchy.
if (profile_->IsOffTheRecord())
index = model_->OriginalIndexToIncognito(index);
BrowserActionView* view = new BrowserActionView(extension, 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) &&
!profile_->GetExtensionService()->IsBeingUpgraded(extension)) {
suppress_chevron_ = true;
SaveDesiredSizeAndAnimate(ui::Tween::LINEAR, visible_actions + 1);
} else {
// Just redraw the (possibly modified) visible icon set.
OnBrowserActionVisibilityChanged();
}
}
void BrowserActionsContainer::BrowserActionRemoved(const Extension* extension) {
CloseOverflowMenu();
if (popup_ && popup_->host()->extension() == extension)
HidePopup();
size_t visible_actions = VisibleBrowserActions();
for (BrowserActionViews::iterator iter = browser_action_views_.begin();
iter != browser_action_views_.end(); ++iter) {
if ((*iter)->button()->extension() == extension) {
RemoveChildView(*iter);
delete *iter;
browser_action_views_.erase(iter);
// 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 (profile_->GetExtensionService()->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.
chevron_->SetVisible(false);
SaveDesiredSizeAndAnimate(ui::Tween::EASE_OUT,
browser_action_views_.size());
}
return;
}
}
}
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();
}
void BrowserActionsContainer::ModelLoaded() {
SetContainerWidth();
}
void BrowserActionsContainer::LoadImages() {
ui::ThemeProvider* tp = GetThemeProvider();
chevron_->SetIcon(*tp->GetBitmapNamed(IDR_BROWSER_ACTIONS_OVERFLOW));
chevron_->SetHoverIcon(*tp->GetBitmapNamed(IDR_BROWSER_ACTIONS_OVERFLOW_H));
chevron_->SetPushedIcon(*tp->GetBitmapNamed(IDR_BROWSER_ACTIONS_OVERFLOW_P));
}
void BrowserActionsContainer::SetContainerWidth() {
int visible_actions = model_->GetVisibleIconCount();
if (visible_actions < 0) // All icons should be visible.
visible_actions = model_->size();
chevron_->SetVisible(static_cast<size_t>(visible_actions) < model_->size());
container_width_ = IconCountToWidth(visible_actions, chevron_->IsVisible());
}
void BrowserActionsContainer::CloseOverflowMenu() {
if (overflow_menu_)
overflow_menu_->CancelMenu();
}
void BrowserActionsContainer::StopShowFolderDropMenuTimer() {
show_menu_task_factory_.RevokeAll();
}
void BrowserActionsContainer::StartShowFolderDropMenuTimer() {
int delay = views::GetMenuShowDelay();
MessageLoop::current()->PostDelayedTask(FROM_HERE,
show_menu_task_factory_.NewRunnableMethod(
&BrowserActionsContainer::ShowDropFolder),
delay);
}
void BrowserActionsContainer::ShowDropFolder() {
DCHECK(!overflow_menu_);
SetDropIndicator(-1);
overflow_menu_ = new BrowserActionOverflowMenuController(
this, chevron_, browser_action_views_, VisibleBrowserActions());
overflow_menu_->set_observer(this);
overflow_menu_->RunMenu(GetWindow()->GetNativeWindow(), true);
}
void BrowserActionsContainer::SetDropIndicator(int x_pos) {
if (drop_indicator_position_ != x_pos) {
drop_indicator_position_ = x_pos;
SchedulePaint();
}
}
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 = 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_->GetPreferredSize().width() - 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::ContainerMinSize() const {
return ToolbarView::kStandardSpacing + kChevronSpacing +
chevron_->GetPreferredSize().width() + ToolbarView::kStandardSpacing;
}
void BrowserActionsContainer::SaveDesiredSizeAndAnimate(
ui::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 (!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() ||
profile_->GetExtensionService()->IsIncognitoEnabled(extension->id()));
}