// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/views/browser_actions_container.h" #include "base/stl_util-inl.h" #include "base/string_util.h" #include "base/utf_string_conversions.h" #include "chrome/browser/extensions/extension_browser_event_router.h" #include "chrome/browser/extensions/extension_host.h" #include "chrome/browser/extensions/extension_service.h" #include "chrome/browser/extensions/extension_tabs_module.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/view_ids.h" #include "chrome/browser/ui/views/detachable_toolbar_view.h" #include "chrome/browser/ui/views/extensions/browser_action_drag_data.h" #include "chrome/browser/ui/views/extensions/extension_popup.h" #include "chrome/browser/ui/views/toolbar_view.h" #include "chrome/common/extensions/extension_action.h" #include "chrome/common/extensions/extension_resource.h" #include "chrome/common/pref_names.h" #include "content/browser/renderer_host/render_view_host.h" #include "content/browser/renderer_host/render_widget_host_view.h" #include "content/browser/tab_contents/tab_contents.h" #include "content/common/notification_source.h" #include "content/common/notification_type.h" #include "grit/app_resources.h" #include "grit/generated_resources.h" #include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/effects/SkGradientShader.h" #include "ui/base/accessibility/accessible_view_state.h" #include "ui/base/animation/slide_animation.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" #include "ui/base/theme_provider.h" #include "ui/gfx/canvas.h" #include "ui/gfx/canvas_skia.h" #include "views/controls/button/menu_button.h" #include "views/controls/button/text_button.h" #include "views/controls/menu/menu_2.h" #include "views/drag_utils.h" #include "views/metrics.h" #include "views/window/window.h" #include "grit/theme_resources.h" // Horizontal spacing between most items in the container, as well as after the // last item or chevron (if visible). static const int kItemSpacing = ToolbarView::kStandardSpacing; // Horizontal spacing before the chevron (if visible). static const int kChevronSpacing = kItemSpacing - 2; // static bool BrowserActionsContainer::disable_animations_during_testing_ = false; //////////////////////////////////////////////////////////////////////////////// // BrowserActionButton BrowserActionButton::BrowserActionButton(const Extension* extension, BrowserActionsContainer* panel) : ALLOW_THIS_IN_INITIALIZER_LIST( MenuButton(this, std::wstring(), NULL, false)), browser_action_(extension->browser_action()), extension_(extension), ALLOW_THIS_IN_INITIALIZER_LIST(tracker_(this)), showing_context_menu_(false), panel_(panel) { set_border(NULL); set_alignment(TextButton::ALIGN_CENTER); // No UpdateState() here because View hierarchy not setup yet. Our parent // should call UpdateState() after creation. registrar_.Add(this, NotificationType::EXTENSION_BROWSER_ACTION_UPDATED, Source<ExtensionAction>(browser_action_)); } void BrowserActionButton::Destroy() { if (showing_context_menu_) { context_menu_menu_->CancelMenu(); MessageLoop::current()->DeleteSoon(FROM_HERE, this); } else { delete this; } } void BrowserActionButton::ViewHierarchyChanged( bool is_add, View* parent, View* child) { if (is_add && child == this) { // The Browser Action API does not allow the default icon path to be // changed at runtime, so we can load this now and cache it. std::string relative_path = browser_action_->default_icon_path(); if (relative_path.empty()) return; // LoadImage is not guaranteed to be synchronous, so we might see the // callback OnImageLoaded execute immediately. It (through UpdateState) // expects parent() to return the owner for this button, so this // function is as early as we can start this request. tracker_.LoadImage(extension_, extension_->GetResource(relative_path), gfx::Size(Extension::kBrowserActionIconMaxSize, Extension::kBrowserActionIconMaxSize), ImageLoadingTracker::DONT_CACHE); } MenuButton::ViewHierarchyChanged(is_add, parent, child); } void BrowserActionButton::ButtonPressed(views::Button* sender, const views::Event& event) { panel_->OnBrowserActionExecuted(this, false); } void BrowserActionButton::OnImageLoaded(SkBitmap* image, const ExtensionResource& resource, int index) { if (image) default_icon_ = *image; // Call back to UpdateState() because a more specific icon might have been set // while the load was outstanding. UpdateState(); } void BrowserActionButton::UpdateState() { int tab_id = panel_->GetCurrentTabId(); if (tab_id < 0) return; SkBitmap icon(browser_action()->GetIcon(tab_id)); if (icon.isNull()) icon = default_icon_; if (!icon.isNull()) { SkPaint paint; paint.setXfermode(SkXfermode::Create(SkXfermode::kSrcOver_Mode)); ResourceBundle& rb = ResourceBundle::GetSharedInstance(); SkBitmap bg; rb.GetBitmapNamed(IDR_BROWSER_ACTION)->copyTo(&bg, SkBitmap::kARGB_8888_Config); SkCanvas bg_canvas(bg); bg_canvas.drawBitmap(icon, SkIntToScalar((bg.width() - icon.width()) / 2), SkIntToScalar((bg.height() - icon.height()) / 2), &paint); SetIcon(bg); SkBitmap bg_h; rb.GetBitmapNamed(IDR_BROWSER_ACTION_H)->copyTo(&bg_h, SkBitmap::kARGB_8888_Config); SkCanvas bg_h_canvas(bg_h); bg_h_canvas.drawBitmap(icon, SkIntToScalar((bg_h.width() - icon.width()) / 2), SkIntToScalar((bg_h.height() - icon.height()) / 2), &paint); SetHoverIcon(bg_h); SkBitmap bg_p; rb.GetBitmapNamed(IDR_BROWSER_ACTION_P)->copyTo(&bg_p, SkBitmap::kARGB_8888_Config); SkCanvas bg_p_canvas(bg_p); bg_p_canvas.drawBitmap(icon, SkIntToScalar((bg_p.width() - icon.width()) / 2), SkIntToScalar((bg_p.height() - icon.height()) / 2), &paint); SetPushedIcon(bg_p); } // If the browser action name is empty, show the extension name instead. string16 name = UTF8ToUTF16(browser_action()->GetTitle(tab_id)); if (name.empty()) name = UTF8ToUTF16(extension()->name()); SetTooltipText(UTF16ToWideHack(name)); parent()->SchedulePaint(); } bool BrowserActionButton::IsPopup() { int tab_id = panel_->GetCurrentTabId(); return (tab_id < 0) ? false : browser_action_->HasPopup(tab_id); } GURL BrowserActionButton::GetPopupUrl() { int tab_id = panel_->GetCurrentTabId(); return (tab_id < 0) ? GURL() : browser_action_->GetPopupUrl(tab_id); } void BrowserActionButton::Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details) { DCHECK(type == NotificationType::EXTENSION_BROWSER_ACTION_UPDATED); UpdateState(); // The browser action may have become visible/hidden so we need to make // sure the state gets updated. panel_->OnBrowserActionVisibilityChanged(); } bool BrowserActionButton::Activate() { if (!IsPopup()) return true; panel_->OnBrowserActionExecuted(this, false); // TODO(erikkay): Run a nested modal loop while the mouse is down to // enable menu-like drag-select behavior. // The return value of this method is returned via OnMousePressed. // We need to return false here since we're handing off focus to another // widget/view, and true will grab it right back and try to send events // to us. return false; } bool BrowserActionButton::OnMousePressed(const views::MouseEvent& event) { if (!event.IsRightMouseButton()) { return IsPopup() ? MenuButton::OnMousePressed(event) : TextButton::OnMousePressed(event); } // Get the top left point of this button in screen coordinates. gfx::Point point = gfx::Point(0, 0); ConvertPointToScreen(this, &point); // Make the menu appear below the button. point.Offset(0, height()); ShowContextMenu(point, true); return false; } void BrowserActionButton::OnMouseReleased(const views::MouseEvent& event) { if (IsPopup() || showing_context_menu_) { // TODO(erikkay) this never actually gets called (probably because of the // loss of focus). MenuButton::OnMouseReleased(event); } else { TextButton::OnMouseReleased(event); } } void BrowserActionButton::OnMouseExited(const views::MouseEvent& event) { if (IsPopup() || showing_context_menu_) MenuButton::OnMouseExited(event); else TextButton::OnMouseExited(event); } bool BrowserActionButton::OnKeyReleased(const views::KeyEvent& event) { return IsPopup() ? MenuButton::OnKeyReleased(event) : TextButton::OnKeyReleased(event); } void BrowserActionButton::ShowContextMenu(const gfx::Point& p, bool is_mouse_gesture) { if (!extension()->ShowConfigureContextMenus()) return; showing_context_menu_ = true; SetButtonPushed(); // Reconstructs the menu every time because the menu's contents are dynamic. context_menu_contents_ = new ExtensionContextMenuModel(extension(), panel_->browser(), panel_); context_menu_menu_.reset(new views::Menu2(context_menu_contents_.get())); context_menu_menu_->RunContextMenuAt(p); SetButtonNotPushed(); showing_context_menu_ = false; } void BrowserActionButton::SetButtonPushed() { SetState(views::CustomButton::BS_PUSHED); menu_visible_ = true; } void BrowserActionButton::SetButtonNotPushed() { SetState(views::CustomButton::BS_NORMAL); menu_visible_ = false; } BrowserActionButton::~BrowserActionButton() { } //////////////////////////////////////////////////////////////////////////////// // BrowserActionView BrowserActionView::BrowserActionView(const Extension* extension, BrowserActionsContainer* panel) : panel_(panel) { button_ = new BrowserActionButton(extension, panel); button_->SetDragController(panel_); AddChildView(button_); button_->UpdateState(); } BrowserActionView::~BrowserActionView() { RemoveChildView(button_); button_->Destroy(); } gfx::Canvas* BrowserActionView::GetIconWithBadge() { int tab_id = panel_->GetCurrentTabId(); SkBitmap icon = button_->extension()->browser_action()->GetIcon(tab_id); if (icon.isNull()) icon = button_->default_icon(); gfx::Canvas* canvas = new gfx::CanvasSkia(icon.width(), icon.height(), false); canvas->DrawBitmapInt(icon, 0, 0); if (tab_id >= 0) { gfx::Rect bounds(icon.width(), icon.height() + ToolbarView::kVertSpacing); button_->extension()->browser_action()->PaintBadge(canvas, bounds, tab_id); } return canvas; } void BrowserActionView::Layout() { // We can't rely on button_->GetPreferredSize() here because that's not set // correctly until the first call to // BrowserActionsContainer::RefreshBrowserActionViews(), whereas this can be // called before that when the initial bounds are set (and then not after, // since the bounds don't change). So instead of setting the height from the // button's preferred size, we use IconHeight(), since that's how big the // button should be regardless of what it's displaying. button_->SetBounds(0, ToolbarView::kVertSpacing, width(), BrowserActionsContainer::IconHeight()); } void BrowserActionView::GetAccessibleState(ui::AccessibleViewState* state) { state->name = l10n_util::GetStringUTF16( IDS_ACCNAME_EXTENSIONS_BROWSER_ACTION); state->role = ui::AccessibilityTypes::ROLE_GROUPING; } void BrowserActionView::PaintChildren(gfx::Canvas* canvas) { View::PaintChildren(canvas); ExtensionAction* action = button()->browser_action(); int tab_id = panel_->GetCurrentTabId(); if (tab_id >= 0) action->PaintBadge(canvas, gfx::Rect(width(), height()), tab_id); } //////////////////////////////////////////////////////////////////////////////// // BrowserActionsContainer BrowserActionsContainer::BrowserActionsContainer(Browser* browser, View* owner_view) : profile_(browser->profile()), browser_(browser), owner_view_(owner_view), popup_(NULL), popup_button_(NULL), model_(NULL), container_width_(0), chevron_(NULL), overflow_menu_(NULL), suppress_chevron_(false), resize_amount_(0), animation_target_size_(0), drop_indicator_position_(-1), ALLOW_THIS_IN_INITIALIZER_LIST(task_factory_(this)), ALLOW_THIS_IN_INITIALIZER_LIST(show_menu_task_factory_(this)) { SetID(VIEW_ID_BROWSER_ACTION_TOOLBAR); if (profile_->GetExtensionService()) { model_ = profile_->GetExtensionService()->toolbar_model(); model_->AddObserver(this); } resize_animation_.reset(new ui::SlideAnimation(this)); resize_area_ = new views::ResizeArea(this); AddChildView(resize_area_); chevron_ = new views::MenuButton(NULL, std::wstring(), this, false); chevron_->set_border(NULL); chevron_->EnableCanvasFlippingForRTLUI(true); chevron_->SetAccessibleName( l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS_CHEVRON)); chevron_->SetVisible(false); AddChildView(chevron_); } BrowserActionsContainer::~BrowserActionsContainer() { if (model_) model_->RemoveObserver(this); StopShowFolderDropMenuTimer(); HidePopup(); DeleteBrowserActionViews(); } // Static. void BrowserActionsContainer::RegisterUserPrefs(PrefService* prefs) { prefs->RegisterIntegerPref(prefs::kBrowserActionContainerWidth, 0); } void BrowserActionsContainer::Init() { LoadImages(); // We wait to set the container width until now so that the chevron images // will be loaded. The width calculation needs to know the chevron size. if (model_ && !profile_->GetPrefs()->HasPrefPath(prefs::kExtensionToolbarSize)) { // Migration code to the new VisibleIconCount pref. // TODO(mpcomplete): remove this after users are upgraded to 5.0. int predefined_width = profile_->GetPrefs()->GetInteger(prefs::kBrowserActionContainerWidth); if (predefined_width != 0) model_->SetVisibleIconCount(WidthToIconCount(predefined_width)); } if (model_ && model_->extensions_initialized()) SetContainerWidth(); } int BrowserActionsContainer::GetCurrentTabId() const { TabContents* tab_contents = browser_->GetSelectedTabContents(); return tab_contents ? tab_contents->controller().session_id().id() : -1; } BrowserActionView* BrowserActionsContainer::GetBrowserActionView( ExtensionAction* action) { for (BrowserActionViews::iterator iter = browser_action_views_.begin(); iter != browser_action_views_.end(); ++iter) { if ((*iter)->button()->browser_action() == action) return *iter; } return NULL; } void BrowserActionsContainer::RefreshBrowserActionViews() { for (size_t i = 0; i < browser_action_views_.size(); ++i) browser_action_views_[i]->button()->UpdateState(); } void BrowserActionsContainer::CreateBrowserActionViews() { DCHECK(browser_action_views_.empty()); if (!model_) return; for (ExtensionList::iterator iter = model_->begin(); iter != model_->end(); ++iter) { if (!ShouldDisplayBrowserAction(*iter)) continue; BrowserActionView* view = new BrowserActionView(*iter, this); browser_action_views_.push_back(view); AddChildView(view); } } void BrowserActionsContainer::DeleteBrowserActionViews() { if (!browser_action_views_.empty()) { for (size_t i = 0; i < browser_action_views_.size(); ++i) RemoveChildView(browser_action_views_[i]); STLDeleteContainerPointers(browser_action_views_.begin(), browser_action_views_.end()); browser_action_views_.clear(); } } void BrowserActionsContainer::OnBrowserActionVisibilityChanged() { SetVisible(!browser_action_views_.empty()); owner_view_->Layout(); owner_view_->SchedulePaint(); } size_t BrowserActionsContainer::VisibleBrowserActions() const { size_t visible_actions = 0; for (size_t i = 0; i < browser_action_views_.size(); ++i) { if (browser_action_views_[i]->IsVisible()) ++visible_actions; } return visible_actions; } void BrowserActionsContainer::OnBrowserActionExecuted( BrowserActionButton* button, bool inspect_with_devtools) { ExtensionAction* browser_action = button->browser_action(); // Popups just display. No notification to the extension. // TODO(erikkay): should there be? if (!button->IsPopup()) { ExtensionService* service = profile_->GetExtensionService(); service->browser_event_router()->BrowserActionExecuted( profile_, browser_action->extension_id(), browser_); return; } // If we're showing the same popup, just hide it and return. bool same_showing = popup_ && button == popup_button_; // Always hide the current popup, even if it's not the same. // Only one popup should be visible at a time. HidePopup(); if (same_showing) return; // We can get the execute event for browser actions that are not visible, // since buttons can be activated from the overflow menu (chevron). In that // case we show the popup as originating from the chevron. View* reference_view = button->parent()->IsVisible() ? button : chevron_; gfx::Point origin; View::ConvertPointToScreen(reference_view, &origin); gfx::Rect rect = reference_view->bounds(); rect.set_origin(origin); BubbleBorder::ArrowLocation arrow_location = base::i18n::IsRTL() ? BubbleBorder::TOP_LEFT : BubbleBorder::TOP_RIGHT; popup_ = ExtensionPopup::Show(button->GetPopupUrl(), browser_, rect, arrow_location, inspect_with_devtools, this); popup_button_ = button; popup_button_->SetButtonPushed(); } gfx::Size BrowserActionsContainer::GetPreferredSize() { if (browser_action_views_.empty()) return gfx::Size(ToolbarView::kStandardSpacing, 0); // We calculate the size of the view by taking the current width and // subtracting resize_amount_ (the latter represents how far the user is // resizing the view or, if animating the snapping, how far to animate it). // But we also clamp it to a minimum size and the maximum size, so that the // container can never shrink too far or take up more space than it needs. In // other words: ContainerMinSize() < width() - resize < ClampTo(MAX). int clamped_width = std::min( std::max(ContainerMinSize(), container_width_ - resize_amount_), IconCountToWidth(-1, false)); return gfx::Size(clamped_width, 0); } void BrowserActionsContainer::Layout() { if (browser_action_views_.empty()) { SetVisible(false); return; } SetVisible(true); resize_area_->SetBounds(0, ToolbarView::kVertSpacing, kItemSpacing, IconHeight()); // If the icons don't all fit, show the chevron (unless suppressed). int max_x = GetPreferredSize().width(); if ((IconCountToWidth(-1, false) > max_x) && !suppress_chevron_) { chevron_->SetVisible(true); gfx::Size chevron_size(chevron_->GetPreferredSize()); max_x -= ToolbarView::kStandardSpacing + chevron_size.width() + kChevronSpacing; chevron_->SetBounds( width() - ToolbarView::kStandardSpacing - chevron_size.width(), ToolbarView::kVertSpacing, chevron_size.width(), chevron_size.height()); } else { chevron_->SetVisible(false); } // Now draw the icons for the browser actions in the available space. int icon_width = IconWidth(false); for (size_t i = 0; i < browser_action_views_.size(); ++i) { BrowserActionView* view = browser_action_views_[i]; int x = ToolbarView::kStandardSpacing + (i * IconWidth(true)); if (x + icon_width <= max_x) { view->SetBounds(x, 0, icon_width, height()); view->SetVisible(true); } else { view->SetVisible(false); } } } bool BrowserActionsContainer::GetDropFormats( int* formats, std::set<OSExchangeData::CustomFormat>* custom_formats) { custom_formats->insert(BrowserActionDragData::GetBrowserActionCustomFormat()); return true; } bool BrowserActionsContainer::AreDropTypesRequired() { return true; } bool BrowserActionsContainer::CanDrop(const OSExchangeData& data) { BrowserActionDragData drop_data; return drop_data.Read(data) ? drop_data.IsFromProfile(profile_) : false; } void BrowserActionsContainer::OnDragEntered( const views::DropTargetEvent& event) { } int BrowserActionsContainer::OnDragUpdated( const views::DropTargetEvent& event) { // First check if we are above the chevron (overflow) menu. if (GetEventHandlerForPoint(event.location()) == chevron_) { if (show_menu_task_factory_.empty() && !overflow_menu_) StartShowFolderDropMenuTimer(); return ui::DragDropTypes::DRAG_MOVE; } StopShowFolderDropMenuTimer(); // Figure out where to display the indicator. This is a complex calculation: // First, we figure out how much space is to the left of the icon area, so we // can calculate the true offset into the icon area. int width_before_icons = ToolbarView::kStandardSpacing + (base::i18n::IsRTL() ? (chevron_->GetPreferredSize().width() + kChevronSpacing) : 0); int offset_into_icon_area = event.x() - width_before_icons; // Next, we determine which icon to place the indicator in front of. We want // to place the indicator in front of icon n when the cursor is between the // midpoints of icons (n - 1) and n. To do this we take the offset into the // icon area and transform it as follows: // // Real icon area: // 0 a * b c // | | | | // |[IC|ON] [IC|ON] [IC|ON] // We want to be before icon 0 for 0 < x <= a, icon 1 for a < x <= b, etc. // Here the "*" represents the offset into the icon area, and since it's // between a and b, we want to return "1". // // Transformed "icon area": // 0 a * b c // | | | | // |[ICON] |[ICON] |[ICON] | // If we shift both our offset and our divider points later by half an icon // plus one spacing unit, then it becomes very easy to calculate how many // divider points we've passed, because they're the multiples of "one icon // plus padding". int before_icon_unclamped = (offset_into_icon_area + (IconWidth(false) / 2) + kItemSpacing) / IconWidth(true); // Because the user can drag outside the container bounds, we need to clamp to // the valid range. Note that the maximum allowable value is (num icons), not // (num icons - 1), because we represent the indicator being past the last // icon as being "before the (last + 1) icon". int before_icon = std::min(std::max(before_icon_unclamped, 0), static_cast<int>(VisibleBrowserActions())); // Now we convert back to a pixel offset into the container. We want to place // the center of the drop indicator at the midpoint of the space before our // chosen icon. SetDropIndicator(width_before_icons + (before_icon * IconWidth(true)) - (kItemSpacing / 2)); return ui::DragDropTypes::DRAG_MOVE; } void BrowserActionsContainer::OnDragExited() { StopShowFolderDropMenuTimer(); drop_indicator_position_ = -1; SchedulePaint(); } int BrowserActionsContainer::OnPerformDrop( const views::DropTargetEvent& event) { BrowserActionDragData data; if (!data.Read(event.data())) return ui::DragDropTypes::DRAG_NONE; // Make sure we have the same view as we started with. DCHECK_EQ(browser_action_views_[data.index()]->button()->extension()->id(), data.id()); DCHECK(model_); size_t i = 0; for (; i < browser_action_views_.size(); ++i) { int view_x = browser_action_views_[i]->GetMirroredBounds().x(); if (!browser_action_views_[i]->IsVisible() || (base::i18n::IsRTL() ? (view_x < drop_indicator_position_) : (view_x >= drop_indicator_position_))) { // We have reached the end of the visible icons or found one that has a // higher x position than the drop point. break; } } // |i| now points to the item to the right of the drop indicator*, which is // correct when dragging an icon to the left. When dragging to the right, // however, we want the icon being dragged to get the index of the item to // the left of the drop indicator, so we subtract one. // * Well, it can also point to the end, but not when dragging to the left. :) if (i > data.index()) --i; if (profile_->IsOffTheRecord()) i = model_->IncognitoIndexToOriginal(i); model_->MoveBrowserAction( browser_action_views_[data.index()]->button()->extension(), i); OnDragExited(); // Perform clean up after dragging. return ui::DragDropTypes::DRAG_MOVE; } void BrowserActionsContainer::GetAccessibleState( ui::AccessibleViewState* state) { state->role = ui::AccessibilityTypes::ROLE_GROUPING; state->name = l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS); } void BrowserActionsContainer::RunMenu(View* source, const gfx::Point& pt) { if (source == chevron_) { overflow_menu_ = new BrowserActionOverflowMenuController( this, chevron_, browser_action_views_, VisibleBrowserActions()); overflow_menu_->set_observer(this); overflow_menu_->RunMenu(GetWindow()->GetNativeWindow(), false); } } void BrowserActionsContainer::WriteDragDataForView(View* sender, const gfx::Point& press_pt, OSExchangeData* data) { DCHECK(data); for (size_t i = 0; i < browser_action_views_.size(); ++i) { BrowserActionButton* button = browser_action_views_[i]->button(); if (button == sender) { // Set the dragging image for the icon. scoped_ptr<gfx::Canvas> canvas( browser_action_views_[i]->GetIconWithBadge()); drag_utils::SetDragImageOnDataObject(*canvas, button->size(), press_pt, data); // Fill in the remaining info. BrowserActionDragData drag_data( browser_action_views_[i]->button()->extension()->id(), i); drag_data.Write(profile_, data); break; } } } int BrowserActionsContainer::GetDragOperationsForView(View* sender, const gfx::Point& p) { return ui::DragDropTypes::DRAG_MOVE; } bool BrowserActionsContainer::CanStartDragForView(View* sender, const gfx::Point& press_pt, const gfx::Point& p) { return true; } void BrowserActionsContainer::OnResize(int resize_amount, bool done_resizing) { if (!done_resizing) { resize_amount_ = resize_amount; OnBrowserActionVisibilityChanged(); return; } // Up until now we've only been modifying the resize_amount, but now it is // time to set the container size to the size we have resized to, and then // animate to the nearest icon count size if necessary (which may be 0). int max_width = IconCountToWidth(-1, false); container_width_ = std::min(std::max(0, container_width_ - resize_amount), max_width); SaveDesiredSizeAndAnimate(ui::Tween::EASE_OUT, WidthToIconCount(container_width_)); } void BrowserActionsContainer::AnimationProgressed( const ui::Animation* animation) { DCHECK_EQ(resize_animation_.get(), animation); resize_amount_ = static_cast<int>(resize_animation_->GetCurrentValue() * (container_width_ - animation_target_size_)); OnBrowserActionVisibilityChanged(); } void BrowserActionsContainer::AnimationEnded(const ui::Animation* animation) { container_width_ = animation_target_size_; animation_target_size_ = 0; resize_amount_ = 0; OnBrowserActionVisibilityChanged(); suppress_chevron_ = false; } void BrowserActionsContainer::NotifyMenuDeleted( BrowserActionOverflowMenuController* controller) { DCHECK(controller == overflow_menu_); overflow_menu_ = NULL; } void BrowserActionsContainer::InspectPopup(ExtensionAction* action) { OnBrowserActionExecuted(GetBrowserActionView(action)->button(), true); } void BrowserActionsContainer::ExtensionPopupIsClosing(ExtensionPopup* popup) { // ExtensionPopup is ref-counted, so we don't need to delete it. DCHECK_EQ(popup_, popup); popup_ = NULL; popup_button_->SetButtonNotPushed(); popup_button_ = NULL; } void BrowserActionsContainer::MoveBrowserAction(const std::string& extension_id, size_t new_index) { ExtensionService* service = profile_->GetExtensionService(); if (service) { const Extension* extension = service->GetExtensionById(extension_id, false); model_->MoveBrowserAction(extension, new_index); SchedulePaint(); } } void BrowserActionsContainer::HidePopup() { if (popup_) popup_->Close(); } void BrowserActionsContainer::TestExecuteBrowserAction(int index) { BrowserActionButton* button = browser_action_views_[index]->button(); OnBrowserActionExecuted(button, false); } void BrowserActionsContainer::TestSetIconVisibilityCount(size_t icons) { model_->SetVisibleIconCount(icons); chevron_->SetVisible(icons < browser_action_views_.size()); container_width_ = IconCountToWidth(icons, chevron_->IsVisible()); Layout(); SchedulePaint(); } void BrowserActionsContainer::OnPaint(gfx::Canvas* canvas) { // TODO(sky/glen): Instead of using a drop indicator, animate the icons while // dragging (like we do for tab dragging). if (drop_indicator_position_ > -1) { // The two-pixel width drop indicator. static const int kDropIndicatorWidth = 2; gfx::Rect indicator_bounds( drop_indicator_position_ - (kDropIndicatorWidth / 2), ToolbarView::kVertSpacing, kDropIndicatorWidth, IconHeight()); // Color of the drop indicator. static const SkColor kDropIndicatorColor = SK_ColorBLACK; canvas->FillRectInt(kDropIndicatorColor, indicator_bounds.x(), indicator_bounds.y(), indicator_bounds.width(), indicator_bounds.height()); } } void BrowserActionsContainer::OnThemeChanged() { LoadImages(); } void BrowserActionsContainer::ViewHierarchyChanged(bool is_add, views::View* parent, views::View* child) { // No extensions (e.g., incognito). if (!model_) return; if (is_add && child == this) { // Initial toolbar button creation and placement in the widget hierarchy. // We do this here instead of in the constructor because AddBrowserAction // calls Layout on the Toolbar, which needs this object to be constructed // before its Layout function is called. CreateBrowserActionViews(); } } // static int BrowserActionsContainer::IconWidth(bool include_padding) { static bool initialized = false; static int icon_width = 0; if (!initialized) { initialized = true; icon_width = ResourceBundle::GetSharedInstance().GetBitmapNamed( IDR_BROWSER_ACTION)->width(); } return icon_width + (include_padding ? kItemSpacing : 0); } // static int BrowserActionsContainer::IconHeight() { static bool initialized = false; static int icon_height = 0; if (!initialized) { initialized = true; icon_height = ResourceBundle::GetSharedInstance().GetBitmapNamed( IDR_BROWSER_ACTION)->height(); } return icon_height; } void BrowserActionsContainer::BrowserActionAdded(const Extension* extension, int index) { #if defined(DEBUG) for (size_t i = 0; i < browser_action_views_.size(); ++i) { DCHECK(browser_action_views_[i]->button()->extension() != extension) << "Asked to add a browser action view for an extension that already " "exists."; } #endif CloseOverflowMenu(); if (!ShouldDisplayBrowserAction(extension)) return; size_t visible_actions = VisibleBrowserActions(); // Add the new browser action to the vector and the view hierarchy. if (profile_->IsOffTheRecord()) index = model_->OriginalIndexToIncognito(index); BrowserActionView* view = new BrowserActionView(extension, this); browser_action_views_.insert(browser_action_views_.begin() + index, view); AddChildViewAt(view, index); // If we are still initializing the container, don't bother animating. if (!model_->extensions_initialized()) return; // Enlarge the container if it was already at maximum size and we're not in // the middle of upgrading. if ((model_->GetVisibleIconCount() < 0) && !profile_->GetExtensionService()->IsBeingUpgraded(extension)) { suppress_chevron_ = true; SaveDesiredSizeAndAnimate(ui::Tween::LINEAR, visible_actions + 1); } else { // Just redraw the (possibly modified) visible icon set. OnBrowserActionVisibilityChanged(); } } void BrowserActionsContainer::BrowserActionRemoved(const Extension* extension) { CloseOverflowMenu(); if (popup_ && popup_->host()->extension() == extension) HidePopup(); size_t visible_actions = VisibleBrowserActions(); for (BrowserActionViews::iterator iter = browser_action_views_.begin(); iter != browser_action_views_.end(); ++iter) { if ((*iter)->button()->extension() == extension) { RemoveChildView(*iter); delete *iter; browser_action_views_.erase(iter); // If the extension is being upgraded we don't want the bar to shrink // because the icon is just going to get re-added to the same location. if (profile_->GetExtensionService()->IsBeingUpgraded(extension)) return; if (browser_action_views_.size() > visible_actions) { // If we have more icons than we can show, then we must not be changing // the container size (since we either removed an icon from the main // area and one from the overflow list will have shifted in, or we // removed an entry directly from the overflow list). OnBrowserActionVisibilityChanged(); } else { // Either we went from overflow to no-overflow, or we shrunk the no- // overflow container by 1. Either way the size changed, so animate. chevron_->SetVisible(false); SaveDesiredSizeAndAnimate(ui::Tween::EASE_OUT, browser_action_views_.size()); } return; } } } void BrowserActionsContainer::BrowserActionMoved(const Extension* extension, int index) { if (!ShouldDisplayBrowserAction(extension)) return; if (profile_->IsOffTheRecord()) index = model_->OriginalIndexToIncognito(index); DCHECK(index >= 0 && index < static_cast<int>(browser_action_views_.size())); DeleteBrowserActionViews(); CreateBrowserActionViews(); Layout(); SchedulePaint(); } void BrowserActionsContainer::ModelLoaded() { SetContainerWidth(); } void BrowserActionsContainer::LoadImages() { ui::ThemeProvider* tp = GetThemeProvider(); chevron_->SetIcon(*tp->GetBitmapNamed(IDR_BROWSER_ACTIONS_OVERFLOW)); chevron_->SetHoverIcon(*tp->GetBitmapNamed(IDR_BROWSER_ACTIONS_OVERFLOW_H)); chevron_->SetPushedIcon(*tp->GetBitmapNamed(IDR_BROWSER_ACTIONS_OVERFLOW_P)); } void BrowserActionsContainer::SetContainerWidth() { int visible_actions = model_->GetVisibleIconCount(); if (visible_actions < 0) // All icons should be visible. visible_actions = model_->size(); chevron_->SetVisible(static_cast<size_t>(visible_actions) < model_->size()); container_width_ = IconCountToWidth(visible_actions, chevron_->IsVisible()); } void BrowserActionsContainer::CloseOverflowMenu() { if (overflow_menu_) overflow_menu_->CancelMenu(); } void BrowserActionsContainer::StopShowFolderDropMenuTimer() { show_menu_task_factory_.RevokeAll(); } void BrowserActionsContainer::StartShowFolderDropMenuTimer() { int delay = views::GetMenuShowDelay(); MessageLoop::current()->PostDelayedTask(FROM_HERE, show_menu_task_factory_.NewRunnableMethod( &BrowserActionsContainer::ShowDropFolder), delay); } void BrowserActionsContainer::ShowDropFolder() { DCHECK(!overflow_menu_); SetDropIndicator(-1); overflow_menu_ = new BrowserActionOverflowMenuController( this, chevron_, browser_action_views_, VisibleBrowserActions()); overflow_menu_->set_observer(this); overflow_menu_->RunMenu(GetWindow()->GetNativeWindow(), true); } void BrowserActionsContainer::SetDropIndicator(int x_pos) { if (drop_indicator_position_ != x_pos) { drop_indicator_position_ = x_pos; SchedulePaint(); } } int BrowserActionsContainer::IconCountToWidth(int icons, bool display_chevron) const { if (icons < 0) icons = browser_action_views_.size(); if ((icons == 0) && !display_chevron) return ToolbarView::kStandardSpacing; int icons_size = (icons == 0) ? 0 : ((icons * IconWidth(true)) - kItemSpacing); int chevron_size = display_chevron ? (kChevronSpacing + chevron_->GetPreferredSize().width()) : 0; return ToolbarView::kStandardSpacing + icons_size + chevron_size + ToolbarView::kStandardSpacing; } size_t BrowserActionsContainer::WidthToIconCount(int pixels) const { // Check for widths large enough to show the entire icon set. if (pixels >= IconCountToWidth(-1, false)) return browser_action_views_.size(); // We need to reserve space for the resize area, chevron, and the spacing on // either side of the chevron. int available_space = pixels - ToolbarView::kStandardSpacing - chevron_->GetPreferredSize().width() - kChevronSpacing - ToolbarView::kStandardSpacing; // Now we add an extra between-item padding value so the space can be divided // evenly by (size of icon with padding). return static_cast<size_t>( std::max(0, available_space + kItemSpacing) / IconWidth(true)); } int BrowserActionsContainer::ContainerMinSize() const { return ToolbarView::kStandardSpacing + kChevronSpacing + chevron_->GetPreferredSize().width() + ToolbarView::kStandardSpacing; } void BrowserActionsContainer::SaveDesiredSizeAndAnimate( ui::Tween::Type tween_type, size_t num_visible_icons) { // Save off the desired number of visible icons. We do this now instead of at // the end of the animation so that even if the browser is shut down while // animating, the right value will be restored on next run. // NOTE: Don't save the icon count in incognito because there may be fewer // icons in that mode. The result is that the container in a normal window is // always at least as wide as in an incognito window. if (!profile_->IsOffTheRecord()) model_->SetVisibleIconCount(num_visible_icons); int target_size = IconCountToWidth(num_visible_icons, num_visible_icons < browser_action_views_.size()); if (!disable_animations_during_testing_) { // Animate! We have to set the animation_target_size_ after calling Reset(), // because that could end up calling AnimationEnded which clears the value. resize_animation_->Reset(); resize_animation_->SetTweenType(tween_type); animation_target_size_ = target_size; resize_animation_->Show(); } else { animation_target_size_ = target_size; AnimationEnded(resize_animation_.get()); } } bool BrowserActionsContainer::ShouldDisplayBrowserAction( const Extension* extension) { // Only display incognito-enabled extensions while in incognito mode. return (!profile_->IsOffTheRecord() || profile_->GetExtensionService()->IsIncognitoEnabled(extension->id())); }