browser_action_view.cc revision 116680a4aac90f2aa7413d9095a592090648e557
1// Copyright 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/ui/views/toolbar/browser_action_view.h" 6 7#include "base/strings/utf_string_conversions.h" 8#include "chrome/browser/chrome_notification_types.h" 9#include "chrome/browser/extensions/api/commands/command_service.h" 10#include "chrome/browser/extensions/extension_action.h" 11#include "chrome/browser/extensions/extension_action_manager.h" 12#include "chrome/browser/extensions/extension_context_menu_model.h" 13#include "chrome/browser/extensions/extension_service.h" 14#include "chrome/browser/profiles/profile.h" 15#include "chrome/browser/themes/theme_service.h" 16#include "chrome/browser/themes/theme_service_factory.h" 17#include "chrome/browser/ui/browser.h" 18#include "chrome/browser/ui/extensions/accelerator_priority.h" 19#include "chrome/browser/ui/view_ids.h" 20#include "chrome/browser/ui/views/frame/browser_view.h" 21#include "chrome/browser/ui/views/toolbar/browser_actions_container.h" 22#include "chrome/browser/ui/views/toolbar/toolbar_view.h" 23#include "extensions/common/extension.h" 24#include "extensions/common/manifest_constants.h" 25#include "grit/generated_resources.h" 26#include "grit/theme_resources.h" 27#include "ui/accessibility/ax_view_state.h" 28#include "ui/base/l10n/l10n_util.h" 29#include "ui/base/resource/resource_bundle.h" 30#include "ui/events/event.h" 31#include "ui/gfx/image/image_skia.h" 32#include "ui/gfx/image/image_skia_operations.h" 33#include "ui/gfx/image/image_skia_source.h" 34#include "ui/views/controls/button/label_button_border.h" 35#include "ui/views/controls/menu/menu_item_view.h" 36#include "ui/views/controls/menu/menu_runner.h" 37 38using extensions::Extension; 39using views::LabelButtonBorder; 40 41namespace { 42 43// We have smaller insets than normal STYLE_TEXTBUTTON buttons so that we can 44// fit user supplied icons in without clipping them. 45const int kBorderInset = 4; 46 47} // namespace 48 49//////////////////////////////////////////////////////////////////////////////// 50// BrowserActionView 51 52BrowserActionView::BrowserActionView(const Extension* extension, 53 Browser* browser, 54 BrowserActionView::Delegate* delegate) 55 : browser_(browser), 56 delegate_(delegate), 57 button_(NULL), 58 extension_(extension) { 59 set_id(VIEW_ID_BROWSER_ACTION); 60 button_ = new BrowserActionButton(extension_, browser_, delegate_); 61 button_->set_drag_controller(delegate_); 62 button_->set_owned_by_client(); 63 AddChildView(button_); 64 button_->UpdateState(); 65} 66 67BrowserActionView::~BrowserActionView() { 68 button_->Destroy(); 69} 70 71gfx::ImageSkia BrowserActionView::GetIconWithBadge() { 72 return button_->GetIconWithBadge(); 73} 74 75void BrowserActionView::Layout() { 76 button_->SetBounds(0, 0, width(), height()); 77} 78 79void BrowserActionView::GetAccessibleState(ui::AXViewState* state) { 80 state->name = l10n_util::GetStringUTF16( 81 IDS_ACCNAME_EXTENSIONS_BROWSER_ACTION); 82 state->role = ui::AX_ROLE_GROUP; 83} 84 85gfx::Size BrowserActionView::GetPreferredSize() const { 86 return gfx::Size(BrowserActionsContainer::IconWidth(false), 87 BrowserActionsContainer::IconHeight()); 88} 89 90void BrowserActionView::PaintChildren(gfx::Canvas* canvas, 91 const views::CullSet& cull_set) { 92 View::PaintChildren(canvas, cull_set); 93 ExtensionAction* action = button()->browser_action(); 94 int tab_id = delegate_->GetCurrentTabId(); 95 if (tab_id >= 0) 96 action->PaintBadge(canvas, GetLocalBounds(), tab_id); 97} 98 99//////////////////////////////////////////////////////////////////////////////// 100// BrowserActionButton 101 102BrowserActionButton::BrowserActionButton(const Extension* extension, 103 Browser* browser, 104 BrowserActionView::Delegate* delegate) 105 : MenuButton(this, base::string16(), NULL, false), 106 browser_(browser), 107 browser_action_( 108 extensions::ExtensionActionManager::Get(browser->profile())-> 109 GetBrowserAction(*extension)), 110 extension_(extension), 111 icon_factory_(browser->profile(), extension, browser_action_, this), 112 delegate_(delegate), 113 called_registered_extension_command_(false), 114 icon_observer_(NULL) { 115 SetHorizontalAlignment(gfx::ALIGN_CENTER); 116 set_context_menu_controller(this); 117 118 // No UpdateState() here because View hierarchy not setup yet. Our parent 119 // should call UpdateState() after creation. 120 121 content::NotificationSource notification_source = 122 content::Source<Profile>(browser_->profile()->GetOriginalProfile()); 123 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED, 124 content::Source<ExtensionAction>(browser_action_)); 125 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED, 126 notification_source); 127 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED, 128 notification_source); 129 130 // We also listen for browser theme changes on linux because a switch from or 131 // to GTK requires that we regrab our browser action images. 132 registrar_.Add( 133 this, 134 chrome::NOTIFICATION_BROWSER_THEME_CHANGED, 135 content::Source<ThemeService>( 136 ThemeServiceFactory::GetForProfile(browser->profile()))); 137} 138 139void BrowserActionButton::Destroy() { 140 MaybeUnregisterExtensionCommand(false); 141 142 if (menu_runner_) { 143 menu_runner_->Cancel(); 144 base::MessageLoop::current()->DeleteSoon(FROM_HERE, this); 145 } else { 146 delete this; 147 } 148} 149 150void BrowserActionButton::ViewHierarchyChanged( 151 const ViewHierarchyChangedDetails& details) { 152 if (details.is_add && !called_registered_extension_command_ && 153 GetFocusManager()) { 154 MaybeRegisterExtensionCommand(); 155 called_registered_extension_command_ = true; 156 } 157 158 MenuButton::ViewHierarchyChanged(details); 159} 160 161bool BrowserActionButton::CanHandleAccelerators() const { 162 // View::CanHandleAccelerators() checks to see if the view is visible before 163 // allowing it to process accelerators. This is not appropriate for browser 164 // actions buttons, which can be hidden inside the overflow area. 165 return true; 166} 167 168void BrowserActionButton::GetAccessibleState(ui::AXViewState* state) { 169 views::MenuButton::GetAccessibleState(state); 170 state->role = ui::AX_ROLE_BUTTON; 171} 172 173void BrowserActionButton::ButtonPressed(views::Button* sender, 174 const ui::Event& event) { 175 delegate_->OnBrowserActionExecuted(this); 176} 177 178void BrowserActionButton::ShowContextMenuForView( 179 View* source, 180 const gfx::Point& point, 181 ui::MenuSourceType source_type) { 182 if (!extension()->ShowConfigureContextMenus()) 183 return; 184 185 SetButtonPushed(); 186 187 // Reconstructs the menu every time because the menu's contents are dynamic. 188 scoped_refptr<ExtensionContextMenuModel> context_menu_contents( 189 new ExtensionContextMenuModel(extension(), browser_, delegate_)); 190 gfx::Point screen_loc; 191 views::View::ConvertPointToScreen(this, &screen_loc); 192 193 views::Widget* parent = NULL; 194 int run_types = views::MenuRunner::HAS_MNEMONICS | 195 views::MenuRunner::CONTEXT_MENU; 196 if (delegate_->ShownInsideMenu()) { 197 run_types |= views::MenuRunner::IS_NESTED; 198 // RunMenuAt expects a nested menu to be parented by the same widget as the 199 // already visible menu, in this case the Chrome menu. 200 parent = BrowserView::GetBrowserViewForBrowser(browser_)->toolbar() 201 ->app_menu() 202 ->GetWidget(); 203 } else { 204 parent = GetWidget(); 205 } 206 207 menu_runner_.reset( 208 new views::MenuRunner(context_menu_contents.get(), run_types)); 209 210 if (menu_runner_->RunMenuAt(parent, 211 NULL, 212 gfx::Rect(screen_loc, size()), 213 views::MENU_ANCHOR_TOPLEFT, 214 source_type) == 215 views::MenuRunner::MENU_DELETED) { 216 return; 217 } 218 219 menu_runner_.reset(); 220 SetButtonNotPushed(); 221} 222 223void BrowserActionButton::UpdateState() { 224 int tab_id = delegate_->GetCurrentTabId(); 225 if (tab_id < 0) 226 return; 227 228 if (!IsEnabled(tab_id)) { 229 SetState(views::CustomButton::STATE_DISABLED); 230 } else { 231 SetState(menu_visible_ ? 232 views::CustomButton::STATE_PRESSED : 233 views::CustomButton::STATE_NORMAL); 234 } 235 236 gfx::ImageSkia icon = *icon_factory_.GetIcon(tab_id).ToImageSkia(); 237 238 if (!icon.isNull()) { 239 if (!browser_action()->GetIsVisible(tab_id)) 240 icon = gfx::ImageSkiaOperations::CreateTransparentImage(icon, .25); 241 242 ThemeService* theme = 243 ThemeServiceFactory::GetForProfile(browser_->profile()); 244 245 gfx::ImageSkia bg = *theme->GetImageSkiaNamed(IDR_BROWSER_ACTION); 246 SetImage(views::Button::STATE_NORMAL, 247 gfx::ImageSkiaOperations::CreateSuperimposedImage(bg, icon)); 248 } 249 250 // If the browser action name is empty, show the extension name instead. 251 std::string title = browser_action()->GetTitle(tab_id); 252 base::string16 name = 253 base::UTF8ToUTF16(title.empty() ? extension()->name() : title); 254 SetTooltipText(name); 255 SetAccessibleName(name); 256 257 parent()->SchedulePaint(); 258} 259 260bool BrowserActionButton::IsPopup() { 261 int tab_id = delegate_->GetCurrentTabId(); 262 return (tab_id < 0) ? false : browser_action_->HasPopup(tab_id); 263} 264 265GURL BrowserActionButton::GetPopupUrl() { 266 int tab_id = delegate_->GetCurrentTabId(); 267 return (tab_id < 0) ? GURL() : browser_action_->GetPopupUrl(tab_id); 268} 269 270void BrowserActionButton::Observe(int type, 271 const content::NotificationSource& source, 272 const content::NotificationDetails& details) { 273 switch (type) { 274 case chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED: 275 UpdateState(); 276 // The browser action may have become visible/hidden so we need to make 277 // sure the state gets updated. 278 delegate_->OnBrowserActionVisibilityChanged(); 279 break; 280 case chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED: 281 case chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED: { 282 std::pair<const std::string, const std::string>* payload = 283 content::Details<std::pair<const std::string, const std::string> >( 284 details).ptr(); 285 if (extension_->id() == payload->first && 286 payload->second == 287 extensions::manifest_values::kBrowserActionCommandEvent) { 288 if (type == chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED) 289 MaybeRegisterExtensionCommand(); 290 else 291 MaybeUnregisterExtensionCommand(true); 292 } 293 break; 294 } 295 case chrome::NOTIFICATION_BROWSER_THEME_CHANGED: 296 UpdateState(); 297 break; 298 default: 299 NOTREACHED(); 300 break; 301 } 302} 303 304void BrowserActionButton::OnIconUpdated() { 305 UpdateState(); 306 if (icon_observer_) 307 icon_observer_->OnIconUpdated(GetIconWithBadge()); 308} 309 310bool BrowserActionButton::Activate() { 311 if (!IsPopup()) 312 return true; 313 314 delegate_->OnBrowserActionExecuted(this); 315 316 // TODO(erikkay): Run a nested modal loop while the mouse is down to 317 // enable menu-like drag-select behavior. 318 319 // The return value of this method is returned via OnMousePressed. 320 // We need to return false here since we're handing off focus to another 321 // widget/view, and true will grab it right back and try to send events 322 // to us. 323 return false; 324} 325 326bool BrowserActionButton::OnMousePressed(const ui::MouseEvent& event) { 327 if (!event.IsRightMouseButton()) { 328 return IsPopup() ? MenuButton::OnMousePressed(event) : 329 LabelButton::OnMousePressed(event); 330 } 331 332 if (!views::View::ShouldShowContextMenuOnMousePress()) { 333 // See comments in MenuButton::Activate() as to why this is needed. 334 SetMouseHandler(NULL); 335 336 ShowContextMenu(gfx::Point(), ui::MENU_SOURCE_MOUSE); 337 } 338 return false; 339} 340 341void BrowserActionButton::OnMouseReleased(const ui::MouseEvent& event) { 342 if (IsPopup() || menu_runner_) { 343 // TODO(erikkay) this never actually gets called (probably because of the 344 // loss of focus). 345 MenuButton::OnMouseReleased(event); 346 } else { 347 LabelButton::OnMouseReleased(event); 348 } 349} 350 351void BrowserActionButton::OnMouseExited(const ui::MouseEvent& event) { 352 if (IsPopup() || menu_runner_) 353 MenuButton::OnMouseExited(event); 354 else 355 LabelButton::OnMouseExited(event); 356} 357 358bool BrowserActionButton::OnKeyReleased(const ui::KeyEvent& event) { 359 return IsPopup() ? MenuButton::OnKeyReleased(event) : 360 LabelButton::OnKeyReleased(event); 361} 362 363void BrowserActionButton::OnGestureEvent(ui::GestureEvent* event) { 364 if (IsPopup()) 365 MenuButton::OnGestureEvent(event); 366 else 367 LabelButton::OnGestureEvent(event); 368} 369 370scoped_ptr<LabelButtonBorder> BrowserActionButton::CreateDefaultBorder() const { 371 scoped_ptr<LabelButtonBorder> border = LabelButton::CreateDefaultBorder(); 372 border->set_insets(gfx::Insets(kBorderInset, kBorderInset, 373 kBorderInset, kBorderInset)); 374 return border.Pass(); 375} 376 377bool BrowserActionButton::AcceleratorPressed( 378 const ui::Accelerator& accelerator) { 379 // Normal priority shortcuts must be handled via standard browser commands to 380 // be processed at the proper time. 381 if (GetAcceleratorPriority(accelerator, extension_) == 382 ui::AcceleratorManager::kNormalPriority) 383 return false; 384 385 delegate_->OnBrowserActionExecuted(this); 386 return true; 387} 388 389void BrowserActionButton::SetButtonPushed() { 390 SetState(views::CustomButton::STATE_PRESSED); 391 menu_visible_ = true; 392} 393 394void BrowserActionButton::SetButtonNotPushed() { 395 SetState(views::CustomButton::STATE_NORMAL); 396 menu_visible_ = false; 397} 398 399bool BrowserActionButton::IsEnabled(int tab_id) const { 400 return browser_action_->GetIsVisible(tab_id); 401} 402 403gfx::ImageSkia BrowserActionButton::GetIconWithBadge() { 404 int tab_id = delegate_->GetCurrentTabId(); 405 gfx::Size spacing(0, ToolbarView::kVertSpacing); 406 gfx::ImageSkia icon = *icon_factory_.GetIcon(tab_id).ToImageSkia(); 407 if (!IsEnabled(tab_id)) 408 icon = gfx::ImageSkiaOperations::CreateTransparentImage(icon, .25); 409 return browser_action_->GetIconWithBadge(icon, tab_id, spacing); 410} 411 412gfx::ImageSkia BrowserActionButton::GetIconForTest() { 413 return GetImage(views::Button::STATE_NORMAL); 414} 415 416BrowserActionButton::~BrowserActionButton() { 417} 418 419void BrowserActionButton::MaybeRegisterExtensionCommand() { 420 extensions::CommandService* command_service = 421 extensions::CommandService::Get(browser_->profile()); 422 extensions::Command browser_action_command; 423 if (command_service->GetBrowserActionCommand( 424 extension_->id(), 425 extensions::CommandService::ACTIVE_ONLY, 426 &browser_action_command, 427 NULL)) { 428 keybinding_.reset(new ui::Accelerator( 429 browser_action_command.accelerator())); 430 GetFocusManager()->RegisterAccelerator( 431 *keybinding_.get(), 432 GetAcceleratorPriority(browser_action_command.accelerator(), 433 extension_), 434 this); 435 } 436} 437 438void BrowserActionButton::MaybeUnregisterExtensionCommand(bool only_if_active) { 439 if (!keybinding_.get() || !GetFocusManager()) 440 return; 441 442 extensions::CommandService* command_service = 443 extensions::CommandService::Get(browser_->profile()); 444 445 extensions::Command browser_action_command; 446 if (!only_if_active || !command_service->GetBrowserActionCommand( 447 extension_->id(), 448 extensions::CommandService::ACTIVE_ONLY, 449 &browser_action_command, 450 NULL)) { 451 GetFocusManager()->UnregisterAccelerator(*keybinding_.get(), this); 452 keybinding_.reset(NULL); 453 } 454} 455