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_actions_container.h" 6 7 #include "base/compiler_specific.h" 8 #include "base/prefs/pref_service.h" 9 #include "base/stl_util.h" 10 #include "chrome/browser/extensions/extension_service.h" 11 #include "chrome/browser/extensions/extension_util.h" 12 #include "chrome/browser/extensions/extension_view_host.h" 13 #include "chrome/browser/extensions/tab_helper.h" 14 #include "chrome/browser/profiles/profile.h" 15 #include "chrome/browser/sessions/session_tab_helper.h" 16 #include "chrome/browser/ui/browser.h" 17 #include "chrome/browser/ui/browser_window.h" 18 #include "chrome/browser/ui/tabs/tab_strip_model.h" 19 #include "chrome/browser/ui/view_ids.h" 20 #include "chrome/browser/ui/views/extensions/browser_action_drag_data.h" 21 #include "chrome/browser/ui/views/extensions/extension_keybinding_registry_views.h" 22 #include "chrome/browser/ui/views/extensions/extension_popup.h" 23 #include "chrome/browser/ui/views/toolbar/browser_action_view.h" 24 #include "chrome/browser/ui/views/toolbar/toolbar_view.h" 25 #include "chrome/common/extensions/command.h" 26 #include "chrome/common/pref_names.h" 27 #include "extensions/browser/extension_system.h" 28 #include "extensions/browser/pref_names.h" 29 #include "extensions/browser/runtime_data.h" 30 #include "grit/generated_resources.h" 31 #include "grit/theme_resources.h" 32 #include "grit/ui_resources.h" 33 #include "third_party/skia/include/core/SkColor.h" 34 #include "ui/accessibility/ax_view_state.h" 35 #include "ui/base/dragdrop/drag_utils.h" 36 #include "ui/base/l10n/l10n_util.h" 37 #include "ui/base/nine_image_painter_factory.h" 38 #include "ui/base/resource/resource_bundle.h" 39 #include "ui/base/theme_provider.h" 40 #include "ui/gfx/animation/slide_animation.h" 41 #include "ui/gfx/canvas.h" 42 #include "ui/gfx/geometry/rect.h" 43 #include "ui/views/controls/button/label_button_border.h" 44 #include "ui/views/controls/resize_area.h" 45 #include "ui/views/metrics.h" 46 #include "ui/views/painter.h" 47 #include "ui/views/widget/widget.h" 48 49 using extensions::Extension; 50 51 namespace { 52 53 // Horizontal spacing between most items in the container, as well as after the 54 // last item or chevron (if visible). 55 const int kItemSpacing = ToolbarView::kStandardSpacing; 56 57 // Horizontal spacing before the chevron (if visible). 58 const int kChevronSpacing = kItemSpacing - 2; 59 60 // A version of MenuButton with almost empty insets to fit properly on the 61 // toolbar. 62 class ChevronMenuButton : public views::MenuButton { 63 public: 64 ChevronMenuButton(views::ButtonListener* listener, 65 const base::string16& text, 66 views::MenuButtonListener* menu_button_listener, 67 bool show_menu_marker) 68 : views::MenuButton(listener, 69 text, 70 menu_button_listener, 71 show_menu_marker) { 72 } 73 74 virtual ~ChevronMenuButton() {} 75 76 virtual scoped_ptr<views::LabelButtonBorder> CreateDefaultBorder() const 77 OVERRIDE { 78 // The chevron resource was designed to not have any insets. 79 scoped_ptr<views::LabelButtonBorder> border = 80 views::MenuButton::CreateDefaultBorder(); 81 border->set_insets(gfx::Insets()); 82 return border.Pass(); 83 } 84 85 private: 86 DISALLOW_COPY_AND_ASSIGN(ChevronMenuButton); 87 }; 88 89 } // namespace 90 91 // static 92 bool BrowserActionsContainer::disable_animations_during_testing_ = false; 93 94 //////////////////////////////////////////////////////////////////////////////// 95 // BrowserActionsContainer 96 97 BrowserActionsContainer::BrowserActionsContainer(Browser* browser, 98 View* owner_view) 99 : profile_(browser->profile()), 100 browser_(browser), 101 owner_view_(owner_view), 102 popup_(NULL), 103 popup_button_(NULL), 104 model_(NULL), 105 container_width_(0), 106 chevron_(NULL), 107 overflow_menu_(NULL), 108 suppress_chevron_(false), 109 resize_amount_(0), 110 animation_target_size_(0), 111 drop_indicator_position_(-1), 112 task_factory_(this), 113 show_menu_task_factory_(this) { 114 set_id(VIEW_ID_BROWSER_ACTION_TOOLBAR); 115 116 model_ = extensions::ExtensionToolbarModel::Get(browser->profile()); 117 if (model_) 118 model_->AddObserver(this); 119 120 extension_keybinding_registry_.reset(new ExtensionKeybindingRegistryViews( 121 browser->profile(), 122 owner_view->GetFocusManager(), 123 extensions::ExtensionKeybindingRegistry::ALL_EXTENSIONS, 124 this)); 125 126 resize_animation_.reset(new gfx::SlideAnimation(this)); 127 resize_area_ = new views::ResizeArea(this); 128 AddChildView(resize_area_); 129 130 chevron_ = new ChevronMenuButton(NULL, base::string16(), this, false); 131 chevron_->EnableCanvasFlippingForRTLUI(true); 132 chevron_->SetAccessibleName( 133 l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS_CHEVRON)); 134 chevron_->SetVisible(false); 135 AddChildView(chevron_); 136 } 137 138 BrowserActionsContainer::~BrowserActionsContainer() { 139 FOR_EACH_OBSERVER(BrowserActionsContainerObserver, 140 observers_, 141 OnBrowserActionsContainerDestroyed()); 142 143 if (overflow_menu_) 144 overflow_menu_->set_observer(NULL); 145 if (model_) 146 model_->RemoveObserver(this); 147 StopShowFolderDropMenuTimer(); 148 if (popup_) 149 popup_->GetWidget()->RemoveObserver(this); 150 HidePopup(); 151 DeleteBrowserActionViews(); 152 } 153 154 void BrowserActionsContainer::Init() { 155 LoadImages(); 156 157 // We wait to set the container width until now so that the chevron images 158 // will be loaded. The width calculation needs to know the chevron size. 159 if (model_ && 160 !profile_->GetPrefs()->HasPrefPath( 161 extensions::pref_names::kToolbarSize)) { 162 // Migration code to the new VisibleIconCount pref. 163 // TODO(mpcomplete): remove this after users are upgraded to 5.0. 164 int predefined_width = profile_->GetPrefs()->GetInteger( 165 extensions::pref_names::kBrowserActionContainerWidth); 166 if (predefined_width != 0) 167 model_->SetVisibleIconCount(WidthToIconCount(predefined_width)); 168 } 169 if (model_ && model_->extensions_initialized()) 170 SetContainerWidth(); 171 } 172 173 BrowserActionView* BrowserActionsContainer::GetBrowserActionView( 174 ExtensionAction* action) { 175 for (BrowserActionViews::iterator i(browser_action_views_.begin()); 176 i != browser_action_views_.end(); ++i) { 177 if ((*i)->button()->browser_action() == action) 178 return *i; 179 } 180 return NULL; 181 } 182 183 void BrowserActionsContainer::RefreshBrowserActionViews() { 184 for (size_t i = 0; i < browser_action_views_.size(); ++i) 185 browser_action_views_[i]->button()->UpdateState(); 186 } 187 188 void BrowserActionsContainer::CreateBrowserActionViews() { 189 DCHECK(browser_action_views_.empty()); 190 if (!model_) 191 return; 192 193 const extensions::ExtensionList& toolbar_items = model_->toolbar_items(); 194 for (extensions::ExtensionList::const_iterator i(toolbar_items.begin()); 195 i != toolbar_items.end(); ++i) { 196 if (!ShouldDisplayBrowserAction(i->get())) 197 continue; 198 199 BrowserActionView* view = new BrowserActionView(i->get(), browser_, this); 200 browser_action_views_.push_back(view); 201 AddChildView(view); 202 } 203 } 204 205 void BrowserActionsContainer::DeleteBrowserActionViews() { 206 HidePopup(); 207 STLDeleteElements(&browser_action_views_); 208 } 209 210 size_t BrowserActionsContainer::VisibleBrowserActions() const { 211 size_t visible_actions = 0; 212 for (size_t i = 0; i < browser_action_views_.size(); ++i) { 213 if (browser_action_views_[i]->visible()) 214 ++visible_actions; 215 } 216 return visible_actions; 217 } 218 219 size_t BrowserActionsContainer::VisibleBrowserActionsAfterAnimation() const { 220 if (!animating()) 221 return VisibleBrowserActions(); 222 223 return WidthToIconCount(animation_target_size_); 224 } 225 226 void BrowserActionsContainer::ExecuteExtensionCommand( 227 const extensions::Extension* extension, 228 const extensions::Command& command) { 229 // Global commands are handled by the ExtensionCommandsGlobalRegistry 230 // instance. 231 DCHECK(!command.global()); 232 extension_keybinding_registry_->ExecuteCommand(extension->id(), 233 command.accelerator()); 234 } 235 236 void BrowserActionsContainer::AddObserver( 237 BrowserActionsContainerObserver* observer) { 238 observers_.AddObserver(observer); 239 } 240 241 void BrowserActionsContainer::RemoveObserver( 242 BrowserActionsContainerObserver* observer) { 243 observers_.RemoveObserver(observer); 244 } 245 246 gfx::Size BrowserActionsContainer::GetPreferredSize() const { 247 // We calculate the size of the view by taking the current width and 248 // subtracting resize_amount_ (the latter represents how far the user is 249 // resizing the view or, if animating the snapping, how far to animate it). 250 // But we also clamp it to a minimum size and the maximum size, so that the 251 // container can never shrink too far or take up more space than it needs. In 252 // other words: MinimumNonemptyWidth() < width() - resize < ClampTo(MAX). 253 int preferred_width = std::min( 254 std::max(MinimumNonemptyWidth(), container_width_ - resize_amount_), 255 IconCountToWidth(-1, false)); 256 // Height will be ignored by the ToolbarView. 257 return gfx::Size(preferred_width, 0); 258 } 259 260 gfx::Size BrowserActionsContainer::GetMinimumSize() const { 261 int min_width = std::min(MinimumNonemptyWidth(), IconCountToWidth(-1, false)); 262 // Height will be ignored by the ToolbarView. 263 return gfx::Size(min_width, 0); 264 } 265 266 void BrowserActionsContainer::Layout() { 267 if (browser_action_views_.empty()) { 268 SetVisible(false); 269 return; 270 } 271 272 SetVisible(true); 273 resize_area_->SetBounds(0, 0, kItemSpacing, height()); 274 275 // If the icons don't all fit, show the chevron (unless suppressed). 276 int max_x = GetPreferredSize().width(); 277 if ((IconCountToWidth(-1, false) > max_x) && !suppress_chevron_) { 278 chevron_->SetVisible(true); 279 gfx::Size chevron_size(chevron_->GetPreferredSize()); 280 max_x -= 281 ToolbarView::kStandardSpacing + chevron_size.width() + kChevronSpacing; 282 chevron_->SetBounds( 283 width() - ToolbarView::kStandardSpacing - chevron_size.width(), 284 0, 285 chevron_size.width(), 286 chevron_size.height()); 287 } else { 288 chevron_->SetVisible(false); 289 } 290 291 // Now draw the icons for the browser actions in the available space. 292 int icon_width = IconWidth(false); 293 for (size_t i = 0; i < browser_action_views_.size(); ++i) { 294 BrowserActionView* view = browser_action_views_[i]; 295 int x = ToolbarView::kStandardSpacing + (i * IconWidth(true)); 296 if (x + icon_width <= max_x) { 297 view->SetBounds(x, 0, icon_width, height()); 298 view->SetVisible(true); 299 } else { 300 view->SetVisible(false); 301 } 302 } 303 } 304 305 bool BrowserActionsContainer::GetDropFormats( 306 int* formats, 307 std::set<OSExchangeData::CustomFormat>* custom_formats) { 308 custom_formats->insert(BrowserActionDragData::GetBrowserActionCustomFormat()); 309 310 return true; 311 } 312 313 bool BrowserActionsContainer::AreDropTypesRequired() { 314 return true; 315 } 316 317 bool BrowserActionsContainer::CanDrop(const OSExchangeData& data) { 318 BrowserActionDragData drop_data; 319 return drop_data.Read(data) ? drop_data.IsFromProfile(profile_) : false; 320 } 321 322 void BrowserActionsContainer::OnDragEntered( 323 const ui::DropTargetEvent& event) { 324 } 325 326 int BrowserActionsContainer::OnDragUpdated( 327 const ui::DropTargetEvent& event) { 328 // First check if we are above the chevron (overflow) menu. 329 if (GetEventHandlerForPoint(event.location()) == chevron_) { 330 if (!show_menu_task_factory_.HasWeakPtrs() && !overflow_menu_) 331 StartShowFolderDropMenuTimer(); 332 return ui::DragDropTypes::DRAG_MOVE; 333 } 334 StopShowFolderDropMenuTimer(); 335 336 // Figure out where to display the indicator. This is a complex calculation: 337 338 // First, we figure out how much space is to the left of the icon area, so we 339 // can calculate the true offset into the icon area. 340 int width_before_icons = ToolbarView::kStandardSpacing + 341 (base::i18n::IsRTL() ? 342 (chevron_->GetPreferredSize().width() + kChevronSpacing) : 0); 343 int offset_into_icon_area = event.x() - width_before_icons; 344 345 // Next, we determine which icon to place the indicator in front of. We want 346 // to place the indicator in front of icon n when the cursor is between the 347 // midpoints of icons (n - 1) and n. To do this we take the offset into the 348 // icon area and transform it as follows: 349 // 350 // Real icon area: 351 // 0 a * b c 352 // | | | | 353 // |[IC|ON] [IC|ON] [IC|ON] 354 // We want to be before icon 0 for 0 < x <= a, icon 1 for a < x <= b, etc. 355 // Here the "*" represents the offset into the icon area, and since it's 356 // between a and b, we want to return "1". 357 // 358 // Transformed "icon area": 359 // 0 a * b c 360 // | | | | 361 // |[ICON] |[ICON] |[ICON] | 362 // If we shift both our offset and our divider points later by half an icon 363 // plus one spacing unit, then it becomes very easy to calculate how many 364 // divider points we've passed, because they're the multiples of "one icon 365 // plus padding". 366 int before_icon_unclamped = (offset_into_icon_area + (IconWidth(false) / 2) + 367 kItemSpacing) / IconWidth(true); 368 369 // Because the user can drag outside the container bounds, we need to clamp to 370 // the valid range. Note that the maximum allowable value is (num icons), not 371 // (num icons - 1), because we represent the indicator being past the last 372 // icon as being "before the (last + 1) icon". 373 int before_icon = std::min(std::max(before_icon_unclamped, 0), 374 static_cast<int>(VisibleBrowserActions())); 375 376 // Now we convert back to a pixel offset into the container. We want to place 377 // the center of the drop indicator at the midpoint of the space before our 378 // chosen icon. 379 SetDropIndicator(width_before_icons + (before_icon * IconWidth(true)) - 380 (kItemSpacing / 2)); 381 382 return ui::DragDropTypes::DRAG_MOVE; 383 } 384 385 void BrowserActionsContainer::OnDragExited() { 386 StopShowFolderDropMenuTimer(); 387 drop_indicator_position_ = -1; 388 SchedulePaint(); 389 } 390 391 int BrowserActionsContainer::OnPerformDrop( 392 const ui::DropTargetEvent& event) { 393 BrowserActionDragData data; 394 if (!data.Read(event.data())) 395 return ui::DragDropTypes::DRAG_NONE; 396 397 // Make sure we have the same view as we started with. 398 DCHECK_EQ(browser_action_views_[data.index()]->button()->extension()->id(), 399 data.id()); 400 DCHECK(model_); 401 402 size_t i = 0; 403 for (; i < browser_action_views_.size(); ++i) { 404 int view_x = browser_action_views_[i]->GetMirroredBounds().x(); 405 if (!browser_action_views_[i]->visible() || 406 (base::i18n::IsRTL() ? (view_x < drop_indicator_position_) : 407 (view_x >= drop_indicator_position_))) { 408 // We have reached the end of the visible icons or found one that has a 409 // higher x position than the drop point. 410 break; 411 } 412 } 413 414 // |i| now points to the item to the right of the drop indicator*, which is 415 // correct when dragging an icon to the left. When dragging to the right, 416 // however, we want the icon being dragged to get the index of the item to 417 // the left of the drop indicator, so we subtract one. 418 // * Well, it can also point to the end, but not when dragging to the left. :) 419 if (i > data.index()) 420 --i; 421 422 if (profile_->IsOffTheRecord()) 423 i = model_->IncognitoIndexToOriginal(i); 424 425 model_->MoveBrowserAction( 426 browser_action_views_[data.index()]->button()->extension(), i); 427 428 OnDragExited(); // Perform clean up after dragging. 429 return ui::DragDropTypes::DRAG_MOVE; 430 } 431 432 void BrowserActionsContainer::GetAccessibleState( 433 ui::AXViewState* state) { 434 state->role = ui::AX_ROLE_GROUP; 435 state->name = l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS); 436 } 437 438 void BrowserActionsContainer::OnMenuButtonClicked(views::View* source, 439 const gfx::Point& point) { 440 if (source == chevron_) { 441 overflow_menu_ = new BrowserActionOverflowMenuController( 442 this, browser_, chevron_, browser_action_views_, 443 VisibleBrowserActions()); 444 overflow_menu_->set_observer(this); 445 overflow_menu_->RunMenu(GetWidget(), false); 446 } 447 } 448 449 void BrowserActionsContainer::WriteDragDataForView(View* sender, 450 const gfx::Point& press_pt, 451 OSExchangeData* data) { 452 DCHECK(data); 453 454 for (size_t i = 0; i < browser_action_views_.size(); ++i) { 455 BrowserActionButton* button = browser_action_views_[i]->button(); 456 if (button == sender) { 457 // Set the dragging image for the icon. 458 gfx::ImageSkia badge(browser_action_views_[i]->GetIconWithBadge()); 459 drag_utils::SetDragImageOnDataObject(badge, 460 press_pt.OffsetFromOrigin(), 461 data); 462 463 // Fill in the remaining info. 464 BrowserActionDragData drag_data( 465 browser_action_views_[i]->button()->extension()->id(), i); 466 drag_data.Write(profile_, data); 467 break; 468 } 469 } 470 } 471 472 int BrowserActionsContainer::GetDragOperationsForView(View* sender, 473 const gfx::Point& p) { 474 return ui::DragDropTypes::DRAG_MOVE; 475 } 476 477 bool BrowserActionsContainer::CanStartDragForView(View* sender, 478 const gfx::Point& press_pt, 479 const gfx::Point& p) { 480 // We don't allow dragging while we're highlighting. 481 return !model_->is_highlighting(); 482 } 483 484 void BrowserActionsContainer::OnResize(int resize_amount, bool done_resizing) { 485 if (!done_resizing) { 486 resize_amount_ = resize_amount; 487 OnBrowserActionVisibilityChanged(); 488 return; 489 } 490 491 // Up until now we've only been modifying the resize_amount, but now it is 492 // time to set the container size to the size we have resized to, and then 493 // animate to the nearest icon count size if necessary (which may be 0). 494 int max_width = IconCountToWidth(-1, false); 495 container_width_ = 496 std::min(std::max(0, container_width_ - resize_amount), max_width); 497 SaveDesiredSizeAndAnimate(gfx::Tween::EASE_OUT, 498 WidthToIconCount(container_width_)); 499 } 500 501 void BrowserActionsContainer::AnimationProgressed( 502 const gfx::Animation* animation) { 503 DCHECK_EQ(resize_animation_.get(), animation); 504 resize_amount_ = static_cast<int>(resize_animation_->GetCurrentValue() * 505 (container_width_ - animation_target_size_)); 506 OnBrowserActionVisibilityChanged(); 507 } 508 509 void BrowserActionsContainer::AnimationEnded(const gfx::Animation* animation) { 510 container_width_ = animation_target_size_; 511 animation_target_size_ = 0; 512 resize_amount_ = 0; 513 suppress_chevron_ = false; 514 OnBrowserActionVisibilityChanged(); 515 516 FOR_EACH_OBSERVER(BrowserActionsContainerObserver, 517 observers_, 518 OnBrowserActionsContainerAnimationEnded()); 519 } 520 521 void BrowserActionsContainer::NotifyMenuDeleted( 522 BrowserActionOverflowMenuController* controller) { 523 DCHECK_EQ(overflow_menu_, controller); 524 overflow_menu_ = NULL; 525 } 526 527 void BrowserActionsContainer::OnWidgetDestroying(views::Widget* widget) { 528 DCHECK_EQ(popup_->GetWidget(), widget); 529 popup_->GetWidget()->RemoveObserver(this); 530 popup_ = NULL; 531 // |popup_button_| is NULL if the extension has been removed. 532 if (popup_button_) { 533 popup_button_->SetButtonNotPushed(); 534 popup_button_ = NULL; 535 } 536 } 537 538 void BrowserActionsContainer::InspectPopup(ExtensionAction* action) { 539 BrowserActionView* view = GetBrowserActionView(action); 540 ShowPopup(view->button(), ExtensionPopup::SHOW_AND_INSPECT, true); 541 } 542 543 int BrowserActionsContainer::GetCurrentTabId() const { 544 content::WebContents* active_tab = 545 browser_->tab_strip_model()->GetActiveWebContents(); 546 if (!active_tab) 547 return -1; 548 549 return SessionTabHelper::FromWebContents(active_tab)->session_id().id(); 550 } 551 552 void BrowserActionsContainer::OnBrowserActionExecuted( 553 BrowserActionButton* button) { 554 ShowPopup(button, ExtensionPopup::SHOW, true); 555 } 556 557 void BrowserActionsContainer::OnBrowserActionVisibilityChanged() { 558 SetVisible(!browser_action_views_.empty()); 559 owner_view_->Layout(); 560 owner_view_->SchedulePaint(); 561 } 562 563 extensions::ActiveTabPermissionGranter* 564 BrowserActionsContainer::GetActiveTabPermissionGranter() { 565 content::WebContents* web_contents = 566 browser_->tab_strip_model()->GetActiveWebContents(); 567 if (!web_contents) 568 return NULL; 569 return extensions::TabHelper::FromWebContents(web_contents)-> 570 active_tab_permission_granter(); 571 } 572 573 void BrowserActionsContainer::MoveBrowserAction(const std::string& extension_id, 574 size_t new_index) { 575 ExtensionService* service = 576 extensions::ExtensionSystem::Get(profile_)->extension_service(); 577 if (service) { 578 const Extension* extension = service->GetExtensionById(extension_id, false); 579 model_->MoveBrowserAction(extension, new_index); 580 SchedulePaint(); 581 } 582 } 583 584 bool BrowserActionsContainer::ShowPopup(const extensions::Extension* extension, 585 bool should_grant) { 586 // Do not override other popups and only show in active window. The window 587 // must also have a toolbar, otherwise it should not be showing popups. 588 // TODO(justinlin): Remove toolbar check when http://crbug.com/308645 is 589 // fixed. 590 if (popup_ || 591 !browser_->window()->IsActive() || 592 !browser_->window()->IsToolbarVisible()) { 593 return false; 594 } 595 596 for (BrowserActionViews::iterator it = browser_action_views_.begin(); 597 it != browser_action_views_.end(); ++it) { 598 BrowserActionButton* button = (*it)->button(); 599 if (button && button->extension() == extension) 600 return ShowPopup(button, ExtensionPopup::SHOW, should_grant); 601 } 602 return false; 603 } 604 605 void BrowserActionsContainer::HidePopup() { 606 // Remove this as an observer and clear |popup_| and |popup_button_| here, 607 // since we might change them before OnWidgetDestroying() gets called. 608 if (popup_) { 609 popup_->GetWidget()->RemoveObserver(this); 610 popup_->GetWidget()->Close(); 611 popup_ = NULL; 612 } 613 if (popup_button_) { 614 popup_button_->SetButtonNotPushed(); 615 popup_button_ = NULL; 616 } 617 } 618 619 void BrowserActionsContainer::TestExecuteBrowserAction(int index) { 620 BrowserActionButton* button = browser_action_views_[index]->button(); 621 OnBrowserActionExecuted(button); 622 } 623 624 void BrowserActionsContainer::TestSetIconVisibilityCount(size_t icons) { 625 model_->SetVisibleIconCount(icons); 626 chevron_->SetVisible(icons < browser_action_views_.size()); 627 container_width_ = IconCountToWidth(icons, chevron_->visible()); 628 Layout(); 629 SchedulePaint(); 630 } 631 632 void BrowserActionsContainer::OnPaint(gfx::Canvas* canvas) { 633 // If the views haven't been initialized yet, wait for the next call to 634 // paint (one will be triggered by entering highlight mode). 635 if (model_->is_highlighting() && !browser_action_views_.empty()) { 636 views::Painter::PaintPainterAt( 637 canvas, highlight_painter_.get(), GetLocalBounds()); 638 } 639 640 // TODO(sky/glen): Instead of using a drop indicator, animate the icons while 641 // dragging (like we do for tab dragging). 642 if (drop_indicator_position_ > -1) { 643 // The two-pixel width drop indicator. 644 static const int kDropIndicatorWidth = 2; 645 gfx::Rect indicator_bounds( 646 drop_indicator_position_ - (kDropIndicatorWidth / 2), 647 0, 648 kDropIndicatorWidth, 649 height()); 650 651 // Color of the drop indicator. 652 static const SkColor kDropIndicatorColor = SK_ColorBLACK; 653 canvas->FillRect(indicator_bounds, kDropIndicatorColor); 654 } 655 } 656 657 void BrowserActionsContainer::OnThemeChanged() { 658 LoadImages(); 659 } 660 661 void BrowserActionsContainer::ViewHierarchyChanged( 662 const ViewHierarchyChangedDetails& details) { 663 // No extensions (e.g., incognito). 664 if (!model_) 665 return; 666 667 if (details.is_add && details.child == this) { 668 // Initial toolbar button creation and placement in the widget hierarchy. 669 // We do this here instead of in the constructor because AddBrowserAction 670 // calls Layout on the Toolbar, which needs this object to be constructed 671 // before its Layout function is called. 672 CreateBrowserActionViews(); 673 } 674 } 675 676 // static 677 int BrowserActionsContainer::IconWidth(bool include_padding) { 678 static bool initialized = false; 679 static int icon_width = 0; 680 if (!initialized) { 681 initialized = true; 682 icon_width = ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 683 IDR_BROWSER_ACTION)->width(); 684 } 685 return icon_width + (include_padding ? kItemSpacing : 0); 686 } 687 688 // static 689 int BrowserActionsContainer::IconHeight() { 690 static bool initialized = false; 691 static int icon_height = 0; 692 if (!initialized) { 693 initialized = true; 694 icon_height = ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 695 IDR_BROWSER_ACTION)->height(); 696 } 697 return icon_height; 698 } 699 700 void BrowserActionsContainer::BrowserActionAdded(const Extension* extension, 701 int index) { 702 #if defined(DEBUG) 703 for (size_t i = 0; i < browser_action_views_.size(); ++i) { 704 DCHECK(browser_action_views_[i]->button()->extension() != extension) << 705 "Asked to add a browser action view for an extension that already " 706 "exists."; 707 } 708 #endif 709 CloseOverflowMenu(); 710 711 if (!ShouldDisplayBrowserAction(extension)) 712 return; 713 714 size_t visible_actions = VisibleBrowserActionsAfterAnimation(); 715 716 // Add the new browser action to the vector and the view hierarchy. 717 if (profile_->IsOffTheRecord()) 718 index = model_->OriginalIndexToIncognito(index); 719 BrowserActionView* view = new BrowserActionView(extension, browser_, this); 720 browser_action_views_.insert(browser_action_views_.begin() + index, view); 721 AddChildViewAt(view, index); 722 723 // If we are still initializing the container, don't bother animating. 724 if (!model_->extensions_initialized()) 725 return; 726 727 // Enlarge the container if it was already at maximum size and we're not in 728 // the middle of upgrading. 729 if ((model_->GetVisibleIconCount() < 0) && 730 !extensions::ExtensionSystem::Get(profile_)->runtime_data()-> 731 IsBeingUpgraded(extension)) { 732 suppress_chevron_ = true; 733 SaveDesiredSizeAndAnimate(gfx::Tween::LINEAR, visible_actions + 1); 734 } else { 735 // Just redraw the (possibly modified) visible icon set. 736 OnBrowserActionVisibilityChanged(); 737 } 738 } 739 740 void BrowserActionsContainer::BrowserActionRemoved(const Extension* extension) { 741 CloseOverflowMenu(); 742 743 if (popup_ && popup_->host()->extension() == extension) 744 HidePopup(); 745 746 size_t visible_actions = VisibleBrowserActionsAfterAnimation(); 747 for (BrowserActionViews::iterator i(browser_action_views_.begin()); 748 i != browser_action_views_.end(); ++i) { 749 if ((*i)->button()->extension() == extension) { 750 delete *i; 751 browser_action_views_.erase(i); 752 753 // If the extension is being upgraded we don't want the bar to shrink 754 // because the icon is just going to get re-added to the same location. 755 if (extensions::ExtensionSystem::Get(profile_)->runtime_data()-> 756 IsBeingUpgraded(extension)) 757 return; 758 759 if (browser_action_views_.size() > visible_actions) { 760 // If we have more icons than we can show, then we must not be changing 761 // the container size (since we either removed an icon from the main 762 // area and one from the overflow list will have shifted in, or we 763 // removed an entry directly from the overflow list). 764 OnBrowserActionVisibilityChanged(); 765 } else { 766 // Either we went from overflow to no-overflow, or we shrunk the no- 767 // overflow container by 1. Either way the size changed, so animate. 768 chevron_->SetVisible(false); 769 SaveDesiredSizeAndAnimate(gfx::Tween::EASE_OUT, 770 browser_action_views_.size()); 771 } 772 return; 773 } 774 } 775 } 776 777 void BrowserActionsContainer::BrowserActionMoved(const Extension* extension, 778 int index) { 779 if (!ShouldDisplayBrowserAction(extension)) 780 return; 781 782 if (profile_->IsOffTheRecord()) 783 index = model_->OriginalIndexToIncognito(index); 784 785 DCHECK(index >= 0 && index < static_cast<int>(browser_action_views_.size())); 786 787 DeleteBrowserActionViews(); 788 CreateBrowserActionViews(); 789 Layout(); 790 SchedulePaint(); 791 } 792 793 bool BrowserActionsContainer::BrowserActionShowPopup( 794 const extensions::Extension* extension) { 795 return ShowPopup(extension, false); 796 } 797 798 void BrowserActionsContainer::VisibleCountChanged() { 799 SetContainerWidth(); 800 } 801 802 void BrowserActionsContainer::HighlightModeChanged(bool is_highlighting) { 803 // The visual highlighting is done in OnPaint(). It's a bit of a pain that 804 // we delete and recreate everything here, but that's how it's done in 805 // BrowserActionMoved(), too. If we want to optimize it, we could move the 806 // existing icons, instead of deleting it all. 807 DeleteBrowserActionViews(); 808 CreateBrowserActionViews(); 809 SaveDesiredSizeAndAnimate(gfx::Tween::LINEAR, browser_action_views_.size()); 810 } 811 812 void BrowserActionsContainer::LoadImages() { 813 ui::ThemeProvider* tp = GetThemeProvider(); 814 chevron_->SetImage(views::Button::STATE_NORMAL, 815 *tp->GetImageSkiaNamed(IDR_BROWSER_ACTIONS_OVERFLOW)); 816 817 const int kImages[] = IMAGE_GRID(IDR_DEVELOPER_MODE_HIGHLIGHT); 818 highlight_painter_.reset(views::Painter::CreateImageGridPainter(kImages)); 819 } 820 821 void BrowserActionsContainer::SetContainerWidth() { 822 int visible_actions = model_->GetVisibleIconCount(); 823 if (visible_actions < 0) // All icons should be visible. 824 visible_actions = model_->toolbar_items().size(); 825 chevron_->SetVisible( 826 static_cast<size_t>(visible_actions) < model_->toolbar_items().size()); 827 container_width_ = IconCountToWidth(visible_actions, chevron_->visible()); 828 } 829 830 void BrowserActionsContainer::CloseOverflowMenu() { 831 if (overflow_menu_) 832 overflow_menu_->CancelMenu(); 833 } 834 835 void BrowserActionsContainer::StopShowFolderDropMenuTimer() { 836 show_menu_task_factory_.InvalidateWeakPtrs(); 837 } 838 839 void BrowserActionsContainer::StartShowFolderDropMenuTimer() { 840 base::MessageLoop::current()->PostDelayedTask( 841 FROM_HERE, 842 base::Bind(&BrowserActionsContainer::ShowDropFolder, 843 show_menu_task_factory_.GetWeakPtr()), 844 base::TimeDelta::FromMilliseconds(views::GetMenuShowDelay())); 845 } 846 847 void BrowserActionsContainer::ShowDropFolder() { 848 DCHECK(!overflow_menu_); 849 SetDropIndicator(-1); 850 overflow_menu_ = new BrowserActionOverflowMenuController( 851 this, browser_, chevron_, browser_action_views_, VisibleBrowserActions()); 852 overflow_menu_->set_observer(this); 853 overflow_menu_->RunMenu(GetWidget(), true); 854 } 855 856 void BrowserActionsContainer::SetDropIndicator(int x_pos) { 857 if (drop_indicator_position_ != x_pos) { 858 drop_indicator_position_ = x_pos; 859 SchedulePaint(); 860 } 861 } 862 863 int BrowserActionsContainer::IconCountToWidth(int icons, 864 bool display_chevron) const { 865 if (icons < 0) 866 icons = browser_action_views_.size(); 867 if ((icons == 0) && !display_chevron) 868 return ToolbarView::kStandardSpacing; 869 int icons_size = 870 (icons == 0) ? 0 : ((icons * IconWidth(true)) - kItemSpacing); 871 int chevron_size = display_chevron ? 872 (kChevronSpacing + chevron_->GetPreferredSize().width()) : 0; 873 return ToolbarView::kStandardSpacing + icons_size + chevron_size + 874 ToolbarView::kStandardSpacing; 875 } 876 877 size_t BrowserActionsContainer::WidthToIconCount(int pixels) const { 878 // Check for widths large enough to show the entire icon set. 879 if (pixels >= IconCountToWidth(-1, false)) 880 return browser_action_views_.size(); 881 882 // We need to reserve space for the resize area, chevron, and the spacing on 883 // either side of the chevron. 884 int available_space = pixels - ToolbarView::kStandardSpacing - 885 chevron_->GetPreferredSize().width() - kChevronSpacing - 886 ToolbarView::kStandardSpacing; 887 // Now we add an extra between-item padding value so the space can be divided 888 // evenly by (size of icon with padding). 889 return static_cast<size_t>( 890 std::max(0, available_space + kItemSpacing) / IconWidth(true)); 891 } 892 893 int BrowserActionsContainer::MinimumNonemptyWidth() const { 894 return ToolbarView::kStandardSpacing + kChevronSpacing + 895 chevron_->GetPreferredSize().width() + ToolbarView::kStandardSpacing; 896 } 897 898 void BrowserActionsContainer::SaveDesiredSizeAndAnimate( 899 gfx::Tween::Type tween_type, 900 size_t num_visible_icons) { 901 // Save off the desired number of visible icons. We do this now instead of at 902 // the end of the animation so that even if the browser is shut down while 903 // animating, the right value will be restored on next run. 904 // NOTE: Don't save the icon count in incognito because there may be fewer 905 // icons in that mode. The result is that the container in a normal window is 906 // always at least as wide as in an incognito window. 907 if (!profile_->IsOffTheRecord()) 908 model_->SetVisibleIconCount(num_visible_icons); 909 int target_size = IconCountToWidth(num_visible_icons, 910 num_visible_icons < browser_action_views_.size()); 911 if (!disable_animations_during_testing_) { 912 // Animate! We have to set the animation_target_size_ after calling Reset(), 913 // because that could end up calling AnimationEnded which clears the value. 914 resize_animation_->Reset(); 915 resize_animation_->SetTweenType(tween_type); 916 animation_target_size_ = target_size; 917 resize_animation_->Show(); 918 } else { 919 animation_target_size_ = target_size; 920 AnimationEnded(resize_animation_.get()); 921 } 922 } 923 924 bool BrowserActionsContainer::ShouldDisplayBrowserAction( 925 const Extension* extension) { 926 // Only display incognito-enabled extensions while in incognito mode. 927 return !profile_->IsOffTheRecord() || 928 extensions::util::IsIncognitoEnabled(extension->id(), profile_); 929 } 930 931 bool BrowserActionsContainer::ShowPopup( 932 BrowserActionButton* button, 933 ExtensionPopup::ShowAction show_action, 934 bool should_grant) { 935 const Extension* extension = button->extension(); 936 GURL popup_url; 937 if (model_->ExecuteBrowserAction( 938 extension, browser_, &popup_url, should_grant) != 939 extensions::ExtensionToolbarModel::ACTION_SHOW_POPUP) { 940 return false; 941 } 942 943 // If we're showing the same popup, just hide it and return. 944 bool same_showing = popup_ && button == popup_button_; 945 946 // Always hide the current popup, even if it's not the same. 947 // Only one popup should be visible at a time. 948 HidePopup(); 949 950 if (same_showing) 951 return false; 952 953 // We can get the execute event for browser actions that are not visible, 954 // since buttons can be activated from the overflow menu (chevron). In that 955 // case we show the popup as originating from the chevron. 956 View* reference_view = button->parent()->visible() ? button : chevron_; 957 popup_ = ExtensionPopup::ShowPopup(popup_url, browser_, reference_view, 958 views::BubbleBorder::TOP_RIGHT, 959 show_action); 960 popup_->GetWidget()->AddObserver(this); 961 popup_button_ = button; 962 963 // Only set button as pushed if it was triggered by a user click. 964 if (should_grant) 965 popup_button_->SetButtonPushed(); 966 return true; 967 } 968