1 // Copyright (c) 2011 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/browser_actions_container.h" 6 7 #include "base/stl_util-inl.h" 8 #include "base/string_util.h" 9 #include "base/utf_string_conversions.h" 10 #include "chrome/browser/extensions/extension_browser_event_router.h" 11 #include "chrome/browser/extensions/extension_host.h" 12 #include "chrome/browser/extensions/extension_service.h" 13 #include "chrome/browser/extensions/extension_tabs_module.h" 14 #include "chrome/browser/profiles/profile.h" 15 #include "chrome/browser/ui/browser.h" 16 #include "chrome/browser/ui/browser_window.h" 17 #include "chrome/browser/ui/view_ids.h" 18 #include "chrome/browser/ui/views/detachable_toolbar_view.h" 19 #include "chrome/browser/ui/views/extensions/browser_action_drag_data.h" 20 #include "chrome/browser/ui/views/extensions/extension_popup.h" 21 #include "chrome/browser/ui/views/toolbar_view.h" 22 #include "chrome/common/extensions/extension_action.h" 23 #include "chrome/common/extensions/extension_resource.h" 24 #include "chrome/common/pref_names.h" 25 #include "content/browser/renderer_host/render_view_host.h" 26 #include "content/browser/renderer_host/render_widget_host_view.h" 27 #include "content/browser/tab_contents/tab_contents.h" 28 #include "content/common/notification_source.h" 29 #include "content/common/notification_type.h" 30 #include "grit/app_resources.h" 31 #include "grit/generated_resources.h" 32 #include "third_party/skia/include/core/SkBitmap.h" 33 #include "third_party/skia/include/effects/SkGradientShader.h" 34 #include "ui/base/accessibility/accessible_view_state.h" 35 #include "ui/base/animation/slide_animation.h" 36 #include "ui/base/l10n/l10n_util.h" 37 #include "ui/base/resource/resource_bundle.h" 38 #include "ui/base/theme_provider.h" 39 #include "ui/gfx/canvas.h" 40 #include "ui/gfx/canvas_skia.h" 41 #include "views/controls/button/menu_button.h" 42 #include "views/controls/button/text_button.h" 43 #include "views/controls/menu/menu_2.h" 44 #include "views/drag_utils.h" 45 #include "views/metrics.h" 46 #include "views/window/window.h" 47 48 #include "grit/theme_resources.h" 49 50 // Horizontal spacing between most items in the container, as well as after the 51 // last item or chevron (if visible). 52 static const int kItemSpacing = ToolbarView::kStandardSpacing; 53 // Horizontal spacing before the chevron (if visible). 54 static const int kChevronSpacing = kItemSpacing - 2; 55 56 // static 57 bool BrowserActionsContainer::disable_animations_during_testing_ = false; 58 59 //////////////////////////////////////////////////////////////////////////////// 60 // BrowserActionButton 61 62 BrowserActionButton::BrowserActionButton(const Extension* extension, 63 BrowserActionsContainer* panel) 64 : ALLOW_THIS_IN_INITIALIZER_LIST( 65 MenuButton(this, std::wstring(), NULL, false)), 66 browser_action_(extension->browser_action()), 67 extension_(extension), 68 ALLOW_THIS_IN_INITIALIZER_LIST(tracker_(this)), 69 showing_context_menu_(false), 70 panel_(panel) { 71 set_border(NULL); 72 set_alignment(TextButton::ALIGN_CENTER); 73 74 // No UpdateState() here because View hierarchy not setup yet. Our parent 75 // should call UpdateState() after creation. 76 77 registrar_.Add(this, NotificationType::EXTENSION_BROWSER_ACTION_UPDATED, 78 Source<ExtensionAction>(browser_action_)); 79 } 80 81 void BrowserActionButton::Destroy() { 82 if (showing_context_menu_) { 83 context_menu_menu_->CancelMenu(); 84 MessageLoop::current()->DeleteSoon(FROM_HERE, this); 85 } else { 86 delete this; 87 } 88 } 89 90 void BrowserActionButton::ViewHierarchyChanged( 91 bool is_add, View* parent, View* child) { 92 if (is_add && child == this) { 93 // The Browser Action API does not allow the default icon path to be 94 // changed at runtime, so we can load this now and cache it. 95 std::string relative_path = browser_action_->default_icon_path(); 96 if (relative_path.empty()) 97 return; 98 99 // LoadImage is not guaranteed to be synchronous, so we might see the 100 // callback OnImageLoaded execute immediately. It (through UpdateState) 101 // expects parent() to return the owner for this button, so this 102 // function is as early as we can start this request. 103 tracker_.LoadImage(extension_, extension_->GetResource(relative_path), 104 gfx::Size(Extension::kBrowserActionIconMaxSize, 105 Extension::kBrowserActionIconMaxSize), 106 ImageLoadingTracker::DONT_CACHE); 107 } 108 109 MenuButton::ViewHierarchyChanged(is_add, parent, child); 110 } 111 112 void BrowserActionButton::ButtonPressed(views::Button* sender, 113 const views::Event& event) { 114 panel_->OnBrowserActionExecuted(this, false); 115 } 116 117 void BrowserActionButton::OnImageLoaded(SkBitmap* image, 118 const ExtensionResource& resource, 119 int index) { 120 if (image) 121 default_icon_ = *image; 122 123 // Call back to UpdateState() because a more specific icon might have been set 124 // while the load was outstanding. 125 UpdateState(); 126 } 127 128 void BrowserActionButton::UpdateState() { 129 int tab_id = panel_->GetCurrentTabId(); 130 if (tab_id < 0) 131 return; 132 133 SkBitmap icon(browser_action()->GetIcon(tab_id)); 134 if (icon.isNull()) 135 icon = default_icon_; 136 if (!icon.isNull()) { 137 SkPaint paint; 138 paint.setXfermode(SkXfermode::Create(SkXfermode::kSrcOver_Mode)); 139 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 140 141 SkBitmap bg; 142 rb.GetBitmapNamed(IDR_BROWSER_ACTION)->copyTo(&bg, 143 SkBitmap::kARGB_8888_Config); 144 SkCanvas bg_canvas(bg); 145 bg_canvas.drawBitmap(icon, SkIntToScalar((bg.width() - icon.width()) / 2), 146 SkIntToScalar((bg.height() - icon.height()) / 2), &paint); 147 SetIcon(bg); 148 149 SkBitmap bg_h; 150 rb.GetBitmapNamed(IDR_BROWSER_ACTION_H)->copyTo(&bg_h, 151 SkBitmap::kARGB_8888_Config); 152 SkCanvas bg_h_canvas(bg_h); 153 bg_h_canvas.drawBitmap(icon, 154 SkIntToScalar((bg_h.width() - icon.width()) / 2), 155 SkIntToScalar((bg_h.height() - icon.height()) / 2), &paint); 156 SetHoverIcon(bg_h); 157 158 SkBitmap bg_p; 159 rb.GetBitmapNamed(IDR_BROWSER_ACTION_P)->copyTo(&bg_p, 160 SkBitmap::kARGB_8888_Config); 161 SkCanvas bg_p_canvas(bg_p); 162 bg_p_canvas.drawBitmap(icon, 163 SkIntToScalar((bg_p.width() - icon.width()) / 2), 164 SkIntToScalar((bg_p.height() - icon.height()) / 2), &paint); 165 SetPushedIcon(bg_p); 166 } 167 168 // If the browser action name is empty, show the extension name instead. 169 string16 name = UTF8ToUTF16(browser_action()->GetTitle(tab_id)); 170 if (name.empty()) 171 name = UTF8ToUTF16(extension()->name()); 172 SetTooltipText(UTF16ToWideHack(name)); 173 parent()->SchedulePaint(); 174 } 175 176 bool BrowserActionButton::IsPopup() { 177 int tab_id = panel_->GetCurrentTabId(); 178 return (tab_id < 0) ? false : browser_action_->HasPopup(tab_id); 179 } 180 181 GURL BrowserActionButton::GetPopupUrl() { 182 int tab_id = panel_->GetCurrentTabId(); 183 return (tab_id < 0) ? GURL() : browser_action_->GetPopupUrl(tab_id); 184 } 185 186 void BrowserActionButton::Observe(NotificationType type, 187 const NotificationSource& source, 188 const NotificationDetails& details) { 189 DCHECK(type == NotificationType::EXTENSION_BROWSER_ACTION_UPDATED); 190 UpdateState(); 191 // The browser action may have become visible/hidden so we need to make 192 // sure the state gets updated. 193 panel_->OnBrowserActionVisibilityChanged(); 194 } 195 196 bool BrowserActionButton::Activate() { 197 if (!IsPopup()) 198 return true; 199 200 panel_->OnBrowserActionExecuted(this, false); 201 202 // TODO(erikkay): Run a nested modal loop while the mouse is down to 203 // enable menu-like drag-select behavior. 204 205 // The return value of this method is returned via OnMousePressed. 206 // We need to return false here since we're handing off focus to another 207 // widget/view, and true will grab it right back and try to send events 208 // to us. 209 return false; 210 } 211 212 bool BrowserActionButton::OnMousePressed(const views::MouseEvent& event) { 213 if (!event.IsRightMouseButton()) { 214 return IsPopup() ? 215 MenuButton::OnMousePressed(event) : TextButton::OnMousePressed(event); 216 } 217 218 // Get the top left point of this button in screen coordinates. 219 gfx::Point point = gfx::Point(0, 0); 220 ConvertPointToScreen(this, &point); 221 222 // Make the menu appear below the button. 223 point.Offset(0, height()); 224 225 ShowContextMenu(point, true); 226 return false; 227 } 228 229 void BrowserActionButton::OnMouseReleased(const views::MouseEvent& event) { 230 if (IsPopup() || showing_context_menu_) { 231 // TODO(erikkay) this never actually gets called (probably because of the 232 // loss of focus). 233 MenuButton::OnMouseReleased(event); 234 } else { 235 TextButton::OnMouseReleased(event); 236 } 237 } 238 239 void BrowserActionButton::OnMouseExited(const views::MouseEvent& event) { 240 if (IsPopup() || showing_context_menu_) 241 MenuButton::OnMouseExited(event); 242 else 243 TextButton::OnMouseExited(event); 244 } 245 246 bool BrowserActionButton::OnKeyReleased(const views::KeyEvent& event) { 247 return IsPopup() ? 248 MenuButton::OnKeyReleased(event) : TextButton::OnKeyReleased(event); 249 } 250 251 void BrowserActionButton::ShowContextMenu(const gfx::Point& p, 252 bool is_mouse_gesture) { 253 if (!extension()->ShowConfigureContextMenus()) 254 return; 255 256 showing_context_menu_ = true; 257 SetButtonPushed(); 258 259 // Reconstructs the menu every time because the menu's contents are dynamic. 260 context_menu_contents_ = 261 new ExtensionContextMenuModel(extension(), panel_->browser(), panel_); 262 context_menu_menu_.reset(new views::Menu2(context_menu_contents_.get())); 263 context_menu_menu_->RunContextMenuAt(p); 264 265 SetButtonNotPushed(); 266 showing_context_menu_ = false; 267 } 268 269 void BrowserActionButton::SetButtonPushed() { 270 SetState(views::CustomButton::BS_PUSHED); 271 menu_visible_ = true; 272 } 273 274 void BrowserActionButton::SetButtonNotPushed() { 275 SetState(views::CustomButton::BS_NORMAL); 276 menu_visible_ = false; 277 } 278 279 BrowserActionButton::~BrowserActionButton() { 280 } 281 282 283 //////////////////////////////////////////////////////////////////////////////// 284 // BrowserActionView 285 286 BrowserActionView::BrowserActionView(const Extension* extension, 287 BrowserActionsContainer* panel) 288 : panel_(panel) { 289 button_ = new BrowserActionButton(extension, panel); 290 button_->SetDragController(panel_); 291 AddChildView(button_); 292 button_->UpdateState(); 293 } 294 295 BrowserActionView::~BrowserActionView() { 296 RemoveChildView(button_); 297 button_->Destroy(); 298 } 299 300 gfx::Canvas* BrowserActionView::GetIconWithBadge() { 301 int tab_id = panel_->GetCurrentTabId(); 302 303 SkBitmap icon = button_->extension()->browser_action()->GetIcon(tab_id); 304 if (icon.isNull()) 305 icon = button_->default_icon(); 306 307 gfx::Canvas* canvas = new gfx::CanvasSkia(icon.width(), icon.height(), false); 308 canvas->DrawBitmapInt(icon, 0, 0); 309 310 if (tab_id >= 0) { 311 gfx::Rect bounds(icon.width(), icon.height() + ToolbarView::kVertSpacing); 312 button_->extension()->browser_action()->PaintBadge(canvas, bounds, tab_id); 313 } 314 315 return canvas; 316 } 317 318 void BrowserActionView::Layout() { 319 // We can't rely on button_->GetPreferredSize() here because that's not set 320 // correctly until the first call to 321 // BrowserActionsContainer::RefreshBrowserActionViews(), whereas this can be 322 // called before that when the initial bounds are set (and then not after, 323 // since the bounds don't change). So instead of setting the height from the 324 // button's preferred size, we use IconHeight(), since that's how big the 325 // button should be regardless of what it's displaying. 326 button_->SetBounds(0, ToolbarView::kVertSpacing, width(), 327 BrowserActionsContainer::IconHeight()); 328 } 329 330 void BrowserActionView::GetAccessibleState(ui::AccessibleViewState* state) { 331 state->name = l10n_util::GetStringUTF16( 332 IDS_ACCNAME_EXTENSIONS_BROWSER_ACTION); 333 state->role = ui::AccessibilityTypes::ROLE_GROUPING; 334 } 335 336 void BrowserActionView::PaintChildren(gfx::Canvas* canvas) { 337 View::PaintChildren(canvas); 338 ExtensionAction* action = button()->browser_action(); 339 int tab_id = panel_->GetCurrentTabId(); 340 if (tab_id >= 0) 341 action->PaintBadge(canvas, gfx::Rect(width(), height()), tab_id); 342 } 343 344 //////////////////////////////////////////////////////////////////////////////// 345 // BrowserActionsContainer 346 347 BrowserActionsContainer::BrowserActionsContainer(Browser* browser, 348 View* owner_view) 349 : profile_(browser->profile()), 350 browser_(browser), 351 owner_view_(owner_view), 352 popup_(NULL), 353 popup_button_(NULL), 354 model_(NULL), 355 container_width_(0), 356 chevron_(NULL), 357 overflow_menu_(NULL), 358 suppress_chevron_(false), 359 resize_amount_(0), 360 animation_target_size_(0), 361 drop_indicator_position_(-1), 362 ALLOW_THIS_IN_INITIALIZER_LIST(task_factory_(this)), 363 ALLOW_THIS_IN_INITIALIZER_LIST(show_menu_task_factory_(this)) { 364 SetID(VIEW_ID_BROWSER_ACTION_TOOLBAR); 365 366 if (profile_->GetExtensionService()) { 367 model_ = profile_->GetExtensionService()->toolbar_model(); 368 model_->AddObserver(this); 369 } 370 371 resize_animation_.reset(new ui::SlideAnimation(this)); 372 resize_area_ = new views::ResizeArea(this); 373 AddChildView(resize_area_); 374 375 chevron_ = new views::MenuButton(NULL, std::wstring(), this, false); 376 chevron_->set_border(NULL); 377 chevron_->EnableCanvasFlippingForRTLUI(true); 378 chevron_->SetAccessibleName( 379 l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS_CHEVRON)); 380 chevron_->SetVisible(false); 381 AddChildView(chevron_); 382 } 383 384 BrowserActionsContainer::~BrowserActionsContainer() { 385 if (model_) 386 model_->RemoveObserver(this); 387 StopShowFolderDropMenuTimer(); 388 HidePopup(); 389 DeleteBrowserActionViews(); 390 } 391 392 // Static. 393 void BrowserActionsContainer::RegisterUserPrefs(PrefService* prefs) { 394 prefs->RegisterIntegerPref(prefs::kBrowserActionContainerWidth, 0); 395 } 396 397 void BrowserActionsContainer::Init() { 398 LoadImages(); 399 400 // We wait to set the container width until now so that the chevron images 401 // will be loaded. The width calculation needs to know the chevron size. 402 if (model_ && 403 !profile_->GetPrefs()->HasPrefPath(prefs::kExtensionToolbarSize)) { 404 // Migration code to the new VisibleIconCount pref. 405 // TODO(mpcomplete): remove this after users are upgraded to 5.0. 406 int predefined_width = 407 profile_->GetPrefs()->GetInteger(prefs::kBrowserActionContainerWidth); 408 if (predefined_width != 0) 409 model_->SetVisibleIconCount(WidthToIconCount(predefined_width)); 410 } 411 if (model_ && model_->extensions_initialized()) 412 SetContainerWidth(); 413 } 414 415 int BrowserActionsContainer::GetCurrentTabId() const { 416 TabContents* tab_contents = browser_->GetSelectedTabContents(); 417 return tab_contents ? tab_contents->controller().session_id().id() : -1; 418 } 419 420 BrowserActionView* BrowserActionsContainer::GetBrowserActionView( 421 ExtensionAction* action) { 422 for (BrowserActionViews::iterator iter = browser_action_views_.begin(); 423 iter != browser_action_views_.end(); ++iter) { 424 if ((*iter)->button()->browser_action() == action) 425 return *iter; 426 } 427 return NULL; 428 } 429 430 void BrowserActionsContainer::RefreshBrowserActionViews() { 431 for (size_t i = 0; i < browser_action_views_.size(); ++i) 432 browser_action_views_[i]->button()->UpdateState(); 433 } 434 435 void BrowserActionsContainer::CreateBrowserActionViews() { 436 DCHECK(browser_action_views_.empty()); 437 if (!model_) 438 return; 439 440 for (ExtensionList::iterator iter = model_->begin(); iter != model_->end(); 441 ++iter) { 442 if (!ShouldDisplayBrowserAction(*iter)) 443 continue; 444 445 BrowserActionView* view = new BrowserActionView(*iter, this); 446 browser_action_views_.push_back(view); 447 AddChildView(view); 448 } 449 } 450 451 void BrowserActionsContainer::DeleteBrowserActionViews() { 452 if (!browser_action_views_.empty()) { 453 for (size_t i = 0; i < browser_action_views_.size(); ++i) 454 RemoveChildView(browser_action_views_[i]); 455 STLDeleteContainerPointers(browser_action_views_.begin(), 456 browser_action_views_.end()); 457 browser_action_views_.clear(); 458 } 459 } 460 461 void BrowserActionsContainer::OnBrowserActionVisibilityChanged() { 462 SetVisible(!browser_action_views_.empty()); 463 owner_view_->Layout(); 464 owner_view_->SchedulePaint(); 465 } 466 467 size_t BrowserActionsContainer::VisibleBrowserActions() const { 468 size_t visible_actions = 0; 469 for (size_t i = 0; i < browser_action_views_.size(); ++i) { 470 if (browser_action_views_[i]->IsVisible()) 471 ++visible_actions; 472 } 473 return visible_actions; 474 } 475 476 void BrowserActionsContainer::OnBrowserActionExecuted( 477 BrowserActionButton* button, 478 bool inspect_with_devtools) { 479 ExtensionAction* browser_action = button->browser_action(); 480 481 // Popups just display. No notification to the extension. 482 // TODO(erikkay): should there be? 483 if (!button->IsPopup()) { 484 ExtensionService* service = profile_->GetExtensionService(); 485 service->browser_event_router()->BrowserActionExecuted( 486 profile_, browser_action->extension_id(), browser_); 487 return; 488 } 489 490 // If we're showing the same popup, just hide it and return. 491 bool same_showing = popup_ && button == popup_button_; 492 493 // Always hide the current popup, even if it's not the same. 494 // Only one popup should be visible at a time. 495 HidePopup(); 496 497 if (same_showing) 498 return; 499 500 // We can get the execute event for browser actions that are not visible, 501 // since buttons can be activated from the overflow menu (chevron). In that 502 // case we show the popup as originating from the chevron. 503 View* reference_view = button->parent()->IsVisible() ? button : chevron_; 504 gfx::Point origin; 505 View::ConvertPointToScreen(reference_view, &origin); 506 gfx::Rect rect = reference_view->bounds(); 507 rect.set_origin(origin); 508 509 BubbleBorder::ArrowLocation arrow_location = base::i18n::IsRTL() ? 510 BubbleBorder::TOP_LEFT : BubbleBorder::TOP_RIGHT; 511 512 popup_ = ExtensionPopup::Show(button->GetPopupUrl(), browser_, rect, 513 arrow_location, inspect_with_devtools, 514 this); 515 popup_button_ = button; 516 popup_button_->SetButtonPushed(); 517 } 518 519 gfx::Size BrowserActionsContainer::GetPreferredSize() { 520 if (browser_action_views_.empty()) 521 return gfx::Size(ToolbarView::kStandardSpacing, 0); 522 523 // We calculate the size of the view by taking the current width and 524 // subtracting resize_amount_ (the latter represents how far the user is 525 // resizing the view or, if animating the snapping, how far to animate it). 526 // But we also clamp it to a minimum size and the maximum size, so that the 527 // container can never shrink too far or take up more space than it needs. In 528 // other words: ContainerMinSize() < width() - resize < ClampTo(MAX). 529 int clamped_width = std::min( 530 std::max(ContainerMinSize(), container_width_ - resize_amount_), 531 IconCountToWidth(-1, false)); 532 return gfx::Size(clamped_width, 0); 533 } 534 535 void BrowserActionsContainer::Layout() { 536 if (browser_action_views_.empty()) { 537 SetVisible(false); 538 return; 539 } 540 541 SetVisible(true); 542 resize_area_->SetBounds(0, ToolbarView::kVertSpacing, kItemSpacing, 543 IconHeight()); 544 545 // If the icons don't all fit, show the chevron (unless suppressed). 546 int max_x = GetPreferredSize().width(); 547 if ((IconCountToWidth(-1, false) > max_x) && !suppress_chevron_) { 548 chevron_->SetVisible(true); 549 gfx::Size chevron_size(chevron_->GetPreferredSize()); 550 max_x -= 551 ToolbarView::kStandardSpacing + chevron_size.width() + kChevronSpacing; 552 chevron_->SetBounds( 553 width() - ToolbarView::kStandardSpacing - chevron_size.width(), 554 ToolbarView::kVertSpacing, chevron_size.width(), chevron_size.height()); 555 } else { 556 chevron_->SetVisible(false); 557 } 558 559 // Now draw the icons for the browser actions in the available space. 560 int icon_width = IconWidth(false); 561 for (size_t i = 0; i < browser_action_views_.size(); ++i) { 562 BrowserActionView* view = browser_action_views_[i]; 563 int x = ToolbarView::kStandardSpacing + (i * IconWidth(true)); 564 if (x + icon_width <= max_x) { 565 view->SetBounds(x, 0, icon_width, height()); 566 view->SetVisible(true); 567 } else { 568 view->SetVisible(false); 569 } 570 } 571 } 572 573 bool BrowserActionsContainer::GetDropFormats( 574 int* formats, 575 std::set<OSExchangeData::CustomFormat>* custom_formats) { 576 custom_formats->insert(BrowserActionDragData::GetBrowserActionCustomFormat()); 577 578 return true; 579 } 580 581 bool BrowserActionsContainer::AreDropTypesRequired() { 582 return true; 583 } 584 585 bool BrowserActionsContainer::CanDrop(const OSExchangeData& data) { 586 BrowserActionDragData drop_data; 587 return drop_data.Read(data) ? drop_data.IsFromProfile(profile_) : false; 588 } 589 590 void BrowserActionsContainer::OnDragEntered( 591 const views::DropTargetEvent& event) { 592 } 593 594 int BrowserActionsContainer::OnDragUpdated( 595 const views::DropTargetEvent& event) { 596 // First check if we are above the chevron (overflow) menu. 597 if (GetEventHandlerForPoint(event.location()) == chevron_) { 598 if (show_menu_task_factory_.empty() && !overflow_menu_) 599 StartShowFolderDropMenuTimer(); 600 return ui::DragDropTypes::DRAG_MOVE; 601 } 602 StopShowFolderDropMenuTimer(); 603 604 // Figure out where to display the indicator. This is a complex calculation: 605 606 // First, we figure out how much space is to the left of the icon area, so we 607 // can calculate the true offset into the icon area. 608 int width_before_icons = ToolbarView::kStandardSpacing + 609 (base::i18n::IsRTL() ? 610 (chevron_->GetPreferredSize().width() + kChevronSpacing) : 0); 611 int offset_into_icon_area = event.x() - width_before_icons; 612 613 // Next, we determine which icon to place the indicator in front of. We want 614 // to place the indicator in front of icon n when the cursor is between the 615 // midpoints of icons (n - 1) and n. To do this we take the offset into the 616 // icon area and transform it as follows: 617 // 618 // Real icon area: 619 // 0 a * b c 620 // | | | | 621 // |[IC|ON] [IC|ON] [IC|ON] 622 // We want to be before icon 0 for 0 < x <= a, icon 1 for a < x <= b, etc. 623 // Here the "*" represents the offset into the icon area, and since it's 624 // between a and b, we want to return "1". 625 // 626 // Transformed "icon area": 627 // 0 a * b c 628 // | | | | 629 // |[ICON] |[ICON] |[ICON] | 630 // If we shift both our offset and our divider points later by half an icon 631 // plus one spacing unit, then it becomes very easy to calculate how many 632 // divider points we've passed, because they're the multiples of "one icon 633 // plus padding". 634 int before_icon_unclamped = (offset_into_icon_area + (IconWidth(false) / 2) + 635 kItemSpacing) / IconWidth(true); 636 637 // Because the user can drag outside the container bounds, we need to clamp to 638 // the valid range. Note that the maximum allowable value is (num icons), not 639 // (num icons - 1), because we represent the indicator being past the last 640 // icon as being "before the (last + 1) icon". 641 int before_icon = std::min(std::max(before_icon_unclamped, 0), 642 static_cast<int>(VisibleBrowserActions())); 643 644 // Now we convert back to a pixel offset into the container. We want to place 645 // the center of the drop indicator at the midpoint of the space before our 646 // chosen icon. 647 SetDropIndicator(width_before_icons + (before_icon * IconWidth(true)) - 648 (kItemSpacing / 2)); 649 650 return ui::DragDropTypes::DRAG_MOVE; 651 } 652 653 void BrowserActionsContainer::OnDragExited() { 654 StopShowFolderDropMenuTimer(); 655 drop_indicator_position_ = -1; 656 SchedulePaint(); 657 } 658 659 int BrowserActionsContainer::OnPerformDrop( 660 const views::DropTargetEvent& event) { 661 BrowserActionDragData data; 662 if (!data.Read(event.data())) 663 return ui::DragDropTypes::DRAG_NONE; 664 665 // Make sure we have the same view as we started with. 666 DCHECK_EQ(browser_action_views_[data.index()]->button()->extension()->id(), 667 data.id()); 668 DCHECK(model_); 669 670 size_t i = 0; 671 for (; i < browser_action_views_.size(); ++i) { 672 int view_x = browser_action_views_[i]->GetMirroredBounds().x(); 673 if (!browser_action_views_[i]->IsVisible() || 674 (base::i18n::IsRTL() ? (view_x < drop_indicator_position_) : 675 (view_x >= drop_indicator_position_))) { 676 // We have reached the end of the visible icons or found one that has a 677 // higher x position than the drop point. 678 break; 679 } 680 } 681 682 // |i| now points to the item to the right of the drop indicator*, which is 683 // correct when dragging an icon to the left. When dragging to the right, 684 // however, we want the icon being dragged to get the index of the item to 685 // the left of the drop indicator, so we subtract one. 686 // * Well, it can also point to the end, but not when dragging to the left. :) 687 if (i > data.index()) 688 --i; 689 690 if (profile_->IsOffTheRecord()) 691 i = model_->IncognitoIndexToOriginal(i); 692 693 model_->MoveBrowserAction( 694 browser_action_views_[data.index()]->button()->extension(), i); 695 696 OnDragExited(); // Perform clean up after dragging. 697 return ui::DragDropTypes::DRAG_MOVE; 698 } 699 700 void BrowserActionsContainer::GetAccessibleState( 701 ui::AccessibleViewState* state) { 702 state->role = ui::AccessibilityTypes::ROLE_GROUPING; 703 state->name = l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS); 704 } 705 706 void BrowserActionsContainer::RunMenu(View* source, const gfx::Point& pt) { 707 if (source == chevron_) { 708 overflow_menu_ = new BrowserActionOverflowMenuController( 709 this, chevron_, browser_action_views_, VisibleBrowserActions()); 710 overflow_menu_->set_observer(this); 711 overflow_menu_->RunMenu(GetWindow()->GetNativeWindow(), false); 712 } 713 } 714 715 void BrowserActionsContainer::WriteDragDataForView(View* sender, 716 const gfx::Point& press_pt, 717 OSExchangeData* data) { 718 DCHECK(data); 719 720 for (size_t i = 0; i < browser_action_views_.size(); ++i) { 721 BrowserActionButton* button = browser_action_views_[i]->button(); 722 if (button == sender) { 723 // Set the dragging image for the icon. 724 scoped_ptr<gfx::Canvas> canvas( 725 browser_action_views_[i]->GetIconWithBadge()); 726 drag_utils::SetDragImageOnDataObject(*canvas, button->size(), press_pt, 727 data); 728 729 // Fill in the remaining info. 730 BrowserActionDragData drag_data( 731 browser_action_views_[i]->button()->extension()->id(), i); 732 drag_data.Write(profile_, data); 733 break; 734 } 735 } 736 } 737 738 int BrowserActionsContainer::GetDragOperationsForView(View* sender, 739 const gfx::Point& p) { 740 return ui::DragDropTypes::DRAG_MOVE; 741 } 742 743 bool BrowserActionsContainer::CanStartDragForView(View* sender, 744 const gfx::Point& press_pt, 745 const gfx::Point& p) { 746 return true; 747 } 748 749 void BrowserActionsContainer::OnResize(int resize_amount, bool done_resizing) { 750 if (!done_resizing) { 751 resize_amount_ = resize_amount; 752 OnBrowserActionVisibilityChanged(); 753 return; 754 } 755 756 // Up until now we've only been modifying the resize_amount, but now it is 757 // time to set the container size to the size we have resized to, and then 758 // animate to the nearest icon count size if necessary (which may be 0). 759 int max_width = IconCountToWidth(-1, false); 760 container_width_ = 761 std::min(std::max(0, container_width_ - resize_amount), max_width); 762 SaveDesiredSizeAndAnimate(ui::Tween::EASE_OUT, 763 WidthToIconCount(container_width_)); 764 } 765 766 void BrowserActionsContainer::AnimationProgressed( 767 const ui::Animation* animation) { 768 DCHECK_EQ(resize_animation_.get(), animation); 769 resize_amount_ = static_cast<int>(resize_animation_->GetCurrentValue() * 770 (container_width_ - animation_target_size_)); 771 OnBrowserActionVisibilityChanged(); 772 } 773 774 void BrowserActionsContainer::AnimationEnded(const ui::Animation* animation) { 775 container_width_ = animation_target_size_; 776 animation_target_size_ = 0; 777 resize_amount_ = 0; 778 OnBrowserActionVisibilityChanged(); 779 suppress_chevron_ = false; 780 } 781 782 void BrowserActionsContainer::NotifyMenuDeleted( 783 BrowserActionOverflowMenuController* controller) { 784 DCHECK(controller == overflow_menu_); 785 overflow_menu_ = NULL; 786 } 787 788 void BrowserActionsContainer::InspectPopup(ExtensionAction* action) { 789 OnBrowserActionExecuted(GetBrowserActionView(action)->button(), true); 790 } 791 792 void BrowserActionsContainer::ExtensionPopupIsClosing(ExtensionPopup* popup) { 793 // ExtensionPopup is ref-counted, so we don't need to delete it. 794 DCHECK_EQ(popup_, popup); 795 popup_ = NULL; 796 popup_button_->SetButtonNotPushed(); 797 popup_button_ = NULL; 798 } 799 800 void BrowserActionsContainer::MoveBrowserAction(const std::string& extension_id, 801 size_t new_index) { 802 ExtensionService* service = profile_->GetExtensionService(); 803 if (service) { 804 const Extension* extension = service->GetExtensionById(extension_id, false); 805 model_->MoveBrowserAction(extension, new_index); 806 SchedulePaint(); 807 } 808 } 809 810 void BrowserActionsContainer::HidePopup() { 811 if (popup_) 812 popup_->Close(); 813 } 814 815 void BrowserActionsContainer::TestExecuteBrowserAction(int index) { 816 BrowserActionButton* button = browser_action_views_[index]->button(); 817 OnBrowserActionExecuted(button, false); 818 } 819 820 void BrowserActionsContainer::TestSetIconVisibilityCount(size_t icons) { 821 model_->SetVisibleIconCount(icons); 822 chevron_->SetVisible(icons < browser_action_views_.size()); 823 container_width_ = IconCountToWidth(icons, chevron_->IsVisible()); 824 Layout(); 825 SchedulePaint(); 826 } 827 828 void BrowserActionsContainer::OnPaint(gfx::Canvas* canvas) { 829 // TODO(sky/glen): Instead of using a drop indicator, animate the icons while 830 // dragging (like we do for tab dragging). 831 if (drop_indicator_position_ > -1) { 832 // The two-pixel width drop indicator. 833 static const int kDropIndicatorWidth = 2; 834 gfx::Rect indicator_bounds( 835 drop_indicator_position_ - (kDropIndicatorWidth / 2), 836 ToolbarView::kVertSpacing, kDropIndicatorWidth, IconHeight()); 837 838 // Color of the drop indicator. 839 static const SkColor kDropIndicatorColor = SK_ColorBLACK; 840 canvas->FillRectInt(kDropIndicatorColor, indicator_bounds.x(), 841 indicator_bounds.y(), indicator_bounds.width(), 842 indicator_bounds.height()); 843 } 844 } 845 846 void BrowserActionsContainer::OnThemeChanged() { 847 LoadImages(); 848 } 849 850 void BrowserActionsContainer::ViewHierarchyChanged(bool is_add, 851 views::View* parent, 852 views::View* child) { 853 // No extensions (e.g., incognito). 854 if (!model_) 855 return; 856 857 if (is_add && child == this) { 858 // Initial toolbar button creation and placement in the widget hierarchy. 859 // We do this here instead of in the constructor because AddBrowserAction 860 // calls Layout on the Toolbar, which needs this object to be constructed 861 // before its Layout function is called. 862 CreateBrowserActionViews(); 863 } 864 } 865 866 // static 867 int BrowserActionsContainer::IconWidth(bool include_padding) { 868 static bool initialized = false; 869 static int icon_width = 0; 870 if (!initialized) { 871 initialized = true; 872 icon_width = ResourceBundle::GetSharedInstance().GetBitmapNamed( 873 IDR_BROWSER_ACTION)->width(); 874 } 875 return icon_width + (include_padding ? kItemSpacing : 0); 876 } 877 878 // static 879 int BrowserActionsContainer::IconHeight() { 880 static bool initialized = false; 881 static int icon_height = 0; 882 if (!initialized) { 883 initialized = true; 884 icon_height = ResourceBundle::GetSharedInstance().GetBitmapNamed( 885 IDR_BROWSER_ACTION)->height(); 886 } 887 return icon_height; 888 } 889 890 void BrowserActionsContainer::BrowserActionAdded(const Extension* extension, 891 int index) { 892 #if defined(DEBUG) 893 for (size_t i = 0; i < browser_action_views_.size(); ++i) { 894 DCHECK(browser_action_views_[i]->button()->extension() != extension) << 895 "Asked to add a browser action view for an extension that already " 896 "exists."; 897 } 898 #endif 899 CloseOverflowMenu(); 900 901 if (!ShouldDisplayBrowserAction(extension)) 902 return; 903 904 size_t visible_actions = VisibleBrowserActions(); 905 906 // Add the new browser action to the vector and the view hierarchy. 907 if (profile_->IsOffTheRecord()) 908 index = model_->OriginalIndexToIncognito(index); 909 BrowserActionView* view = new BrowserActionView(extension, this); 910 browser_action_views_.insert(browser_action_views_.begin() + index, view); 911 AddChildViewAt(view, index); 912 913 // If we are still initializing the container, don't bother animating. 914 if (!model_->extensions_initialized()) 915 return; 916 917 // Enlarge the container if it was already at maximum size and we're not in 918 // the middle of upgrading. 919 if ((model_->GetVisibleIconCount() < 0) && 920 !profile_->GetExtensionService()->IsBeingUpgraded(extension)) { 921 suppress_chevron_ = true; 922 SaveDesiredSizeAndAnimate(ui::Tween::LINEAR, visible_actions + 1); 923 } else { 924 // Just redraw the (possibly modified) visible icon set. 925 OnBrowserActionVisibilityChanged(); 926 } 927 } 928 929 void BrowserActionsContainer::BrowserActionRemoved(const Extension* extension) { 930 CloseOverflowMenu(); 931 932 if (popup_ && popup_->host()->extension() == extension) 933 HidePopup(); 934 935 size_t visible_actions = VisibleBrowserActions(); 936 for (BrowserActionViews::iterator iter = browser_action_views_.begin(); 937 iter != browser_action_views_.end(); ++iter) { 938 if ((*iter)->button()->extension() == extension) { 939 RemoveChildView(*iter); 940 delete *iter; 941 browser_action_views_.erase(iter); 942 943 // If the extension is being upgraded we don't want the bar to shrink 944 // because the icon is just going to get re-added to the same location. 945 if (profile_->GetExtensionService()->IsBeingUpgraded(extension)) 946 return; 947 948 if (browser_action_views_.size() > visible_actions) { 949 // If we have more icons than we can show, then we must not be changing 950 // the container size (since we either removed an icon from the main 951 // area and one from the overflow list will have shifted in, or we 952 // removed an entry directly from the overflow list). 953 OnBrowserActionVisibilityChanged(); 954 } else { 955 // Either we went from overflow to no-overflow, or we shrunk the no- 956 // overflow container by 1. Either way the size changed, so animate. 957 chevron_->SetVisible(false); 958 SaveDesiredSizeAndAnimate(ui::Tween::EASE_OUT, 959 browser_action_views_.size()); 960 } 961 return; 962 } 963 } 964 } 965 966 void BrowserActionsContainer::BrowserActionMoved(const Extension* extension, 967 int index) { 968 if (!ShouldDisplayBrowserAction(extension)) 969 return; 970 971 if (profile_->IsOffTheRecord()) 972 index = model_->OriginalIndexToIncognito(index); 973 974 DCHECK(index >= 0 && index < static_cast<int>(browser_action_views_.size())); 975 976 DeleteBrowserActionViews(); 977 CreateBrowserActionViews(); 978 Layout(); 979 SchedulePaint(); 980 } 981 982 void BrowserActionsContainer::ModelLoaded() { 983 SetContainerWidth(); 984 } 985 986 void BrowserActionsContainer::LoadImages() { 987 ui::ThemeProvider* tp = GetThemeProvider(); 988 chevron_->SetIcon(*tp->GetBitmapNamed(IDR_BROWSER_ACTIONS_OVERFLOW)); 989 chevron_->SetHoverIcon(*tp->GetBitmapNamed(IDR_BROWSER_ACTIONS_OVERFLOW_H)); 990 chevron_->SetPushedIcon(*tp->GetBitmapNamed(IDR_BROWSER_ACTIONS_OVERFLOW_P)); 991 } 992 993 void BrowserActionsContainer::SetContainerWidth() { 994 int visible_actions = model_->GetVisibleIconCount(); 995 if (visible_actions < 0) // All icons should be visible. 996 visible_actions = model_->size(); 997 chevron_->SetVisible(static_cast<size_t>(visible_actions) < model_->size()); 998 container_width_ = IconCountToWidth(visible_actions, chevron_->IsVisible()); 999 } 1000 1001 void BrowserActionsContainer::CloseOverflowMenu() { 1002 if (overflow_menu_) 1003 overflow_menu_->CancelMenu(); 1004 } 1005 1006 void BrowserActionsContainer::StopShowFolderDropMenuTimer() { 1007 show_menu_task_factory_.RevokeAll(); 1008 } 1009 1010 void BrowserActionsContainer::StartShowFolderDropMenuTimer() { 1011 int delay = views::GetMenuShowDelay(); 1012 MessageLoop::current()->PostDelayedTask(FROM_HERE, 1013 show_menu_task_factory_.NewRunnableMethod( 1014 &BrowserActionsContainer::ShowDropFolder), 1015 delay); 1016 } 1017 1018 void BrowserActionsContainer::ShowDropFolder() { 1019 DCHECK(!overflow_menu_); 1020 SetDropIndicator(-1); 1021 overflow_menu_ = new BrowserActionOverflowMenuController( 1022 this, chevron_, browser_action_views_, VisibleBrowserActions()); 1023 overflow_menu_->set_observer(this); 1024 overflow_menu_->RunMenu(GetWindow()->GetNativeWindow(), true); 1025 } 1026 1027 void BrowserActionsContainer::SetDropIndicator(int x_pos) { 1028 if (drop_indicator_position_ != x_pos) { 1029 drop_indicator_position_ = x_pos; 1030 SchedulePaint(); 1031 } 1032 } 1033 1034 int BrowserActionsContainer::IconCountToWidth(int icons, 1035 bool display_chevron) const { 1036 if (icons < 0) 1037 icons = browser_action_views_.size(); 1038 if ((icons == 0) && !display_chevron) 1039 return ToolbarView::kStandardSpacing; 1040 int icons_size = 1041 (icons == 0) ? 0 : ((icons * IconWidth(true)) - kItemSpacing); 1042 int chevron_size = display_chevron ? 1043 (kChevronSpacing + chevron_->GetPreferredSize().width()) : 0; 1044 return ToolbarView::kStandardSpacing + icons_size + chevron_size + 1045 ToolbarView::kStandardSpacing; 1046 } 1047 1048 size_t BrowserActionsContainer::WidthToIconCount(int pixels) const { 1049 // Check for widths large enough to show the entire icon set. 1050 if (pixels >= IconCountToWidth(-1, false)) 1051 return browser_action_views_.size(); 1052 1053 // We need to reserve space for the resize area, chevron, and the spacing on 1054 // either side of the chevron. 1055 int available_space = pixels - ToolbarView::kStandardSpacing - 1056 chevron_->GetPreferredSize().width() - kChevronSpacing - 1057 ToolbarView::kStandardSpacing; 1058 // Now we add an extra between-item padding value so the space can be divided 1059 // evenly by (size of icon with padding). 1060 return static_cast<size_t>( 1061 std::max(0, available_space + kItemSpacing) / IconWidth(true)); 1062 } 1063 1064 int BrowserActionsContainer::ContainerMinSize() const { 1065 return ToolbarView::kStandardSpacing + kChevronSpacing + 1066 chevron_->GetPreferredSize().width() + ToolbarView::kStandardSpacing; 1067 } 1068 1069 void BrowserActionsContainer::SaveDesiredSizeAndAnimate( 1070 ui::Tween::Type tween_type, 1071 size_t num_visible_icons) { 1072 // Save off the desired number of visible icons. We do this now instead of at 1073 // the end of the animation so that even if the browser is shut down while 1074 // animating, the right value will be restored on next run. 1075 // NOTE: Don't save the icon count in incognito because there may be fewer 1076 // icons in that mode. The result is that the container in a normal window is 1077 // always at least as wide as in an incognito window. 1078 if (!profile_->IsOffTheRecord()) 1079 model_->SetVisibleIconCount(num_visible_icons); 1080 1081 int target_size = IconCountToWidth(num_visible_icons, 1082 num_visible_icons < browser_action_views_.size()); 1083 if (!disable_animations_during_testing_) { 1084 // Animate! We have to set the animation_target_size_ after calling Reset(), 1085 // because that could end up calling AnimationEnded which clears the value. 1086 resize_animation_->Reset(); 1087 resize_animation_->SetTweenType(tween_type); 1088 animation_target_size_ = target_size; 1089 resize_animation_->Show(); 1090 } else { 1091 animation_target_size_ = target_size; 1092 AnimationEnded(resize_animation_.get()); 1093 } 1094 } 1095 1096 bool BrowserActionsContainer::ShouldDisplayBrowserAction( 1097 const Extension* extension) { 1098 // Only display incognito-enabled extensions while in incognito mode. 1099 return 1100 (!profile_->IsOffTheRecord() || 1101 profile_->GetExtensionService()->IsIncognitoEnabled(extension->id())); 1102 } 1103