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/location.h"
8 #include "base/memory/scoped_vector.h"
9 #include "base/single_thread_task_runner.h"
10 #include "base/strings/utf_string_conversions.h"
11 #include "base/thread_task_runner_handle.h"
12 #include "chrome/browser/extensions/extension_action.h"
13 #include "chrome/browser/extensions/extension_action_icon_factory.h"
14 #include "chrome/browser/extensions/extension_context_menu_model.h"
15 #include "chrome/browser/profiles/profile.h"
16 #include "chrome/browser/ui/browser.h"
17 #include "chrome/browser/ui/extensions/extension_action_view_controller.h"
18 #include "chrome/browser/ui/views/extensions/browser_action_drag_data.h"
19 #include "chrome/browser/ui/views/toolbar/browser_actions_container.h"
20 #include "chrome/browser/ui/views/toolbar/toolbar_action_view.h"
21 #include "extensions/common/extension.h"
22 #include "ui/views/border.h"
23 #include "ui/views/controls/button/label_button_border.h"
24 #include "ui/views/controls/menu/menu_delegate.h"
25 #include "ui/views/controls/menu/menu_item_view.h"
26 #include "ui/views/controls/menu/menu_runner.h"
27 #include "ui/views/metrics.h"
31 // In the browser actions container's chevron menu, a menu item view's icon
32 // comes from ToolbarActionView::GetIconWithBadge() when the menu item view is
33 // created. But, the browser action's icon may not be loaded in time because it
34 // is read from file system in another thread.
35 // The IconUpdater will update the menu item view's icon when the browser
36 // action's icon has been updated.
37 class IconUpdater
: public ExtensionActionIconFactory::Observer
{
39 IconUpdater(views::MenuItemView
* menu_item_view
,
40 ToolbarActionView
* represented_view
)
41 : menu_item_view_(menu_item_view
),
42 represented_view_(represented_view
) {
43 DCHECK(menu_item_view
);
44 DCHECK(represented_view
);
45 view_controller()->set_icon_observer(this);
47 ~IconUpdater() override
{ view_controller()->set_icon_observer(nullptr); }
49 // ExtensionActionIconFactory::Observer:
50 void OnIconUpdated() override
{
51 menu_item_view_
->SetIcon(
52 represented_view_
->GetImage(views::Button::STATE_NORMAL
));
56 ExtensionActionViewController
* view_controller() {
57 // Since the chevron overflow menu is only used in a world where toolbar
58 // actions are only extensions, this cast is safe.
59 return static_cast<ExtensionActionViewController
*>(
60 represented_view_
->view_controller());
63 // The menu item view whose icon might be updated.
64 views::MenuItemView
* menu_item_view_
;
66 // The view this icon updater is helping represent in the chevron overflow
67 // menu. When its icon changes, this updates the corresponding menu item
69 ToolbarActionView
* represented_view_
;
71 DISALLOW_COPY_AND_ASSIGN(IconUpdater
);
76 // This class handles the overflow menu for browser actions.
77 class ChevronMenuButton::MenuController
: public views::MenuDelegate
{
79 MenuController(ChevronMenuButton
* owner
,
80 BrowserActionsContainer
* browser_actions_container
,
82 ~MenuController() override
;
84 // Shows the overflow menu.
85 void RunMenu(views::Widget
* widget
);
87 // Closes the overflow menu (and its context menu if open as well).
91 // views::MenuDelegate:
92 bool IsCommandEnabled(int id
) const override
;
93 void ExecuteCommand(int id
) override
;
94 bool ShowContextMenu(views::MenuItemView
* source
,
97 ui::MenuSourceType source_type
) override
;
98 void DropMenuClosed(views::MenuItemView
* menu
) override
;
99 // These drag functions offer support for dragging icons into the overflow
102 views::MenuItemView
* menu
,
104 std::set
<ui::OSExchangeData::CustomFormat
>* custom_formats
) override
;
105 bool AreDropTypesRequired(views::MenuItemView
* menu
) override
;
106 bool CanDrop(views::MenuItemView
* menu
,
107 const ui::OSExchangeData
& data
) override
;
108 int GetDropOperation(views::MenuItemView
* item
,
109 const ui::DropTargetEvent
& event
,
110 DropPosition
* position
) override
;
111 int OnPerformDrop(views::MenuItemView
* menu
,
112 DropPosition position
,
113 const ui::DropTargetEvent
& event
) override
;
114 // These three drag functions offer support for dragging icons out of the
116 bool CanDrag(views::MenuItemView
* menu
) override
;
117 void WriteDragData(views::MenuItemView
* sender
,
118 ui::OSExchangeData
* data
) override
;
119 int GetDragOperations(views::MenuItemView
* sender
) override
;
121 // Returns the offset into |views_| for the given |id|.
122 size_t IndexForId(int id
) const;
124 // The owning ChevronMenuButton.
125 ChevronMenuButton
* owner_
;
127 // A pointer to the browser action container.
128 BrowserActionsContainer
* browser_actions_container_
;
130 // The overflow menu for the menu button. Owned by |menu_runner_|.
131 views::MenuItemView
* menu_
;
133 // Resposible for running the menu.
134 scoped_ptr
<views::MenuRunner
> menu_runner_
;
136 // The index into the ToolbarActionView vector, indicating where to start
137 // picking browser actions to draw.
140 // Whether this controller is being used for drop.
143 // The vector keeps all icon updaters associated with menu item views in the
144 // controller. The icon updater will update the menu item view's icon when
145 // the browser action view's icon has been updated.
146 ScopedVector
<IconUpdater
> icon_updaters_
;
148 DISALLOW_COPY_AND_ASSIGN(MenuController
);
151 ChevronMenuButton::MenuController::MenuController(
152 ChevronMenuButton
* owner
,
153 BrowserActionsContainer
* browser_actions_container
,
156 browser_actions_container_(browser_actions_container
),
159 browser_actions_container_
->VisibleBrowserActionsAfterAnimation()),
160 for_drop_(for_drop
) {
161 menu_
= new views::MenuItemView(this);
162 menu_runner_
.reset(new views::MenuRunner(
163 menu_
, for_drop_
? views::MenuRunner::FOR_DROP
: 0));
164 menu_
->set_has_icons(true);
166 size_t command_id
= 1; // Menu id 0 is reserved, start with 1.
167 for (size_t i
= start_index_
;
168 i
< browser_actions_container_
->num_toolbar_actions(); ++i
) {
169 ToolbarActionView
* view
=
170 browser_actions_container_
->GetToolbarActionViewAt(i
);
171 views::MenuItemView
* menu_item
= menu_
->AppendMenuItemWithIcon(
173 view
->view_controller()->GetActionName(),
174 view
->GetImage(views::Button::STATE_NORMAL
));
176 // Set the tooltip for this item.
178 view
->view_controller()->GetTooltip(view
->GetCurrentWebContents()),
181 icon_updaters_
.push_back(new IconUpdater(menu_item
, view
));
187 ChevronMenuButton::MenuController::~MenuController() {
190 void ChevronMenuButton::MenuController::RunMenu(views::Widget
* window
) {
191 gfx::Rect bounds
= owner_
->bounds();
192 gfx::Point screen_loc
;
193 views::View::ConvertPointToScreen(owner_
, &screen_loc
);
194 bounds
.set_x(screen_loc
.x());
195 bounds
.set_y(screen_loc
.y());
197 if (menu_runner_
->RunMenuAt(window
,
200 views::MENU_ANCHOR_TOPRIGHT
,
201 ui::MENU_SOURCE_NONE
) ==
202 views::MenuRunner::MENU_DELETED
)
206 // Give the context menu (if any) a chance to execute the user-selected
208 base::ThreadTaskRunnerHandle::Get()->PostTask(
209 FROM_HERE
, base::Bind(&ChevronMenuButton::MenuDone
,
210 owner_
->weak_factory_
.GetWeakPtr()));
214 void ChevronMenuButton::MenuController::CloseMenu() {
218 bool ChevronMenuButton::MenuController::IsCommandEnabled(int id
) const {
219 ToolbarActionView
* view
=
220 browser_actions_container_
->GetToolbarActionViewAt(start_index_
+ id
- 1);
221 return view
->view_controller()->IsEnabled(view
->GetCurrentWebContents());
224 void ChevronMenuButton::MenuController::ExecuteCommand(int id
) {
225 browser_actions_container_
->GetToolbarActionViewAt(start_index_
+ id
- 1)->
226 view_controller()->ExecuteAction(true);
229 bool ChevronMenuButton::MenuController::ShowContextMenu(
230 views::MenuItemView
* source
,
233 ui::MenuSourceType source_type
) {
234 ToolbarActionView
* view
= browser_actions_container_
->GetToolbarActionViewAt(
235 start_index_
+ id
- 1);
236 ExtensionActionViewController
* view_controller
=
237 static_cast<ExtensionActionViewController
*>(view
->view_controller());
238 if (!view_controller
->extension()->ShowConfigureContextMenus())
241 scoped_ptr
<extensions::ExtensionContextMenuModel
> context_menu_contents(
242 new extensions::ExtensionContextMenuModel(
243 view_controller
->extension(), view_controller
->browser(),
244 extensions::ExtensionContextMenuModel::OVERFLOWED
, view_controller
));
245 views::MenuRunner
context_menu_runner(context_menu_contents
.get(),
246 views::MenuRunner::HAS_MNEMONICS
|
247 views::MenuRunner::IS_NESTED
|
248 views::MenuRunner::CONTEXT_MENU
);
250 // We can ignore the result as we delete ourself.
251 // This blocks until the user chooses something or dismisses the menu.
252 if (context_menu_runner
.RunMenuAt(owner_
->GetWidget(),
254 gfx::Rect(p
, gfx::Size()),
255 views::MENU_ANCHOR_TOPLEFT
,
257 views::MenuRunner::MENU_DELETED
)
260 // The user is done with the context menu, so we can close the underlying
267 void ChevronMenuButton::MenuController::DropMenuClosed(
268 views::MenuItemView
* menu
) {
272 bool ChevronMenuButton::MenuController::GetDropFormats(
273 views::MenuItemView
* menu
,
275 std::set
<OSExchangeData::CustomFormat
>* custom_formats
) {
276 return BrowserActionDragData::GetDropFormats(custom_formats
);
279 bool ChevronMenuButton::MenuController::AreDropTypesRequired(
280 views::MenuItemView
* menu
) {
281 return BrowserActionDragData::AreDropTypesRequired();
284 bool ChevronMenuButton::MenuController::CanDrop(
285 views::MenuItemView
* menu
, const OSExchangeData
& data
) {
286 return BrowserActionDragData::CanDrop(
287 data
, browser_actions_container_
->browser()->profile());
290 int ChevronMenuButton::MenuController::GetDropOperation(
291 views::MenuItemView
* item
,
292 const ui::DropTargetEvent
& event
,
293 DropPosition
* position
) {
294 // Don't allow dropping from the BrowserActionContainer into slot 0 of the
295 // overflow menu since once the move has taken place the item you are dragging
296 // falls right out of the menu again once the user releases the button
297 // (because we don't shrink the BrowserActionContainer when you do this).
298 if ((item
->GetCommand() == 0) && (*position
== DROP_BEFORE
)) {
299 BrowserActionDragData drop_data
;
300 if (!drop_data
.Read(event
.data()))
301 return ui::DragDropTypes::DRAG_NONE
;
303 if (drop_data
.index() < browser_actions_container_
->VisibleBrowserActions())
304 return ui::DragDropTypes::DRAG_NONE
;
307 return ui::DragDropTypes::DRAG_MOVE
;
310 int ChevronMenuButton::MenuController::OnPerformDrop(
311 views::MenuItemView
* menu
,
312 DropPosition position
,
313 const ui::DropTargetEvent
& event
) {
314 BrowserActionDragData drop_data
;
315 if (!drop_data
.Read(event
.data()))
316 return ui::DragDropTypes::DRAG_NONE
;
318 size_t drop_index
= IndexForId(menu
->GetCommand());
320 // When not dragging within the overflow menu (dragging an icon into the menu)
321 // subtract one to get the right index.
322 if (position
== DROP_BEFORE
&&
323 drop_data
.index() < browser_actions_container_
->VisibleBrowserActions())
326 ToolbarActionsBar::DragType drag_type
=
327 drop_data
.index() < browser_actions_container_
->VisibleBrowserActions() ?
328 ToolbarActionsBar::DRAG_TO_OVERFLOW
:
329 ToolbarActionsBar::DRAG_TO_SAME
;
330 browser_actions_container_
->toolbar_actions_bar()->OnDragDrop(
331 drop_data
.index(), drop_index
, drag_type
);
335 return ui::DragDropTypes::DRAG_MOVE
;
338 bool ChevronMenuButton::MenuController::CanDrag(views::MenuItemView
* menu
) {
342 void ChevronMenuButton::MenuController::WriteDragData(
343 views::MenuItemView
* sender
, OSExchangeData
* data
) {
344 size_t drag_index
= IndexForId(sender
->GetCommand());
345 BrowserActionDragData
drag_data(
346 browser_actions_container_
->GetIdAt(drag_index
), drag_index
);
347 drag_data
.Write(browser_actions_container_
->browser()->profile(), data
);
350 int ChevronMenuButton::MenuController::GetDragOperations(
351 views::MenuItemView
* sender
) {
352 return ui::DragDropTypes::DRAG_MOVE
;
355 size_t ChevronMenuButton::MenuController::IndexForId(int id
) const {
356 // The index of the view being dragged (GetCommand gives a 1-based index into
357 // the overflow menu).
358 DCHECK_GT(browser_actions_container_
->VisibleBrowserActions() + id
, 0u);
359 return browser_actions_container_
->VisibleBrowserActions() + id
- 1;
362 ChevronMenuButton::ChevronMenuButton(
363 BrowserActionsContainer
* browser_actions_container
)
364 : views::MenuButton(NULL
, base::string16(), this, false),
365 browser_actions_container_(browser_actions_container
),
366 weak_factory_(this) {
369 ChevronMenuButton::~ChevronMenuButton() {
372 void ChevronMenuButton::CloseMenu() {
373 if (menu_controller_
)
374 menu_controller_
->CloseMenu();
377 scoped_ptr
<views::LabelButtonBorder
> ChevronMenuButton::CreateDefaultBorder()
379 // The chevron resource was designed to not have any insets.
380 scoped_ptr
<views::LabelButtonBorder
> border
=
381 views::MenuButton::CreateDefaultBorder();
382 border
->set_insets(gfx::Insets());
383 return border
.Pass();
386 bool ChevronMenuButton::GetDropFormats(
388 std::set
<OSExchangeData::CustomFormat
>* custom_formats
) {
389 return BrowserActionDragData::GetDropFormats(custom_formats
);
392 bool ChevronMenuButton::AreDropTypesRequired() {
393 return BrowserActionDragData::AreDropTypesRequired();
396 bool ChevronMenuButton::CanDrop(const OSExchangeData
& data
) {
397 return BrowserActionDragData::CanDrop(
398 data
, browser_actions_container_
->browser()->profile());
401 void ChevronMenuButton::OnDragEntered(const ui::DropTargetEvent
& event
) {
402 DCHECK(!weak_factory_
.HasWeakPtrs());
403 if (!menu_controller_
) {
404 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
405 FROM_HERE
, base::Bind(&ChevronMenuButton::ShowOverflowMenu
,
406 weak_factory_
.GetWeakPtr(), true),
407 base::TimeDelta::FromMilliseconds(views::GetMenuShowDelay()));
411 int ChevronMenuButton::OnDragUpdated(const ui::DropTargetEvent
& event
) {
412 return ui::DragDropTypes::DRAG_MOVE
;
415 void ChevronMenuButton::OnDragExited() {
416 weak_factory_
.InvalidateWeakPtrs();
419 int ChevronMenuButton::OnPerformDrop(const ui::DropTargetEvent
& event
) {
420 weak_factory_
.InvalidateWeakPtrs();
421 return ui::DragDropTypes::DRAG_MOVE
;
424 void ChevronMenuButton::OnMenuButtonClicked(views::View
* source
,
425 const gfx::Point
& point
) {
426 DCHECK_EQ(this, source
);
427 // The menu could already be open if a user dragged an item over it but
428 // ultimately dropped elsewhere (as in that case the menu will close on a
429 // timer). In this case, the click should close the open menu.
430 if (menu_controller_
)
431 menu_controller_
->CloseMenu();
433 ShowOverflowMenu(false);
436 void ChevronMenuButton::ShowOverflowMenu(bool for_drop
) {
437 // We should never try to show an overflow menu when one is already visible.
438 DCHECK(!menu_controller_
);
439 menu_controller_
.reset(new MenuController(
440 this, browser_actions_container_
, for_drop
));
441 menu_controller_
->RunMenu(GetWidget());
444 void ChevronMenuButton::MenuDone() {
445 menu_controller_
.reset();