| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/views/toolbar/browser_actions_container.h" |
| |
| #include "base/compiler_specific.h" |
| #include "base/stl_util.h" |
| #include "chrome/browser/extensions/extension_action_manager.h" |
| #include "chrome/browser/extensions/tab_helper.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/extensions/extension_action_view_controller.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/toolbar/component_toolbar_actions_factory.h" |
| #include "chrome/browser/ui/view_ids.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/frame/browser_view.h" |
| #include "chrome/browser/ui/views/toolbar/browser_actions_container_observer.h" |
| #include "chrome/browser/ui/views/toolbar/toolbar_view.h" |
| #include "chrome/common/extensions/command.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/crx_file/id_util.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/runtime_data.h" |
| #include "extensions/common/feature_switch.h" |
| #include "grit/theme_resources.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "ui/accessibility/ax_view_state.h" |
| #include "ui/base/dragdrop/drag_utils.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/nine_image_painter_factory.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/base/theme_provider.h" |
| #include "ui/gfx/animation/slide_animation.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/resources/grit/ui_resources.h" |
| #include "ui/views/controls/resize_area.h" |
| #include "ui/views/painter.h" |
| #include "ui/views/widget/widget.h" |
| |
| using extensions::Extension; |
| |
| namespace { |
| |
| // Horizontal spacing before the chevron (if visible). |
| const int kChevronSpacing = ToolbarView::kStandardSpacing - 2; |
| |
| // Returns the set of controllers for all actions. |
| // TODO(devlin): We should move this to the model, once it supports component |
| // actions. |
| ScopedVector<ToolbarActionViewController> GetToolbarActions( |
| extensions::ExtensionToolbarModel* model, |
| Browser* browser) { |
| ScopedVector<ToolbarActionViewController> actions; |
| |
| // Extension actions come first. |
| extensions::ExtensionActionManager* action_manager = |
| extensions::ExtensionActionManager::Get(browser->profile()); |
| const extensions::ExtensionList& toolbar_items = model->GetItemOrderForTab( |
| browser->tab_strip_model()->GetActiveWebContents()); |
| for (const scoped_refptr<const Extension>& extension : toolbar_items) { |
| actions.push_back(new ExtensionActionViewController( |
| extension.get(), |
| browser, |
| action_manager->GetExtensionAction(*extension))); |
| } |
| |
| // Component actions come second. |
| ScopedVector<ToolbarActionViewController> component_actions = |
| ComponentToolbarActionsFactory::GetInstance()-> |
| GetComponentToolbarActions(); |
| DCHECK(extensions::FeatureSwitch::extension_action_redesign()->IsEnabled() || |
| component_actions.empty()); |
| actions.insert( |
| actions.end(), component_actions.begin(), component_actions.end()); |
| component_actions.weak_clear(); |
| return actions.Pass(); |
| } |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // BrowserActionsContainer::DropPosition |
| |
| struct BrowserActionsContainer::DropPosition { |
| DropPosition(size_t row, size_t icon_in_row); |
| |
| // The (0-indexed) row into which the action will be dropped. |
| size_t row; |
| |
| // The (0-indexed) icon in the row before the action will be dropped. |
| size_t icon_in_row; |
| }; |
| |
| BrowserActionsContainer::DropPosition::DropPosition( |
| size_t row, size_t icon_in_row) |
| : row(row), icon_in_row(icon_in_row) { |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // BrowserActionsContainer |
| |
| // static |
| int BrowserActionsContainer::icons_per_overflow_menu_row_ = 1; |
| |
| // static |
| const int BrowserActionsContainer::kItemSpacing = ToolbarView::kStandardSpacing; |
| |
| // static |
| bool BrowserActionsContainer::disable_animations_during_testing_ = false; |
| |
| BrowserActionsContainer::BrowserActionsContainer( |
| Browser* browser, |
| BrowserActionsContainer* main_container) |
| : profile_(browser->profile()), |
| browser_(browser), |
| main_container_(main_container), |
| popup_owner_(NULL), |
| model_(extensions::ExtensionToolbarModel::Get(browser->profile())), |
| container_width_(0), |
| resize_area_(NULL), |
| chevron_(NULL), |
| suppress_chevron_(false), |
| suppress_animation_(false), |
| suppress_layout_(false), |
| resize_amount_(0), |
| animation_target_size_(0) { |
| set_id(VIEW_ID_BROWSER_ACTION_TOOLBAR); |
| if (model_) // |model_| can be NULL in views unittests. |
| model_->AddObserver(this); |
| |
| bool overflow_experiment = |
| extensions::FeatureSwitch::extension_action_redesign()->IsEnabled(); |
| DCHECK(!in_overflow_mode() || overflow_experiment); |
| |
| if (!in_overflow_mode()) { |
| resize_animation_.reset(new gfx::SlideAnimation(this)); |
| resize_area_ = new views::ResizeArea(this); |
| AddChildView(resize_area_); |
| |
| // 'Main' mode doesn't need a chevron overflow when overflow is shown inside |
| // the Chrome menu. |
| if (!overflow_experiment) { |
| // Since the ChevronMenuButton holds a raw pointer to us, we need to |
| // ensure it doesn't outlive us. Having it owned by the view hierarchy as |
| // a child will suffice. |
| chevron_ = new ChevronMenuButton(this); |
| chevron_->EnableCanvasFlippingForRTLUI(true); |
| chevron_->SetAccessibleName( |
| l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS_CHEVRON)); |
| chevron_->SetVisible(false); |
| AddChildView(chevron_); |
| } |
| } |
| } |
| |
| BrowserActionsContainer::~BrowserActionsContainer() { |
| FOR_EACH_OBSERVER(BrowserActionsContainerObserver, |
| observers_, |
| OnBrowserActionsContainerDestroyed()); |
| |
| if (model_) |
| model_->RemoveObserver(this); |
| HideActivePopup(); |
| DeleteToolbarActionViews(); |
| } |
| |
| 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_ && model_->extensions_initialized()) { |
| container_width_ = GetPreferredWidth(); |
| SetChevronVisibility(); |
| } |
| } |
| |
| const std::string& BrowserActionsContainer::GetIdAt(size_t index) { |
| return toolbar_action_views_[index]->view_controller()->GetId(); |
| } |
| |
| ToolbarActionView* BrowserActionsContainer::GetViewForExtension( |
| const Extension* extension) { |
| for (ToolbarActionView* view : toolbar_action_views_) { |
| if (view->view_controller()->GetId() == extension->id()) |
| return view; |
| } |
| return nullptr; |
| } |
| |
| void BrowserActionsContainer::RefreshToolbarActionViews() { |
| if (toolbar_action_views_.empty()) |
| return; // Nothing to do. |
| |
| // When we do a bulk-refresh of views (such as when we switch tabs), we don't |
| // animate the difference. We only animate when it's a change driven by the |
| // action. |
| base::AutoReset<bool> animation_resetter(&suppress_animation_, true); |
| |
| { |
| // Don't layout until the end. |
| base::AutoReset<bool> layout_resetter(&suppress_layout_, true); |
| for (ToolbarActionView* view : toolbar_action_views_) |
| view->UpdateState(); |
| } |
| |
| ReorderViews(); // Also triggers a layout. |
| } |
| |
| void BrowserActionsContainer::CreateToolbarActionViews() { |
| DCHECK(toolbar_action_views_.empty()); |
| if (!model_) |
| return; |
| |
| { |
| // We don't Layout while creating views. Instead, Layout() once at the end. |
| base::AutoReset<bool> layout_resetter(&suppress_layout_, true); |
| |
| ScopedVector<ToolbarActionViewController> actions = |
| GetToolbarActions(model_, browser_); |
| for (ToolbarActionViewController* controller : actions) { |
| ToolbarActionView* view = |
| new ToolbarActionView(make_scoped_ptr(controller), browser_, this); |
| toolbar_action_views_.push_back(view); |
| AddChildView(view); |
| } |
| actions.weak_clear(); |
| } |
| |
| Layout(); |
| SchedulePaint(); |
| } |
| |
| void BrowserActionsContainer::DeleteToolbarActionViews() { |
| HideActivePopup(); |
| STLDeleteElements(&toolbar_action_views_); |
| } |
| |
| size_t BrowserActionsContainer::VisibleBrowserActions() const { |
| size_t visible_actions = 0; |
| for (const ToolbarActionView* view : toolbar_action_views_) { |
| if (view->visible()) |
| ++visible_actions; |
| } |
| return visible_actions; |
| } |
| |
| size_t BrowserActionsContainer::VisibleBrowserActionsAfterAnimation() const { |
| if (!animating()) |
| return VisibleBrowserActions(); |
| |
| return WidthToIconCount(animation_target_size_); |
| } |
| |
| void BrowserActionsContainer::ExecuteExtensionCommand( |
| const extensions::Extension* extension, |
| const extensions::Command& command) { |
| // Global commands are handled by the ExtensionCommandsGlobalRegistry |
| // instance. |
| DCHECK(!command.global()); |
| extension_keybinding_registry_->ExecuteCommand(extension->id(), |
| command.accelerator()); |
| } |
| |
| void BrowserActionsContainer::NotifyActionMovedToOverflow() { |
| // When an action is moved to overflow, we shrink the size of the container |
| // by 1. |
| size_t icon_count = model_->visible_icon_count(); |
| // Since this happens when an icon moves from the main bar to overflow, we |
| // can't possibly have had no visible icons on the main bar. |
| DCHECK_NE(0u, icon_count); |
| model_->SetVisibleIconCount(icon_count - 1); |
| } |
| |
| bool BrowserActionsContainer::ShownInsideMenu() const { |
| return in_overflow_mode(); |
| } |
| |
| void BrowserActionsContainer::OnToolbarActionViewDragDone() { |
| ToolbarVisibleCountChanged(); |
| FOR_EACH_OBSERVER(BrowserActionsContainerObserver, |
| observers_, |
| OnBrowserActionDragDone()); |
| } |
| |
| views::MenuButton* BrowserActionsContainer::GetOverflowReferenceView() { |
| // With traditional overflow, the reference is the chevron. With the |
| // redesign, we use the wrench menu instead. |
| return chevron_ ? |
| chevron_ : |
| BrowserView::GetBrowserViewForBrowser(browser_)->toolbar()->app_menu(); |
| } |
| |
| void BrowserActionsContainer::SetPopupOwner(ToolbarActionView* popup_owner) { |
| // We should never be setting a popup owner when one already exists, and |
| // never unsetting one when one wasn't set. |
| DCHECK((!popup_owner_ && popup_owner) || |
| (popup_owner_ && !popup_owner)); |
| popup_owner_ = popup_owner; |
| } |
| |
| void BrowserActionsContainer::HideActivePopup() { |
| if (popup_owner_) |
| popup_owner_->view_controller()->HidePopup(); |
| } |
| |
| ToolbarActionView* BrowserActionsContainer::GetMainViewForAction( |
| ToolbarActionView* view) { |
| if (!in_overflow_mode()) |
| return view; // This is the main view. |
| |
| // The overflow container and main container each have the same views and |
| // view indices, so we can return the view of the index that |view| has in |
| // this container. |
| ToolbarActionViews::const_iterator iter = |
| std::find(toolbar_action_views_.begin(), |
| toolbar_action_views_.end(), |
| view); |
| DCHECK(iter != toolbar_action_views_.end()); |
| size_t index = iter - toolbar_action_views_.begin(); |
| return main_container_->toolbar_action_views_[index]; |
| } |
| |
| void BrowserActionsContainer::AddObserver( |
| BrowserActionsContainerObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void BrowserActionsContainer::RemoveObserver( |
| BrowserActionsContainerObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| gfx::Size BrowserActionsContainer::GetPreferredSize() const { |
| if (in_overflow_mode()) { |
| int icon_count = GetIconCount(); |
| // In overflow, we always have a preferred size of a full row (even if we |
| // don't use it), and always of at least one row. The parent may decide to |
| // show us even when empty, e.g. as a drag target for dragging in icons from |
| // the main container. |
| int row_count = |
| ((std::max(0, icon_count - 1)) / icons_per_overflow_menu_row_) + 1; |
| return gfx::Size(IconCountToWidth(icons_per_overflow_menu_row_), |
| row_count * IconHeight()); |
| } |
| |
| // If there are no actions to show, then don't show the container at all. |
| if (toolbar_action_views_.empty()) |
| return gfx::Size(); |
| |
| // 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: MinimumNonemptyWidth() < width() - resize < ClampTo(MAX). |
| int preferred_width = std::min( |
| std::max(MinimumNonemptyWidth(), container_width_ - resize_amount_), |
| IconCountToWidth(-1)); |
| return gfx::Size(preferred_width, IconHeight()); |
| } |
| |
| int BrowserActionsContainer::GetHeightForWidth(int width) const { |
| if (in_overflow_mode()) |
| icons_per_overflow_menu_row_ = (width - kItemSpacing) / IconWidth(true); |
| return GetPreferredSize().height(); |
| } |
| |
| gfx::Size BrowserActionsContainer::GetMinimumSize() const { |
| int min_width = std::min(MinimumNonemptyWidth(), IconCountToWidth(-1)); |
| return gfx::Size(min_width, IconHeight()); |
| } |
| |
| void BrowserActionsContainer::Layout() { |
| if (suppress_layout_) |
| return; |
| |
| if (toolbar_action_views_.empty()) { |
| SetVisible(false); |
| return; |
| } |
| |
| SetVisible(true); |
| if (resize_area_) |
| resize_area_->SetBounds(0, 0, kItemSpacing, height()); |
| |
| // If the icons don't all fit, show the chevron (unless suppressed). |
| int max_x = GetPreferredSize().width(); |
| if (IconCountToWidth(-1) > max_x && !suppress_chevron_ && chevron_) { |
| chevron_->SetVisible(true); |
| gfx::Size chevron_size(chevron_->GetPreferredSize()); |
| max_x -= chevron_size.width() + kChevronSpacing; |
| chevron_->SetBounds( |
| width() - ToolbarView::kStandardSpacing - chevron_size.width(), |
| 0, |
| chevron_size.width(), |
| chevron_size.height()); |
| } else if (chevron_) { |
| chevron_->SetVisible(false); |
| } |
| |
| // The padding before the first icon and after the last icon in the container. |
| int container_padding = |
| in_overflow_mode() ? kItemSpacing : ToolbarView::kStandardSpacing; |
| // The range of visible icons, from start_index (inclusive) to end_index |
| // (exclusive). |
| size_t start_index = in_overflow_mode() ? |
| main_container_->VisibleBrowserActionsAfterAnimation() : 0u; |
| // For the main container's last visible icon, we calculate how many icons we |
| // can display with the given width. We add an extra kItemSpacing because the |
| // last icon doesn't need padding, but we want it to divide easily. |
| size_t end_index = in_overflow_mode() ? |
| toolbar_action_views_.size() : |
| (max_x - 2 * container_padding + kItemSpacing) / IconWidth(true); |
| // The maximum length for one row of icons. |
| size_t row_length = |
| in_overflow_mode() ? icons_per_overflow_menu_row_ : end_index; |
| |
| // Now draw the icons for the actions in the available space. Once all the |
| // variables are in place, the layout works equally well for the main and |
| // overflow container. |
| for (size_t i = 0u; i < toolbar_action_views_.size(); ++i) { |
| ToolbarActionView* view = toolbar_action_views_[i]; |
| if (i < start_index || i >= end_index) { |
| view->SetVisible(false); |
| } else { |
| size_t relative_index = i - start_index; |
| size_t index_in_row = relative_index % row_length; |
| size_t row_index = relative_index / row_length; |
| view->SetBounds(container_padding + index_in_row * IconWidth(true), |
| row_index * IconHeight(), |
| IconWidth(false), |
| IconHeight()); |
| view->SetVisible(true); |
| } |
| } |
| } |
| |
| bool BrowserActionsContainer::GetDropFormats( |
| int* formats, |
| std::set<OSExchangeData::CustomFormat>* custom_formats) { |
| return BrowserActionDragData::GetDropFormats(custom_formats); |
| } |
| |
| bool BrowserActionsContainer::AreDropTypesRequired() { |
| return BrowserActionDragData::AreDropTypesRequired(); |
| } |
| |
| bool BrowserActionsContainer::CanDrop(const OSExchangeData& data) { |
| return BrowserActionDragData::CanDrop(data, profile_); |
| } |
| |
| int BrowserActionsContainer::OnDragUpdated( |
| const ui::DropTargetEvent& event) { |
| size_t row_index = 0; |
| size_t before_icon_in_row = 0; |
| // If there are no visible actions (such as when dragging an icon to an empty |
| // overflow/main container), then 0, 0 for row, column is correct. |
| if (VisibleBrowserActions() != 0) { |
| // Figure out where to display the indicator. This is a complex calculation: |
| |
| // First, we subtract out the padding to the left of the icon area, which is |
| // ToolbarView::kStandardSpacing. If we're right-to-left, we also mirror the |
| // event.x() so that our calculations are consistent with left-to-right. |
| int offset_into_icon_area = |
| GetMirroredXInView(event.x()) - ToolbarView::kStandardSpacing; |
| |
| // Next, figure out what row we're on. This only matters for overflow mode, |
| // but the calculation is the same for both. |
| row_index = event.y() / IconHeight(); |
| |
| // Sanity check - we should never be on a different row in the main |
| // container. |
| DCHECK(in_overflow_mode() || row_index == 0); |
| |
| // 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); |
| |
| // We need to figure out how many icons are visible on the relevant row. |
| // In the main container, this will just be the visible actions. |
| int visible_icons_on_row = VisibleBrowserActionsAfterAnimation(); |
| if (in_overflow_mode()) { |
| // If this is the final row of the overflow, then this is the remainder of |
| // visible icons. Otherwise, it's a full row (kIconsPerRow). |
| visible_icons_on_row = |
| row_index == |
| static_cast<size_t>(visible_icons_on_row / |
| icons_per_overflow_menu_row_) ? |
| visible_icons_on_row % icons_per_overflow_menu_row_ : |
| icons_per_overflow_menu_row_; |
| } |
| |
| // 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". |
| before_icon_in_row = |
| std::min(std::max(before_icon_unclamped, 0), visible_icons_on_row); |
| } |
| |
| if (!drop_position_.get() || |
| !(drop_position_->row == row_index && |
| drop_position_->icon_in_row == before_icon_in_row)) { |
| drop_position_.reset(new DropPosition(row_index, before_icon_in_row)); |
| SchedulePaint(); |
| } |
| |
| return ui::DragDropTypes::DRAG_MOVE; |
| } |
| |
| void BrowserActionsContainer::OnDragExited() { |
| drop_position_.reset(); |
| SchedulePaint(); |
| } |
| |
| int BrowserActionsContainer::OnPerformDrop( |
| const ui::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(GetIdAt(data.index()), data.id()); |
| DCHECK(model_); |
| |
| size_t i = drop_position_->row * icons_per_overflow_menu_row_ + |
| drop_position_->icon_in_row; |
| if (in_overflow_mode()) |
| i += main_container_->VisibleBrowserActionsAfterAnimation(); |
| // |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 this was a drag between containers, we will have to adjust the number of |
| // visible icons. |
| bool drag_between_containers = |
| !toolbar_action_views_[data.index()]->visible(); |
| model_->MoveExtensionIcon(GetIdAt(data.index()), i); |
| |
| if (drag_between_containers) { |
| // Let the main container update the model. |
| if (in_overflow_mode()) |
| main_container_->NotifyActionMovedToOverflow(); |
| else // This is the main container. |
| model_->SetVisibleIconCount(model_->visible_icon_count() + 1); |
| } |
| |
| OnDragExited(); // Perform clean up after dragging. |
| return ui::DragDropTypes::DRAG_MOVE; |
| } |
| |
| void BrowserActionsContainer::GetAccessibleState( |
| ui::AXViewState* state) { |
| state->role = ui::AX_ROLE_GROUP; |
| state->name = l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS); |
| } |
| |
| void BrowserActionsContainer::WriteDragDataForView(View* sender, |
| const gfx::Point& press_pt, |
| OSExchangeData* data) { |
| DCHECK(data); |
| |
| ToolbarActionViews::iterator iter = std::find(toolbar_action_views_.begin(), |
| toolbar_action_views_.end(), |
| sender); |
| DCHECK(iter != toolbar_action_views_.end()); |
| ToolbarActionViewController* view_controller = (*iter)->view_controller(); |
| drag_utils::SetDragImageOnDataObject( |
| view_controller->GetIconWithBadge(), |
| press_pt.OffsetFromOrigin(), |
| data); |
| // Fill in the remaining info. |
| BrowserActionDragData drag_data(view_controller->GetId(), |
| iter - toolbar_action_views_.begin()); |
| drag_data.Write(profile_, data); |
| } |
| |
| 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) { |
| // We don't allow dragging while we're highlighting. |
| return !model_->is_highlighting(); |
| } |
| |
| 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); |
| container_width_ = |
| std::min(std::max(0, container_width_ - resize_amount), max_width); |
| |
| // 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. |
| int visible_icons = WidthToIconCount(container_width_); |
| model_->SetVisibleIconCount(visible_icons); |
| } |
| |
| void BrowserActionsContainer::AnimationProgressed( |
| const gfx::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 gfx::Animation* animation) { |
| container_width_ = animation_target_size_; |
| animation_target_size_ = 0; |
| resize_amount_ = 0; |
| suppress_chevron_ = false; |
| SetChevronVisibility(); |
| OnBrowserActionVisibilityChanged(); |
| |
| FOR_EACH_OBSERVER(BrowserActionsContainerObserver, |
| observers_, |
| OnBrowserActionsContainerAnimationEnded()); |
| } |
| |
| content::WebContents* BrowserActionsContainer::GetCurrentWebContents() { |
| return browser_->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| extensions::ActiveTabPermissionGranter* |
| BrowserActionsContainer::GetActiveTabPermissionGranter() { |
| content::WebContents* web_contents = GetCurrentWebContents(); |
| if (!web_contents) |
| return NULL; |
| return extensions::TabHelper::FromWebContents(web_contents)-> |
| active_tab_permission_granter(); |
| } |
| |
| gfx::NativeView BrowserActionsContainer::TestGetPopup() { |
| return popup_owner_ ? |
| popup_owner_->view_controller()->GetPopupNativeView() : |
| NULL; |
| } |
| |
| void BrowserActionsContainer::OnPaint(gfx::Canvas* canvas) { |
| // If the views haven't been initialized yet, wait for the next call to |
| // paint (one will be triggered by entering highlight mode). |
| if (model_->is_highlighting() && !toolbar_action_views_.empty() && |
| !in_overflow_mode()) { |
| views::Painter::PaintPainterAt( |
| canvas, highlight_painter_.get(), GetLocalBounds()); |
| } |
| |
| // TODO(sky/glen): Instead of using a drop indicator, animate the icons while |
| // dragging (like we do for tab dragging). |
| if (drop_position_.get()) { |
| // The two-pixel width drop indicator. |
| static const int kDropIndicatorWidth = 2; |
| |
| // Convert back to a pixel offset into the container. First find the X |
| // coordinate of the drop icon. |
| int drop_icon_x = ToolbarView::kStandardSpacing + |
| (drop_position_->icon_in_row * IconWidth(true)); |
| // Next, find the space before the drop icon. This will either be |
| // kItemSpacing or ToolbarView::kStandardSpacing, depending on whether this |
| // is the first icon. |
| // NOTE: Right now, these are the same. But let's do this right for if they |
| // ever aren't. |
| int space_before_drop_icon = drop_position_->icon_in_row == 0 ? |
| ToolbarView::kStandardSpacing : kItemSpacing; |
| // Now place the drop indicator halfway between this and the end of the |
| // previous icon. If there is an odd amount of available space between the |
| // two icons (or the icon and the address bar) after subtracting the drop |
| // indicator width, this calculation puts the extra pixel on the left side |
| // of the indicator, since when the indicator is between the address bar and |
| // the first icon, it looks better closer to the icon. |
| int drop_indicator_x = drop_icon_x - |
| ((space_before_drop_icon + kDropIndicatorWidth) / 2); |
| int row_height = IconHeight(); |
| int drop_indicator_y = row_height * drop_position_->row; |
| gfx::Rect indicator_bounds(drop_indicator_x, |
| drop_indicator_y, |
| kDropIndicatorWidth, |
| row_height); |
| indicator_bounds.set_x(GetMirroredXForRect(indicator_bounds)); |
| |
| // Color of the drop indicator. |
| static const SkColor kDropIndicatorColor = SK_ColorBLACK; |
| canvas->FillRect(indicator_bounds, kDropIndicatorColor); |
| } |
| } |
| |
| void BrowserActionsContainer::OnThemeChanged() { |
| LoadImages(); |
| } |
| |
| void BrowserActionsContainer::ViewHierarchyChanged( |
| const ViewHierarchyChangedDetails& details) { |
| if (!model_) |
| return; |
| |
| if (details.is_add && details.child == this) { |
| if (!in_overflow_mode()) { // We only need one keybinding registry. |
| extension_keybinding_registry_.reset(new ExtensionKeybindingRegistryViews( |
| browser_->profile(), |
| parent()->GetFocusManager(), |
| extensions::ExtensionKeybindingRegistry::ALL_EXTENSIONS, |
| 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. |
| // If the model is not initialized, this is called later from |
| // OnToolbarModelInitialized(). |
| if (model_->extensions_initialized()) |
| CreateToolbarActionViews(); |
| } |
| } |
| |
| // static |
| int BrowserActionsContainer::IconWidth(bool include_padding) { |
| static bool initialized = false; |
| static int icon_width = 0; |
| if (!initialized) { |
| initialized = true; |
| icon_width = ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| 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 = ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| IDR_BROWSER_ACTION)->height(); |
| } |
| return icon_height; |
| } |
| |
| void BrowserActionsContainer::ToolbarExtensionAdded(const Extension* extension, |
| int index) { |
| DCHECK(!GetViewForExtension(extension)) << |
| "Asked to add a browser action view for an extension that already exists"; |
| if (chevron_) |
| chevron_->CloseMenu(); |
| |
| // Add the new action to the vector and the view hierarchy. |
| ToolbarActionView* view = new ToolbarActionView( |
| make_scoped_ptr(new ExtensionActionViewController( |
| extension, |
| browser_, |
| extensions::ExtensionActionManager::Get(profile_)-> |
| GetExtensionAction(*extension))), |
| browser_, |
| this); |
| toolbar_action_views_.insert(toolbar_action_views_.begin() + index, view); |
| AddChildViewAt(view, index); |
| |
| // If we are still initializing the container, don't bother animating. |
| if (!model_->extensions_initialized()) |
| return; |
| |
| // If this is just an upgrade, then don't worry about resizing. |
| if (!extensions::ExtensionSystem::Get(profile_)->runtime_data()-> |
| IsBeingUpgraded(extension)) { |
| // We need to resize if either: |
| // - The container is set to display all icons, or |
| // - The container will need to expand to include the chevron. This can |
| // happen when the container is set to display <n> icons, where <n> is |
| // the number of icons before the new icon. With the new icon, the chevron |
| // will need to be displayed. |
| if (model_->all_icons_visible() || |
| (model_->visible_icon_count() < toolbar_action_views_.size() && |
| (chevron_ && !chevron_->visible()))) { |
| suppress_chevron_ = true; |
| Animate(gfx::Tween::LINEAR, GetIconCount()); |
| return; |
| } |
| } |
| |
| // Otherwise, we don't have to resize, so just redraw the (possibly modified) |
| // visible icon set. |
| OnBrowserActionVisibilityChanged(); |
| } |
| |
| void BrowserActionsContainer::ToolbarExtensionRemoved( |
| const Extension* extension) { |
| if (chevron_) |
| chevron_->CloseMenu(); |
| |
| size_t visible_actions = VisibleBrowserActionsAfterAnimation(); |
| for (ToolbarActionViews::iterator i(toolbar_action_views_.begin()); |
| i != toolbar_action_views_.end(); ++i) { |
| if ((*i)->view_controller()->GetId() == extension->id()) { |
| delete *i; |
| toolbar_action_views_.erase(i); |
| |
| // 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 (extensions::ExtensionSystem::Get(profile_)->runtime_data()-> |
| IsBeingUpgraded(extension)) |
| return; |
| |
| if (toolbar_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. |
| if (chevron_) |
| chevron_->SetVisible(false); |
| Animate(gfx::Tween::EASE_OUT, toolbar_action_views_.size()); |
| } |
| return; // We have found the action to remove, bail out. |
| } |
| } |
| } |
| |
| void BrowserActionsContainer::ToolbarExtensionMoved(const Extension* extension, |
| int index) { |
| DCHECK(index >= 0 && index < static_cast<int>(toolbar_action_views_.size())); |
| |
| ToolbarActionViews::iterator iter = toolbar_action_views_.begin(); |
| while (iter != toolbar_action_views_.end() && |
| (*iter)->view_controller()->GetId() != extension->id()) |
| ++iter; |
| |
| DCHECK(iter != toolbar_action_views_.end()); |
| if (iter - toolbar_action_views_.begin() == index) |
| return; // Already in place. |
| |
| ToolbarActionView* moved_view = *iter; |
| toolbar_action_views_.erase(iter); |
| toolbar_action_views_.insert( |
| toolbar_action_views_.begin() + index, moved_view); |
| |
| Layout(); |
| SchedulePaint(); |
| } |
| |
| void BrowserActionsContainer::ToolbarExtensionUpdated( |
| const Extension* extension) { |
| ToolbarActionView* view = GetViewForExtension(extension); |
| // There might not be a view in cases where we are highlighting or if we |
| // haven't fully initialized extensions. |
| if (view) |
| view->UpdateState(); |
| } |
| |
| bool BrowserActionsContainer::ShowExtensionActionPopup( |
| const Extension* extension, |
| bool grant_active_tab) { |
| // Don't override another popup, and only show in the active window. |
| if (popup_owner_ || !browser_->window()->IsActive()) |
| return false; |
| |
| ToolbarActionView* view = GetViewForExtension(extension); |
| return view && view->view_controller()->ExecuteAction(grant_active_tab); |
| } |
| |
| void BrowserActionsContainer::ToolbarVisibleCountChanged() { |
| if (GetPreferredWidth() != container_width_) { |
| Animate(gfx::Tween::EASE_OUT, GetIconCount()); |
| } else if (animation_target_size_ != 0) { |
| // It's possible that we're right where we're supposed to be in terms of |
| // icon count, but that we're also currently resizing. If this is the case, |
| // end the current animation with the current width. |
| animation_target_size_ = container_width_; |
| resize_animation_->Reset(); |
| } |
| } |
| |
| void BrowserActionsContainer::ToolbarHighlightModeChanged( |
| bool is_highlighting) { |
| // The visual highlighting is done in OnPaint(). It's a bit of a pain that |
| // we delete and recreate everything here, but given everything else going on |
| // (the lack of highlight, n more extensions appearing, etc), it's not worth |
| // the extra complexity to create and insert only the new extensions. |
| DeleteToolbarActionViews(); |
| CreateToolbarActionViews(); |
| Animate(gfx::Tween::LINEAR, GetIconCount()); |
| } |
| |
| void BrowserActionsContainer::OnToolbarModelInitialized() { |
| CreateToolbarActionViews(); |
| ToolbarVisibleCountChanged(); |
| } |
| |
| void BrowserActionsContainer::OnToolbarReorderNecessary( |
| content::WebContents* web_contents) { |
| if (GetCurrentWebContents() == web_contents) |
| ReorderViews(); |
| } |
| |
| Browser* BrowserActionsContainer::GetBrowser() { |
| return browser_; |
| } |
| |
| void BrowserActionsContainer::LoadImages() { |
| if (in_overflow_mode()) |
| return; // Overflow mode has neither a chevron nor highlighting. |
| |
| ui::ThemeProvider* tp = GetThemeProvider(); |
| if (tp && chevron_) { |
| chevron_->SetImage(views::Button::STATE_NORMAL, |
| *tp->GetImageSkiaNamed(IDR_BROWSER_ACTIONS_OVERFLOW)); |
| } |
| |
| const int kImages[] = IMAGE_GRID(IDR_DEVELOPER_MODE_HIGHLIGHT); |
| highlight_painter_.reset(views::Painter::CreateImageGridPainter(kImages)); |
| } |
| |
| void BrowserActionsContainer::OnBrowserActionVisibilityChanged() { |
| SetVisible(!toolbar_action_views_.empty()); |
| if (parent()) { // Parent can be null in testing. |
| parent()->Layout(); |
| parent()->SchedulePaint(); |
| } |
| } |
| |
| int BrowserActionsContainer::GetPreferredWidth() { |
| return IconCountToWidth(GetIconCount()); |
| } |
| |
| void BrowserActionsContainer::SetChevronVisibility() { |
| if (chevron_) { |
| chevron_->SetVisible( |
| VisibleBrowserActionsAfterAnimation() < toolbar_action_views_.size()); |
| } |
| } |
| |
| int BrowserActionsContainer::IconCountToWidth(int icons) const { |
| if (icons < 0) |
| icons = toolbar_action_views_.size(); |
| bool display_chevron = |
| chevron_ && static_cast<size_t>(icons) < toolbar_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; |
| // In overflow mode, our padding is to use item spacing on either end (just so |
| // we can see the drop indicator). Otherwise we use the standard toolbar |
| // spacing. |
| // Note: These are actually the same thing, but, on the offchance one |
| // changes, let's get it right. |
| int padding = |
| 2 * (in_overflow_mode() ? kItemSpacing : ToolbarView::kStandardSpacing); |
| return icons_size + chevron_size + padding; |
| } |
| |
| size_t BrowserActionsContainer::WidthToIconCount(int pixels) const { |
| // Check for widths large enough to show the entire icon set. |
| if (pixels >= IconCountToWidth(-1)) |
| return toolbar_action_views_.size(); |
| |
| // We reserve space for the padding on either side of the toolbar... |
| int available_space = pixels - (ToolbarView::kStandardSpacing * 2); |
| // ... and, if the chevron is enabled, the chevron. |
| if (chevron_) |
| available_space -= (chevron_->GetPreferredSize().width() + kChevronSpacing); |
| |
| // 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::MinimumNonemptyWidth() const { |
| if (!chevron_) |
| return ToolbarView::kStandardSpacing; |
| return (ToolbarView::kStandardSpacing * 2) + kChevronSpacing + |
| chevron_->GetPreferredSize().width(); |
| } |
| |
| void BrowserActionsContainer::Animate(gfx::Tween::Type tween_type, |
| size_t num_visible_icons) { |
| int target_size = IconCountToWidth(num_visible_icons); |
| if (resize_animation_ && !disable_animations_during_testing_ && |
| !suppress_animation_) { |
| // 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()); |
| } |
| } |
| |
| void BrowserActionsContainer::ReorderViews() { |
| extensions::ExtensionList new_order = |
| model_->GetItemOrderForTab(GetCurrentWebContents()); |
| if (new_order.empty()) |
| return; // Nothing to do. |
| |
| #if DCHECK_IS_ON |
| // Make sure the lists are in sync. There should be a view for each action in |
| // the new order. |
| // |toolbar_action_views_| may have more views than actions are present in |
| // |new_order| if there are any component toolbar actions. |
| // TODO(devlin): Change this to DCHECK_EQ when all toolbar actions are shown |
| // in the model. |
| DCHECK_LE(new_order.size(), toolbar_action_views_.size()); |
| for (const scoped_refptr<const Extension>& extension : new_order) |
| DCHECK(GetViewForExtension(extension.get())); |
| #endif |
| |
| // Run through the views and compare them to the desired order. If something |
| // is out of place, find the correct spot for it. |
| for (size_t i = 0; i < new_order.size() - 1; ++i) { |
| if (new_order[i]->id() != |
| toolbar_action_views_[i]->view_controller()->GetId()) { |
| // Find where the correct view is (it's guaranteed to be after our current |
| // index, since everything up to this point is correct). |
| size_t j = i + 1; |
| while (new_order[i]->id() != |
| toolbar_action_views_[j]->view_controller()->GetId()) |
| ++j; |
| std::swap(toolbar_action_views_[i], toolbar_action_views_[j]); |
| } |
| } |
| |
| // Our visible browser actions may have changed - re-Layout() and check the |
| // size. |
| ToolbarVisibleCountChanged(); |
| OnBrowserActionVisibilityChanged(); |
| } |
| |
| size_t BrowserActionsContainer::GetIconCount() const { |
| if (!model_) |
| return 0u; |
| |
| // Find the absolute value for the model's visible count. |
| size_t model_visible_size = model_->GetVisibleIconCountForTab( |
| browser_->tab_strip_model()->GetActiveWebContents()); |
| |
| #if DCHECK_IS_ON |
| // Good time for some sanity checks: We should never try to display more |
| // icons than we have, and we should always have a view per item in the model. |
| // (The only exception is if this is in initialization.) |
| if (initialized_ && !suppress_layout_) { |
| size_t num_extension_actions = 0u; |
| for (ToolbarActionView* view : toolbar_action_views_) { |
| // No component action should ever have a valid extension id, so we can |
| // use this to check the extension amount. |
| // TODO(devlin): Fix this to just check model size when the model also |
| // includes component actions. |
| if (crx_file::id_util::IdIsValid(view->view_controller()->GetId())) |
| ++num_extension_actions; |
| } |
| DCHECK_LE(model_visible_size, num_extension_actions); |
| DCHECK_EQ(model_->toolbar_items().size(), num_extension_actions); |
| } |
| #endif |
| |
| // The overflow displays any icons not shown by the main bar. |
| return in_overflow_mode() ? |
| model_->toolbar_items().size() - model_visible_size : model_visible_size; |
| } |