blob: e1f48c283305108b660088a6549eea0c370611e3 [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_action_view.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/extensions/api/commands/command_service.h"
#include "chrome/browser/extensions/extension_action.h"
#include "chrome/browser/extensions/extension_action_manager.h"
#include "chrome/browser/extensions/extension_context_menu_model.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/extensions/accelerator_priority.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/toolbar/browser_actions_container.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_constants.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "ui/accessibility/ax_view_state.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/events/event.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/image/image_skia_source.h"
#include "ui/views/controls/button/label_button_border.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_runner.h"
using extensions::Extension;
using views::LabelButtonBorder;
namespace {
// We have smaller insets than normal STYLE_TEXTBUTTON buttons so that we can
// fit user supplied icons in without clipping them.
const int kBorderInset = 4;
} // namespace
////////////////////////////////////////////////////////////////////////////////
// BrowserActionView
BrowserActionView::BrowserActionView(const Extension* extension,
Browser* browser,
BrowserActionView::Delegate* delegate)
: browser_(browser),
delegate_(delegate),
button_(NULL),
extension_(extension) {
set_id(VIEW_ID_BROWSER_ACTION);
button_ = new BrowserActionButton(extension_, browser_, delegate_);
button_->set_drag_controller(delegate_);
button_->set_owned_by_client();
AddChildView(button_);
button_->UpdateState();
}
BrowserActionView::~BrowserActionView() {
button_->Destroy();
}
gfx::ImageSkia BrowserActionView::GetIconWithBadge() {
return button_->GetIconWithBadge();
}
void BrowserActionView::Layout() {
button_->SetBounds(0, 0, width(), height());
}
void BrowserActionView::GetAccessibleState(ui::AXViewState* state) {
state->name = l10n_util::GetStringUTF16(
IDS_ACCNAME_EXTENSIONS_BROWSER_ACTION);
state->role = ui::AX_ROLE_GROUP;
}
gfx::Size BrowserActionView::GetPreferredSize() const {
return gfx::Size(BrowserActionsContainer::IconWidth(false),
BrowserActionsContainer::IconHeight());
}
void BrowserActionView::PaintChildren(gfx::Canvas* canvas,
const views::CullSet& cull_set) {
View::PaintChildren(canvas, cull_set);
ExtensionAction* action = button()->browser_action();
int tab_id = delegate_->GetCurrentTabId();
if (tab_id >= 0)
action->PaintBadge(canvas, GetLocalBounds(), tab_id);
}
////////////////////////////////////////////////////////////////////////////////
// BrowserActionButton
BrowserActionButton::BrowserActionButton(const Extension* extension,
Browser* browser,
BrowserActionView::Delegate* delegate)
: MenuButton(this, base::string16(), NULL, false),
browser_(browser),
browser_action_(
extensions::ExtensionActionManager::Get(browser->profile())->
GetBrowserAction(*extension)),
extension_(extension),
icon_factory_(browser->profile(), extension, browser_action_, this),
delegate_(delegate),
called_registered_extension_command_(false),
icon_observer_(NULL) {
SetHorizontalAlignment(gfx::ALIGN_CENTER);
set_context_menu_controller(this);
// No UpdateState() here because View hierarchy not setup yet. Our parent
// should call UpdateState() after creation.
content::NotificationSource notification_source =
content::Source<Profile>(browser_->profile()->GetOriginalProfile());
registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED,
content::Source<ExtensionAction>(browser_action_));
registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED,
notification_source);
registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED,
notification_source);
// We also listen for browser theme changes on linux because a switch from or
// to GTK requires that we regrab our browser action images.
registrar_.Add(
this,
chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
content::Source<ThemeService>(
ThemeServiceFactory::GetForProfile(browser->profile())));
}
void BrowserActionButton::Destroy() {
MaybeUnregisterExtensionCommand(false);
if (menu_runner_) {
menu_runner_->Cancel();
base::MessageLoop::current()->DeleteSoon(FROM_HERE, this);
} else {
delete this;
}
}
void BrowserActionButton::ViewHierarchyChanged(
const ViewHierarchyChangedDetails& details) {
if (details.is_add && !called_registered_extension_command_ &&
GetFocusManager()) {
MaybeRegisterExtensionCommand();
called_registered_extension_command_ = true;
}
MenuButton::ViewHierarchyChanged(details);
}
bool BrowserActionButton::CanHandleAccelerators() const {
// View::CanHandleAccelerators() checks to see if the view is visible before
// allowing it to process accelerators. This is not appropriate for browser
// actions buttons, which can be hidden inside the overflow area.
return true;
}
void BrowserActionButton::GetAccessibleState(ui::AXViewState* state) {
views::MenuButton::GetAccessibleState(state);
state->role = ui::AX_ROLE_BUTTON;
}
void BrowserActionButton::ButtonPressed(views::Button* sender,
const ui::Event& event) {
delegate_->OnBrowserActionExecuted(this);
}
void BrowserActionButton::ShowContextMenuForView(
View* source,
const gfx::Point& point,
ui::MenuSourceType source_type) {
if (!extension()->ShowConfigureContextMenus())
return;
SetButtonPushed();
// Reconstructs the menu every time because the menu's contents are dynamic.
scoped_refptr<ExtensionContextMenuModel> context_menu_contents(
new ExtensionContextMenuModel(extension(), browser_, delegate_));
gfx::Point screen_loc;
views::View::ConvertPointToScreen(this, &screen_loc);
views::Widget* parent = NULL;
int run_types = views::MenuRunner::HAS_MNEMONICS |
views::MenuRunner::CONTEXT_MENU;
if (delegate_->ShownInsideMenu()) {
run_types |= views::MenuRunner::IS_NESTED;
// RunMenuAt expects a nested menu to be parented by the same widget as the
// already visible menu, in this case the Chrome menu.
parent = BrowserView::GetBrowserViewForBrowser(browser_)->toolbar()
->app_menu()
->GetWidget();
} else {
parent = GetWidget();
}
menu_runner_.reset(
new views::MenuRunner(context_menu_contents.get(), run_types));
if (menu_runner_->RunMenuAt(parent,
NULL,
gfx::Rect(screen_loc, size()),
views::MENU_ANCHOR_TOPLEFT,
source_type) ==
views::MenuRunner::MENU_DELETED) {
return;
}
menu_runner_.reset();
SetButtonNotPushed();
}
void BrowserActionButton::UpdateState() {
int tab_id = delegate_->GetCurrentTabId();
if (tab_id < 0)
return;
if (!IsEnabled(tab_id)) {
SetState(views::CustomButton::STATE_DISABLED);
} else {
SetState(menu_visible_ ?
views::CustomButton::STATE_PRESSED :
views::CustomButton::STATE_NORMAL);
}
gfx::ImageSkia icon = *icon_factory_.GetIcon(tab_id).ToImageSkia();
if (!icon.isNull()) {
if (!browser_action()->GetIsVisible(tab_id))
icon = gfx::ImageSkiaOperations::CreateTransparentImage(icon, .25);
ThemeService* theme =
ThemeServiceFactory::GetForProfile(browser_->profile());
gfx::ImageSkia bg = *theme->GetImageSkiaNamed(IDR_BROWSER_ACTION);
SetImage(views::Button::STATE_NORMAL,
gfx::ImageSkiaOperations::CreateSuperimposedImage(bg, icon));
}
// If the browser action name is empty, show the extension name instead.
std::string title = browser_action()->GetTitle(tab_id);
base::string16 name =
base::UTF8ToUTF16(title.empty() ? extension()->name() : title);
SetTooltipText(name);
SetAccessibleName(name);
parent()->SchedulePaint();
}
bool BrowserActionButton::IsPopup() {
int tab_id = delegate_->GetCurrentTabId();
return (tab_id < 0) ? false : browser_action_->HasPopup(tab_id);
}
GURL BrowserActionButton::GetPopupUrl() {
int tab_id = delegate_->GetCurrentTabId();
return (tab_id < 0) ? GURL() : browser_action_->GetPopupUrl(tab_id);
}
void BrowserActionButton::Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
switch (type) {
case chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED:
UpdateState();
// The browser action may have become visible/hidden so we need to make
// sure the state gets updated.
delegate_->OnBrowserActionVisibilityChanged();
break;
case chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED:
case chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED: {
std::pair<const std::string, const std::string>* payload =
content::Details<std::pair<const std::string, const std::string> >(
details).ptr();
if (extension_->id() == payload->first &&
payload->second ==
extensions::manifest_values::kBrowserActionCommandEvent) {
if (type == chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED)
MaybeRegisterExtensionCommand();
else
MaybeUnregisterExtensionCommand(true);
}
break;
}
case chrome::NOTIFICATION_BROWSER_THEME_CHANGED:
UpdateState();
break;
default:
NOTREACHED();
break;
}
}
void BrowserActionButton::OnIconUpdated() {
UpdateState();
if (icon_observer_)
icon_observer_->OnIconUpdated(GetIconWithBadge());
}
bool BrowserActionButton::Activate() {
if (!IsPopup())
return true;
delegate_->OnBrowserActionExecuted(this);
// 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 ui::MouseEvent& event) {
if (!event.IsRightMouseButton()) {
return IsPopup() ? MenuButton::OnMousePressed(event) :
LabelButton::OnMousePressed(event);
}
if (!views::View::ShouldShowContextMenuOnMousePress()) {
// See comments in MenuButton::Activate() as to why this is needed.
SetMouseHandler(NULL);
ShowContextMenu(gfx::Point(), ui::MENU_SOURCE_MOUSE);
}
return false;
}
void BrowserActionButton::OnMouseReleased(const ui::MouseEvent& event) {
if (IsPopup() || menu_runner_) {
// TODO(erikkay) this never actually gets called (probably because of the
// loss of focus).
MenuButton::OnMouseReleased(event);
} else {
LabelButton::OnMouseReleased(event);
}
}
void BrowserActionButton::OnMouseExited(const ui::MouseEvent& event) {
if (IsPopup() || menu_runner_)
MenuButton::OnMouseExited(event);
else
LabelButton::OnMouseExited(event);
}
bool BrowserActionButton::OnKeyReleased(const ui::KeyEvent& event) {
return IsPopup() ? MenuButton::OnKeyReleased(event) :
LabelButton::OnKeyReleased(event);
}
void BrowserActionButton::OnGestureEvent(ui::GestureEvent* event) {
if (IsPopup())
MenuButton::OnGestureEvent(event);
else
LabelButton::OnGestureEvent(event);
}
scoped_ptr<LabelButtonBorder> BrowserActionButton::CreateDefaultBorder() const {
scoped_ptr<LabelButtonBorder> border = LabelButton::CreateDefaultBorder();
border->set_insets(gfx::Insets(kBorderInset, kBorderInset,
kBorderInset, kBorderInset));
return border.Pass();
}
bool BrowserActionButton::AcceleratorPressed(
const ui::Accelerator& accelerator) {
// Normal priority shortcuts must be handled via standard browser commands to
// be processed at the proper time.
if (GetAcceleratorPriority(accelerator, extension_) ==
ui::AcceleratorManager::kNormalPriority)
return false;
delegate_->OnBrowserActionExecuted(this);
return true;
}
void BrowserActionButton::SetButtonPushed() {
SetState(views::CustomButton::STATE_PRESSED);
menu_visible_ = true;
}
void BrowserActionButton::SetButtonNotPushed() {
SetState(views::CustomButton::STATE_NORMAL);
menu_visible_ = false;
}
bool BrowserActionButton::IsEnabled(int tab_id) const {
return browser_action_->GetIsVisible(tab_id);
}
gfx::ImageSkia BrowserActionButton::GetIconWithBadge() {
int tab_id = delegate_->GetCurrentTabId();
gfx::Size spacing(0, ToolbarView::kVertSpacing);
gfx::ImageSkia icon = *icon_factory_.GetIcon(tab_id).ToImageSkia();
if (!IsEnabled(tab_id))
icon = gfx::ImageSkiaOperations::CreateTransparentImage(icon, .25);
return browser_action_->GetIconWithBadge(icon, tab_id, spacing);
}
gfx::ImageSkia BrowserActionButton::GetIconForTest() {
return GetImage(views::Button::STATE_NORMAL);
}
BrowserActionButton::~BrowserActionButton() {
}
void BrowserActionButton::MaybeRegisterExtensionCommand() {
extensions::CommandService* command_service =
extensions::CommandService::Get(browser_->profile());
extensions::Command browser_action_command;
if (command_service->GetBrowserActionCommand(
extension_->id(),
extensions::CommandService::ACTIVE_ONLY,
&browser_action_command,
NULL)) {
keybinding_.reset(new ui::Accelerator(
browser_action_command.accelerator()));
GetFocusManager()->RegisterAccelerator(
*keybinding_.get(),
GetAcceleratorPriority(browser_action_command.accelerator(),
extension_),
this);
}
}
void BrowserActionButton::MaybeUnregisterExtensionCommand(bool only_if_active) {
if (!keybinding_.get() || !GetFocusManager())
return;
extensions::CommandService* command_service =
extensions::CommandService::Get(browser_->profile());
extensions::Command browser_action_command;
if (!only_if_active || !command_service->GetBrowserActionCommand(
extension_->id(),
extensions::CommandService::ACTIVE_ONLY,
&browser_action_command,
NULL)) {
GetFocusManager()->UnregisterAccelerator(*keybinding_.get(), this);
keybinding_.reset(NULL);
}
}