1 // Copyright 2014 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.
5 #include "chrome/browser/ui/views/toolbar/chevron_menu_button.h"
7 #include "base/memory/scoped_vector.h"
8 #include "base/message_loop/message_loop.h"
9 #include "base/strings/utf_string_conversions.h"
10 #include "chrome/browser/extensions/extension_action.h"
11 #include "chrome/browser/extensions/extension_action_icon_factory.h"
12 #include "chrome/browser/extensions/extension_context_menu_model.h"
13 #include "chrome/browser/extensions/extension_toolbar_model.h"
14 #include "chrome/browser/profiles/profile.h"
15 #include "chrome/browser/ui/browser.h"
16 #include "chrome/browser/ui/extensions/extension_action_view_controller.h"
17 #include "chrome/browser/ui/views/extensions/browser_action_drag_data.h"
18 #include "chrome/browser/ui/views/toolbar/browser_actions_container.h"
19 #include "chrome/browser/ui/views/toolbar/toolbar_action_view.h"
20 #include "extensions/common/extension.h"
21 #include "ui/views/border.h"
22 #include "ui/views/controls/button/label_button_border.h"
23 #include "ui/views/controls/menu/menu_delegate.h"
24 #include "ui/views/controls/menu/menu_item_view.h"
25 #include "ui/views/controls/menu/menu_runner.h"
26 #include "ui/views/metrics.h"
30 // In the browser actions container's chevron menu, a menu item view's icon
31 // comes from ToolbarActionView::GetIconWithBadge() when the menu item view is
32 // created. But, the browser action's icon may not be loaded in time because it
33 // is read from file system in another thread.
34 // The IconUpdater will update the menu item view's icon when the browser
35 // action's icon has been updated.
36 class IconUpdater : public ExtensionActionIconFactory::Observer {
38 IconUpdater(views::MenuItemView* menu_item_view,
39 ExtensionActionViewController* view_controller)
40 : menu_item_view_(menu_item_view),
41 view_controller_(view_controller) {
42 DCHECK(menu_item_view);
43 DCHECK(view_controller);
44 view_controller->set_icon_observer(this);
46 ~IconUpdater() override { view_controller_->set_icon_observer(nullptr); }
48 // ExtensionActionIconFactory::Observer:
49 void OnIconUpdated() override {
50 menu_item_view_->SetIcon(view_controller_->GetIconWithBadge());
54 // The menu item view whose icon might be updated.
55 views::MenuItemView* menu_item_view_;
57 // The view controller to be observed. When its icon changes, update the
58 // corresponding menu item view's icon.
59 ExtensionActionViewController* view_controller_;
61 DISALLOW_COPY_AND_ASSIGN(IconUpdater);
66 // This class handles the overflow menu for browser actions.
67 class ChevronMenuButton::MenuController : public views::MenuDelegate {
69 MenuController(ChevronMenuButton* owner,
70 BrowserActionsContainer* browser_actions_container,
72 ~MenuController() override;
74 // Shows the overflow menu.
75 void RunMenu(views::Widget* widget);
77 // Closes the overflow menu (and its context menu if open as well).
81 // views::MenuDelegate:
82 bool IsCommandEnabled(int id) const override;
83 void ExecuteCommand(int id) override;
84 bool ShowContextMenu(views::MenuItemView* source,
87 ui::MenuSourceType source_type) override;
88 void DropMenuClosed(views::MenuItemView* menu) override;
89 // These drag functions offer support for dragging icons into the overflow
92 views::MenuItemView* menu,
94 std::set<ui::OSExchangeData::CustomFormat>* custom_formats) override;
95 bool AreDropTypesRequired(views::MenuItemView* menu) override;
96 bool CanDrop(views::MenuItemView* menu,
97 const ui::OSExchangeData& data) override;
98 int GetDropOperation(views::MenuItemView* item,
99 const ui::DropTargetEvent& event,
100 DropPosition* position) override;
101 int OnPerformDrop(views::MenuItemView* menu,
102 DropPosition position,
103 const ui::DropTargetEvent& event) override;
104 // These three drag functions offer support for dragging icons out of the
106 bool CanDrag(views::MenuItemView* menu) override;
107 void WriteDragData(views::MenuItemView* sender,
108 ui::OSExchangeData* data) override;
109 int GetDragOperations(views::MenuItemView* sender) override;
111 // Returns the offset into |views_| for the given |id|.
112 size_t IndexForId(int id) const;
114 // The owning ChevronMenuButton.
115 ChevronMenuButton* owner_;
117 // A pointer to the browser action container.
118 BrowserActionsContainer* browser_actions_container_;
120 // The overflow menu for the menu button. Owned by |menu_runner_|.
121 views::MenuItemView* menu_;
123 // Resposible for running the menu.
124 scoped_ptr<views::MenuRunner> menu_runner_;
126 // The index into the ToolbarActionView vector, indicating where to start
127 // picking browser actions to draw.
130 // Whether this controller is being used for drop.
133 // The vector keeps all icon updaters associated with menu item views in the
134 // controller. The icon updater will update the menu item view's icon when
135 // the browser action view's icon has been updated.
136 ScopedVector<IconUpdater> icon_updaters_;
138 DISALLOW_COPY_AND_ASSIGN(MenuController);
141 ChevronMenuButton::MenuController::MenuController(
142 ChevronMenuButton* owner,
143 BrowserActionsContainer* browser_actions_container,
146 browser_actions_container_(browser_actions_container),
149 browser_actions_container_->VisibleBrowserActionsAfterAnimation()),
150 for_drop_(for_drop) {
151 menu_ = new views::MenuItemView(this);
152 menu_runner_.reset(new views::MenuRunner(
153 menu_, for_drop_ ? views::MenuRunner::FOR_DROP : 0));
154 menu_->set_has_icons(true);
156 size_t command_id = 1; // Menu id 0 is reserved, start with 1.
157 for (size_t i = start_index_;
158 i < browser_actions_container_->num_toolbar_actions(); ++i) {
159 ToolbarActionView* view =
160 browser_actions_container_->GetToolbarActionViewAt(i);
161 views::MenuItemView* menu_item = menu_->AppendMenuItemWithIcon(
163 view->view_controller()->GetActionName(),
164 view->view_controller()->GetIconWithBadge());
166 // Set the tooltip for this item.
168 view->view_controller()->GetTooltip(view->GetCurrentWebContents()),
171 icon_updaters_.push_back(new IconUpdater(
173 static_cast<ExtensionActionViewController*>(view->view_controller())));
179 ChevronMenuButton::MenuController::~MenuController() {
182 void ChevronMenuButton::MenuController::RunMenu(views::Widget* window) {
183 gfx::Rect bounds = owner_->bounds();
184 gfx::Point screen_loc;
185 views::View::ConvertPointToScreen(owner_, &screen_loc);
186 bounds.set_x(screen_loc.x());
187 bounds.set_y(screen_loc.y());
189 if (menu_runner_->RunMenuAt(window,
192 views::MENU_ANCHOR_TOPRIGHT,
193 ui::MENU_SOURCE_NONE) ==
194 views::MenuRunner::MENU_DELETED)
198 // Give the context menu (if any) a chance to execute the user-selected
200 base::MessageLoop::current()->PostTask(
202 base::Bind(&ChevronMenuButton::MenuDone,
203 owner_->weak_factory_.GetWeakPtr()));
207 void ChevronMenuButton::MenuController::CloseMenu() {
211 bool ChevronMenuButton::MenuController::IsCommandEnabled(int id) const {
212 ToolbarActionView* view =
213 browser_actions_container_->GetToolbarActionViewAt(start_index_ + id - 1);
214 return view->view_controller()->IsEnabled(view->GetCurrentWebContents());
217 void ChevronMenuButton::MenuController::ExecuteCommand(int id) {
218 browser_actions_container_->GetToolbarActionViewAt(start_index_ + id - 1)->
219 view_controller()->ExecuteAction(true);
222 bool ChevronMenuButton::MenuController::ShowContextMenu(
223 views::MenuItemView* source,
226 ui::MenuSourceType source_type) {
227 ToolbarActionView* view = browser_actions_container_->GetToolbarActionViewAt(
228 start_index_ + id - 1);
229 ExtensionActionViewController* view_controller =
230 static_cast<ExtensionActionViewController*>(view->view_controller());
231 if (!view_controller->extension()->ShowConfigureContextMenus())
234 scoped_refptr<ExtensionContextMenuModel> context_menu_contents =
235 new ExtensionContextMenuModel(view_controller->extension(),
238 views::MenuRunner context_menu_runner(context_menu_contents.get(),
239 views::MenuRunner::HAS_MNEMONICS |
240 views::MenuRunner::IS_NESTED |
241 views::MenuRunner::CONTEXT_MENU);
243 // We can ignore the result as we delete ourself.
244 // This blocks until the user chooses something or dismisses the menu.
245 if (context_menu_runner.RunMenuAt(owner_->GetWidget(),
247 gfx::Rect(p, gfx::Size()),
248 views::MENU_ANCHOR_TOPLEFT,
250 views::MenuRunner::MENU_DELETED)
253 // The user is done with the context menu, so we can close the underlying
260 void ChevronMenuButton::MenuController::DropMenuClosed(
261 views::MenuItemView* menu) {
265 bool ChevronMenuButton::MenuController::GetDropFormats(
266 views::MenuItemView* menu,
268 std::set<OSExchangeData::CustomFormat>* custom_formats) {
269 return BrowserActionDragData::GetDropFormats(custom_formats);
272 bool ChevronMenuButton::MenuController::AreDropTypesRequired(
273 views::MenuItemView* menu) {
274 return BrowserActionDragData::AreDropTypesRequired();
277 bool ChevronMenuButton::MenuController::CanDrop(
278 views::MenuItemView* menu, const OSExchangeData& data) {
279 return BrowserActionDragData::CanDrop(data,
280 browser_actions_container_->profile());
283 int ChevronMenuButton::MenuController::GetDropOperation(
284 views::MenuItemView* item,
285 const ui::DropTargetEvent& event,
286 DropPosition* position) {
287 // Don't allow dropping from the BrowserActionContainer into slot 0 of the
288 // overflow menu since once the move has taken place the item you are dragging
289 // falls right out of the menu again once the user releases the button
290 // (because we don't shrink the BrowserActionContainer when you do this).
291 if ((item->GetCommand() == 0) && (*position == DROP_BEFORE)) {
292 BrowserActionDragData drop_data;
293 if (!drop_data.Read(event.data()))
294 return ui::DragDropTypes::DRAG_NONE;
296 if (drop_data.index() < browser_actions_container_->VisibleBrowserActions())
297 return ui::DragDropTypes::DRAG_NONE;
300 return ui::DragDropTypes::DRAG_MOVE;
303 int ChevronMenuButton::MenuController::OnPerformDrop(
304 views::MenuItemView* menu,
305 DropPosition position,
306 const ui::DropTargetEvent& event) {
307 BrowserActionDragData drop_data;
308 if (!drop_data.Read(event.data()))
309 return ui::DragDropTypes::DRAG_NONE;
311 size_t drop_index = IndexForId(menu->GetCommand());
313 // When not dragging within the overflow menu (dragging an icon into the menu)
314 // subtract one to get the right index.
315 if (position == DROP_BEFORE &&
316 drop_data.index() < browser_actions_container_->VisibleBrowserActions())
319 Profile* profile = browser_actions_container_->profile();
320 // Move the extension in the model.
321 extensions::ExtensionToolbarModel* toolbar_model =
322 extensions::ExtensionToolbarModel::Get(profile);
323 toolbar_model->MoveExtensionIcon(drop_data.id(), drop_index);
325 // If the extension was moved to the overflow menu from the main bar, notify
327 if (drop_data.index() < browser_actions_container_->VisibleBrowserActions())
328 browser_actions_container_->NotifyActionMovedToOverflow();
332 return ui::DragDropTypes::DRAG_MOVE;
335 bool ChevronMenuButton::MenuController::CanDrag(views::MenuItemView* menu) {
339 void ChevronMenuButton::MenuController::WriteDragData(
340 views::MenuItemView* sender, OSExchangeData* data) {
341 size_t drag_index = IndexForId(sender->GetCommand());
342 BrowserActionDragData drag_data(
343 browser_actions_container_->GetIdAt(drag_index), drag_index);
344 drag_data.Write(browser_actions_container_->profile(), data);
347 int ChevronMenuButton::MenuController::GetDragOperations(
348 views::MenuItemView* sender) {
349 return ui::DragDropTypes::DRAG_MOVE;
352 size_t ChevronMenuButton::MenuController::IndexForId(int id) const {
353 // The index of the view being dragged (GetCommand gives a 1-based index into
354 // the overflow menu).
355 DCHECK_GT(browser_actions_container_->VisibleBrowserActions() + id, 0u);
356 return browser_actions_container_->VisibleBrowserActions() + id - 1;
359 ChevronMenuButton::ChevronMenuButton(
360 BrowserActionsContainer* browser_actions_container)
361 : views::MenuButton(NULL, base::string16(), this, false),
362 browser_actions_container_(browser_actions_container),
363 weak_factory_(this) {
366 ChevronMenuButton::~ChevronMenuButton() {
369 void ChevronMenuButton::CloseMenu() {
370 if (menu_controller_)
371 menu_controller_->CloseMenu();
374 scoped_ptr<views::LabelButtonBorder> ChevronMenuButton::CreateDefaultBorder()
376 // The chevron resource was designed to not have any insets.
377 scoped_ptr<views::LabelButtonBorder> border =
378 views::MenuButton::CreateDefaultBorder();
379 border->set_insets(gfx::Insets());
380 return border.Pass();
383 bool ChevronMenuButton::GetDropFormats(
385 std::set<OSExchangeData::CustomFormat>* custom_formats) {
386 return BrowserActionDragData::GetDropFormats(custom_formats);
389 bool ChevronMenuButton::AreDropTypesRequired() {
390 return BrowserActionDragData::AreDropTypesRequired();
393 bool ChevronMenuButton::CanDrop(const OSExchangeData& data) {
394 return BrowserActionDragData::CanDrop(
395 data, browser_actions_container_->profile());
398 void ChevronMenuButton::OnDragEntered(const ui::DropTargetEvent& event) {
399 DCHECK(!weak_factory_.HasWeakPtrs());
400 if (!menu_controller_) {
401 base::MessageLoop::current()->PostDelayedTask(
403 base::Bind(&ChevronMenuButton::ShowOverflowMenu,
404 weak_factory_.GetWeakPtr(),
406 base::TimeDelta::FromMilliseconds(views::GetMenuShowDelay()));
410 int ChevronMenuButton::OnDragUpdated(const ui::DropTargetEvent& event) {
411 return ui::DragDropTypes::DRAG_MOVE;
414 void ChevronMenuButton::OnDragExited() {
415 weak_factory_.InvalidateWeakPtrs();
418 int ChevronMenuButton::OnPerformDrop(const ui::DropTargetEvent& event) {
419 weak_factory_.InvalidateWeakPtrs();
420 return ui::DragDropTypes::DRAG_MOVE;
423 void ChevronMenuButton::OnMenuButtonClicked(views::View* source,
424 const gfx::Point& point) {
425 DCHECK_EQ(this, source);
426 // The menu could already be open if a user dragged an item over it but
427 // ultimately dropped elsewhere (as in that case the menu will close on a
428 // timer). In this case, the click should close the open menu.
429 if (menu_controller_)
430 menu_controller_->CloseMenu();
432 ShowOverflowMenu(false);
435 void ChevronMenuButton::ShowOverflowMenu(bool for_drop) {
436 // We should never try to show an overflow menu when one is already visible.
437 DCHECK(!menu_controller_);
438 menu_controller_.reset(new MenuController(
439 this, browser_actions_container_, for_drop));
440 menu_controller_->RunMenu(GetWidget());
443 void ChevronMenuButton::MenuDone() {
444 menu_controller_.reset();