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_refptr
<ExtensionContextMenuModel
> context_menu_contents
=
242 new ExtensionContextMenuModel(view_controller
->extension(),
243 view_controller
->browser(),
244 ExtensionContextMenuModel::OVERFLOWED
,
246 views::MenuRunner
context_menu_runner(context_menu_contents
.get(),
247 views::MenuRunner::HAS_MNEMONICS
|
248 views::MenuRunner::IS_NESTED
|
249 views::MenuRunner::CONTEXT_MENU
);
251 // We can ignore the result as we delete ourself.
252 // This blocks until the user chooses something or dismisses the menu.
253 if (context_menu_runner
.RunMenuAt(owner_
->GetWidget(),
255 gfx::Rect(p
, gfx::Size()),
256 views::MENU_ANCHOR_TOPLEFT
,
258 views::MenuRunner::MENU_DELETED
)
261 // The user is done with the context menu, so we can close the underlying
268 void ChevronMenuButton::MenuController::DropMenuClosed(
269 views::MenuItemView
* menu
) {
273 bool ChevronMenuButton::MenuController::GetDropFormats(
274 views::MenuItemView
* menu
,
276 std::set
<OSExchangeData::CustomFormat
>* custom_formats
) {
277 return BrowserActionDragData::GetDropFormats(custom_formats
);
280 bool ChevronMenuButton::MenuController::AreDropTypesRequired(
281 views::MenuItemView
* menu
) {
282 return BrowserActionDragData::AreDropTypesRequired();
285 bool ChevronMenuButton::MenuController::CanDrop(
286 views::MenuItemView
* menu
, const OSExchangeData
& data
) {
287 return BrowserActionDragData::CanDrop(
288 data
, browser_actions_container_
->browser()->profile());
291 int ChevronMenuButton::MenuController::GetDropOperation(
292 views::MenuItemView
* item
,
293 const ui::DropTargetEvent
& event
,
294 DropPosition
* position
) {
295 // Don't allow dropping from the BrowserActionContainer into slot 0 of the
296 // overflow menu since once the move has taken place the item you are dragging
297 // falls right out of the menu again once the user releases the button
298 // (because we don't shrink the BrowserActionContainer when you do this).
299 if ((item
->GetCommand() == 0) && (*position
== DROP_BEFORE
)) {
300 BrowserActionDragData drop_data
;
301 if (!drop_data
.Read(event
.data()))
302 return ui::DragDropTypes::DRAG_NONE
;
304 if (drop_data
.index() < browser_actions_container_
->VisibleBrowserActions())
305 return ui::DragDropTypes::DRAG_NONE
;
308 return ui::DragDropTypes::DRAG_MOVE
;
311 int ChevronMenuButton::MenuController::OnPerformDrop(
312 views::MenuItemView
* menu
,
313 DropPosition position
,
314 const ui::DropTargetEvent
& event
) {
315 BrowserActionDragData drop_data
;
316 if (!drop_data
.Read(event
.data()))
317 return ui::DragDropTypes::DRAG_NONE
;
319 size_t drop_index
= IndexForId(menu
->GetCommand());
321 // When not dragging within the overflow menu (dragging an icon into the menu)
322 // subtract one to get the right index.
323 if (position
== DROP_BEFORE
&&
324 drop_data
.index() < browser_actions_container_
->VisibleBrowserActions())
327 ToolbarActionsBar::DragType drag_type
=
328 drop_data
.index() < browser_actions_container_
->VisibleBrowserActions() ?
329 ToolbarActionsBar::DRAG_TO_OVERFLOW
:
330 ToolbarActionsBar::DRAG_TO_SAME
;
331 browser_actions_container_
->toolbar_actions_bar()->OnDragDrop(
332 drop_data
.index(), drop_index
, drag_type
);
336 return ui::DragDropTypes::DRAG_MOVE
;
339 bool ChevronMenuButton::MenuController::CanDrag(views::MenuItemView
* menu
) {
343 void ChevronMenuButton::MenuController::WriteDragData(
344 views::MenuItemView
* sender
, OSExchangeData
* data
) {
345 size_t drag_index
= IndexForId(sender
->GetCommand());
346 BrowserActionDragData
drag_data(
347 browser_actions_container_
->GetIdAt(drag_index
), drag_index
);
348 drag_data
.Write(browser_actions_container_
->browser()->profile(), data
);
351 int ChevronMenuButton::MenuController::GetDragOperations(
352 views::MenuItemView
* sender
) {
353 return ui::DragDropTypes::DRAG_MOVE
;
356 size_t ChevronMenuButton::MenuController::IndexForId(int id
) const {
357 // The index of the view being dragged (GetCommand gives a 1-based index into
358 // the overflow menu).
359 DCHECK_GT(browser_actions_container_
->VisibleBrowserActions() + id
, 0u);
360 return browser_actions_container_
->VisibleBrowserActions() + id
- 1;
363 ChevronMenuButton::ChevronMenuButton(
364 BrowserActionsContainer
* browser_actions_container
)
365 : views::MenuButton(NULL
, base::string16(), this, false),
366 browser_actions_container_(browser_actions_container
),
367 weak_factory_(this) {
370 ChevronMenuButton::~ChevronMenuButton() {
373 void ChevronMenuButton::CloseMenu() {
374 if (menu_controller_
)
375 menu_controller_
->CloseMenu();
378 scoped_ptr
<views::LabelButtonBorder
> ChevronMenuButton::CreateDefaultBorder()
380 // The chevron resource was designed to not have any insets.
381 scoped_ptr
<views::LabelButtonBorder
> border
=
382 views::MenuButton::CreateDefaultBorder();
383 border
->set_insets(gfx::Insets());
384 return border
.Pass();
387 bool ChevronMenuButton::GetDropFormats(
389 std::set
<OSExchangeData::CustomFormat
>* custom_formats
) {
390 return BrowserActionDragData::GetDropFormats(custom_formats
);
393 bool ChevronMenuButton::AreDropTypesRequired() {
394 return BrowserActionDragData::AreDropTypesRequired();
397 bool ChevronMenuButton::CanDrop(const OSExchangeData
& data
) {
398 return BrowserActionDragData::CanDrop(
399 data
, browser_actions_container_
->browser()->profile());
402 void ChevronMenuButton::OnDragEntered(const ui::DropTargetEvent
& event
) {
403 DCHECK(!weak_factory_
.HasWeakPtrs());
404 if (!menu_controller_
) {
405 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
406 FROM_HERE
, base::Bind(&ChevronMenuButton::ShowOverflowMenu
,
407 weak_factory_
.GetWeakPtr(), true),
408 base::TimeDelta::FromMilliseconds(views::GetMenuShowDelay()));
412 int ChevronMenuButton::OnDragUpdated(const ui::DropTargetEvent
& event
) {
413 return ui::DragDropTypes::DRAG_MOVE
;
416 void ChevronMenuButton::OnDragExited() {
417 weak_factory_
.InvalidateWeakPtrs();
420 int ChevronMenuButton::OnPerformDrop(const ui::DropTargetEvent
& event
) {
421 weak_factory_
.InvalidateWeakPtrs();
422 return ui::DragDropTypes::DRAG_MOVE
;
425 void ChevronMenuButton::OnMenuButtonClicked(views::View
* source
,
426 const gfx::Point
& point
) {
427 DCHECK_EQ(this, source
);
428 // The menu could already be open if a user dragged an item over it but
429 // ultimately dropped elsewhere (as in that case the menu will close on a
430 // timer). In this case, the click should close the open menu.
431 if (menu_controller_
)
432 menu_controller_
->CloseMenu();
434 ShowOverflowMenu(false);
437 void ChevronMenuButton::ShowOverflowMenu(bool for_drop
) {
438 // We should never try to show an overflow menu when one is already visible.
439 DCHECK(!menu_controller_
);
440 menu_controller_
.reset(new MenuController(
441 this, browser_actions_container_
, for_drop
));
442 menu_controller_
->RunMenu(GetWidget());
445 void ChevronMenuButton::MenuDone() {
446 menu_controller_
.reset();