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/extensions/extension_toolbar_model.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/ui/browser.h"
18 #include "chrome/browser/ui/extensions/extension_action_view_controller.h"
19 #include "chrome/browser/ui/views/extensions/browser_action_drag_data.h"
20 #include "chrome/browser/ui/views/toolbar/browser_actions_container.h"
21 #include "chrome/browser/ui/views/toolbar/toolbar_action_view.h"
22 #include "extensions/common/extension.h"
23 #include "ui/views/border.h"
24 #include "ui/views/controls/button/label_button_border.h"
25 #include "ui/views/controls/menu/menu_delegate.h"
26 #include "ui/views/controls/menu/menu_item_view.h"
27 #include "ui/views/controls/menu/menu_runner.h"
28 #include "ui/views/metrics.h"
32 // In the browser actions container's chevron menu, a menu item view's icon
33 // comes from ToolbarActionView::GetIconWithBadge() when the menu item view is
34 // created. But, the browser action's icon may not be loaded in time because it
35 // is read from file system in another thread.
36 // The IconUpdater will update the menu item view's icon when the browser
37 // action's icon has been updated.
38 class IconUpdater
: public ExtensionActionIconFactory::Observer
{
40 IconUpdater(views::MenuItemView
* menu_item_view
,
41 ToolbarActionView
* represented_view
)
42 : menu_item_view_(menu_item_view
),
43 represented_view_(represented_view
) {
44 DCHECK(menu_item_view
);
45 DCHECK(represented_view
);
46 view_controller()->set_icon_observer(this);
48 ~IconUpdater() override
{ view_controller()->set_icon_observer(nullptr); }
50 // ExtensionActionIconFactory::Observer:
51 void OnIconUpdated() override
{
52 menu_item_view_
->SetIcon(
53 represented_view_
->GetImage(views::Button::STATE_NORMAL
));
57 ExtensionActionViewController
* view_controller() {
58 // Since the chevron overflow menu is only used in a world where toolbar
59 // actions are only extensions, this cast is safe.
60 return static_cast<ExtensionActionViewController
*>(
61 represented_view_
->view_controller());
64 // The menu item view whose icon might be updated.
65 views::MenuItemView
* menu_item_view_
;
67 // The view this icon updater is helping represent in the chevron overflow
68 // menu. When its icon changes, this updates the corresponding menu item
70 ToolbarActionView
* represented_view_
;
72 DISALLOW_COPY_AND_ASSIGN(IconUpdater
);
77 // This class handles the overflow menu for browser actions.
78 class ChevronMenuButton::MenuController
: public views::MenuDelegate
{
80 MenuController(ChevronMenuButton
* owner
,
81 BrowserActionsContainer
* browser_actions_container
,
83 ~MenuController() override
;
85 // Shows the overflow menu.
86 void RunMenu(views::Widget
* widget
);
88 // Closes the overflow menu (and its context menu if open as well).
92 // views::MenuDelegate:
93 bool IsCommandEnabled(int id
) const override
;
94 void ExecuteCommand(int id
) override
;
95 bool ShowContextMenu(views::MenuItemView
* source
,
98 ui::MenuSourceType source_type
) override
;
99 void DropMenuClosed(views::MenuItemView
* menu
) override
;
100 // These drag functions offer support for dragging icons into the overflow
103 views::MenuItemView
* menu
,
105 std::set
<ui::OSExchangeData::CustomFormat
>* custom_formats
) override
;
106 bool AreDropTypesRequired(views::MenuItemView
* menu
) override
;
107 bool CanDrop(views::MenuItemView
* menu
,
108 const ui::OSExchangeData
& data
) override
;
109 int GetDropOperation(views::MenuItemView
* item
,
110 const ui::DropTargetEvent
& event
,
111 DropPosition
* position
) override
;
112 int OnPerformDrop(views::MenuItemView
* menu
,
113 DropPosition position
,
114 const ui::DropTargetEvent
& event
) override
;
115 // These three drag functions offer support for dragging icons out of the
117 bool CanDrag(views::MenuItemView
* menu
) override
;
118 void WriteDragData(views::MenuItemView
* sender
,
119 ui::OSExchangeData
* data
) override
;
120 int GetDragOperations(views::MenuItemView
* sender
) override
;
122 // Returns the offset into |views_| for the given |id|.
123 size_t IndexForId(int id
) const;
125 // The owning ChevronMenuButton.
126 ChevronMenuButton
* owner_
;
128 // A pointer to the browser action container.
129 BrowserActionsContainer
* browser_actions_container_
;
131 // The overflow menu for the menu button. Owned by |menu_runner_|.
132 views::MenuItemView
* menu_
;
134 // Resposible for running the menu.
135 scoped_ptr
<views::MenuRunner
> menu_runner_
;
137 // The index into the ToolbarActionView vector, indicating where to start
138 // picking browser actions to draw.
141 // Whether this controller is being used for drop.
144 // The vector keeps all icon updaters associated with menu item views in the
145 // controller. The icon updater will update the menu item view's icon when
146 // the browser action view's icon has been updated.
147 ScopedVector
<IconUpdater
> icon_updaters_
;
149 DISALLOW_COPY_AND_ASSIGN(MenuController
);
152 ChevronMenuButton::MenuController::MenuController(
153 ChevronMenuButton
* owner
,
154 BrowserActionsContainer
* browser_actions_container
,
157 browser_actions_container_(browser_actions_container
),
160 browser_actions_container_
->VisibleBrowserActionsAfterAnimation()),
161 for_drop_(for_drop
) {
162 menu_
= new views::MenuItemView(this);
163 menu_runner_
.reset(new views::MenuRunner(
164 menu_
, for_drop_
? views::MenuRunner::FOR_DROP
: 0));
165 menu_
->set_has_icons(true);
167 size_t command_id
= 1; // Menu id 0 is reserved, start with 1.
168 for (size_t i
= start_index_
;
169 i
< browser_actions_container_
->num_toolbar_actions(); ++i
) {
170 ToolbarActionView
* view
=
171 browser_actions_container_
->GetToolbarActionViewAt(i
);
172 views::MenuItemView
* menu_item
= menu_
->AppendMenuItemWithIcon(
174 view
->view_controller()->GetActionName(),
175 view
->GetImage(views::Button::STATE_NORMAL
));
177 // Set the tooltip for this item.
179 view
->view_controller()->GetTooltip(view
->GetCurrentWebContents()),
182 icon_updaters_
.push_back(new IconUpdater(menu_item
, view
));
188 ChevronMenuButton::MenuController::~MenuController() {
191 void ChevronMenuButton::MenuController::RunMenu(views::Widget
* window
) {
192 gfx::Rect bounds
= owner_
->bounds();
193 gfx::Point screen_loc
;
194 views::View::ConvertPointToScreen(owner_
, &screen_loc
);
195 bounds
.set_x(screen_loc
.x());
196 bounds
.set_y(screen_loc
.y());
198 if (menu_runner_
->RunMenuAt(window
,
201 views::MENU_ANCHOR_TOPRIGHT
,
202 ui::MENU_SOURCE_NONE
) ==
203 views::MenuRunner::MENU_DELETED
)
207 // Give the context menu (if any) a chance to execute the user-selected
209 base::ThreadTaskRunnerHandle::Get()->PostTask(
210 FROM_HERE
, base::Bind(&ChevronMenuButton::MenuDone
,
211 owner_
->weak_factory_
.GetWeakPtr()));
215 void ChevronMenuButton::MenuController::CloseMenu() {
219 bool ChevronMenuButton::MenuController::IsCommandEnabled(int id
) const {
220 ToolbarActionView
* view
=
221 browser_actions_container_
->GetToolbarActionViewAt(start_index_
+ id
- 1);
222 return view
->view_controller()->IsEnabled(view
->GetCurrentWebContents());
225 void ChevronMenuButton::MenuController::ExecuteCommand(int id
) {
226 browser_actions_container_
->GetToolbarActionViewAt(start_index_
+ id
- 1)->
227 view_controller()->ExecuteAction(true);
230 bool ChevronMenuButton::MenuController::ShowContextMenu(
231 views::MenuItemView
* source
,
234 ui::MenuSourceType source_type
) {
235 ToolbarActionView
* view
= browser_actions_container_
->GetToolbarActionViewAt(
236 start_index_
+ id
- 1);
237 ExtensionActionViewController
* view_controller
=
238 static_cast<ExtensionActionViewController
*>(view
->view_controller());
239 if (!view_controller
->extension()->ShowConfigureContextMenus())
242 scoped_refptr
<ExtensionContextMenuModel
> context_menu_contents
=
243 new ExtensionContextMenuModel(view_controller
->extension(),
244 view_controller
->browser(),
245 ExtensionContextMenuModel::OVERFLOWED
,
247 views::MenuRunner
context_menu_runner(context_menu_contents
.get(),
248 views::MenuRunner::HAS_MNEMONICS
|
249 views::MenuRunner::IS_NESTED
|
250 views::MenuRunner::CONTEXT_MENU
);
252 // We can ignore the result as we delete ourself.
253 // This blocks until the user chooses something or dismisses the menu.
254 if (context_menu_runner
.RunMenuAt(owner_
->GetWidget(),
256 gfx::Rect(p
, gfx::Size()),
257 views::MENU_ANCHOR_TOPLEFT
,
259 views::MenuRunner::MENU_DELETED
)
262 // The user is done with the context menu, so we can close the underlying
269 void ChevronMenuButton::MenuController::DropMenuClosed(
270 views::MenuItemView
* menu
) {
274 bool ChevronMenuButton::MenuController::GetDropFormats(
275 views::MenuItemView
* menu
,
277 std::set
<OSExchangeData::CustomFormat
>* custom_formats
) {
278 return BrowserActionDragData::GetDropFormats(custom_formats
);
281 bool ChevronMenuButton::MenuController::AreDropTypesRequired(
282 views::MenuItemView
* menu
) {
283 return BrowserActionDragData::AreDropTypesRequired();
286 bool ChevronMenuButton::MenuController::CanDrop(
287 views::MenuItemView
* menu
, const OSExchangeData
& data
) {
288 return BrowserActionDragData::CanDrop(
289 data
, browser_actions_container_
->browser()->profile());
292 int ChevronMenuButton::MenuController::GetDropOperation(
293 views::MenuItemView
* item
,
294 const ui::DropTargetEvent
& event
,
295 DropPosition
* position
) {
296 // Don't allow dropping from the BrowserActionContainer into slot 0 of the
297 // overflow menu since once the move has taken place the item you are dragging
298 // falls right out of the menu again once the user releases the button
299 // (because we don't shrink the BrowserActionContainer when you do this).
300 if ((item
->GetCommand() == 0) && (*position
== DROP_BEFORE
)) {
301 BrowserActionDragData drop_data
;
302 if (!drop_data
.Read(event
.data()))
303 return ui::DragDropTypes::DRAG_NONE
;
305 if (drop_data
.index() < browser_actions_container_
->VisibleBrowserActions())
306 return ui::DragDropTypes::DRAG_NONE
;
309 return ui::DragDropTypes::DRAG_MOVE
;
312 int ChevronMenuButton::MenuController::OnPerformDrop(
313 views::MenuItemView
* menu
,
314 DropPosition position
,
315 const ui::DropTargetEvent
& event
) {
316 BrowserActionDragData drop_data
;
317 if (!drop_data
.Read(event
.data()))
318 return ui::DragDropTypes::DRAG_NONE
;
320 size_t drop_index
= IndexForId(menu
->GetCommand());
322 // When not dragging within the overflow menu (dragging an icon into the menu)
323 // subtract one to get the right index.
324 if (position
== DROP_BEFORE
&&
325 drop_data
.index() < browser_actions_container_
->VisibleBrowserActions())
328 ToolbarActionsBar::DragType drag_type
=
329 drop_data
.index() < browser_actions_container_
->VisibleBrowserActions() ?
330 ToolbarActionsBar::DRAG_TO_OVERFLOW
:
331 ToolbarActionsBar::DRAG_TO_SAME
;
332 browser_actions_container_
->toolbar_actions_bar()->OnDragDrop(
333 drop_data
.index(), drop_index
, drag_type
);
337 return ui::DragDropTypes::DRAG_MOVE
;
340 bool ChevronMenuButton::MenuController::CanDrag(views::MenuItemView
* menu
) {
344 void ChevronMenuButton::MenuController::WriteDragData(
345 views::MenuItemView
* sender
, OSExchangeData
* data
) {
346 size_t drag_index
= IndexForId(sender
->GetCommand());
347 BrowserActionDragData
drag_data(
348 browser_actions_container_
->GetIdAt(drag_index
), drag_index
);
349 drag_data
.Write(browser_actions_container_
->browser()->profile(), data
);
352 int ChevronMenuButton::MenuController::GetDragOperations(
353 views::MenuItemView
* sender
) {
354 return ui::DragDropTypes::DRAG_MOVE
;
357 size_t ChevronMenuButton::MenuController::IndexForId(int id
) const {
358 // The index of the view being dragged (GetCommand gives a 1-based index into
359 // the overflow menu).
360 DCHECK_GT(browser_actions_container_
->VisibleBrowserActions() + id
, 0u);
361 return browser_actions_container_
->VisibleBrowserActions() + id
- 1;
364 ChevronMenuButton::ChevronMenuButton(
365 BrowserActionsContainer
* browser_actions_container
)
366 : views::MenuButton(NULL
, base::string16(), this, false),
367 browser_actions_container_(browser_actions_container
),
368 weak_factory_(this) {
371 ChevronMenuButton::~ChevronMenuButton() {
374 void ChevronMenuButton::CloseMenu() {
375 if (menu_controller_
)
376 menu_controller_
->CloseMenu();
379 scoped_ptr
<views::LabelButtonBorder
> ChevronMenuButton::CreateDefaultBorder()
381 // The chevron resource was designed to not have any insets.
382 scoped_ptr
<views::LabelButtonBorder
> border
=
383 views::MenuButton::CreateDefaultBorder();
384 border
->set_insets(gfx::Insets());
385 return border
.Pass();
388 bool ChevronMenuButton::GetDropFormats(
390 std::set
<OSExchangeData::CustomFormat
>* custom_formats
) {
391 return BrowserActionDragData::GetDropFormats(custom_formats
);
394 bool ChevronMenuButton::AreDropTypesRequired() {
395 return BrowserActionDragData::AreDropTypesRequired();
398 bool ChevronMenuButton::CanDrop(const OSExchangeData
& data
) {
399 return BrowserActionDragData::CanDrop(
400 data
, browser_actions_container_
->browser()->profile());
403 void ChevronMenuButton::OnDragEntered(const ui::DropTargetEvent
& event
) {
404 DCHECK(!weak_factory_
.HasWeakPtrs());
405 if (!menu_controller_
) {
406 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
407 FROM_HERE
, base::Bind(&ChevronMenuButton::ShowOverflowMenu
,
408 weak_factory_
.GetWeakPtr(), true),
409 base::TimeDelta::FromMilliseconds(views::GetMenuShowDelay()));
413 int ChevronMenuButton::OnDragUpdated(const ui::DropTargetEvent
& event
) {
414 return ui::DragDropTypes::DRAG_MOVE
;
417 void ChevronMenuButton::OnDragExited() {
418 weak_factory_
.InvalidateWeakPtrs();
421 int ChevronMenuButton::OnPerformDrop(const ui::DropTargetEvent
& event
) {
422 weak_factory_
.InvalidateWeakPtrs();
423 return ui::DragDropTypes::DRAG_MOVE
;
426 void ChevronMenuButton::OnMenuButtonClicked(views::View
* source
,
427 const gfx::Point
& point
) {
428 DCHECK_EQ(this, source
);
429 // The menu could already be open if a user dragged an item over it but
430 // ultimately dropped elsewhere (as in that case the menu will close on a
431 // timer). In this case, the click should close the open menu.
432 if (menu_controller_
)
433 menu_controller_
->CloseMenu();
435 ShowOverflowMenu(false);
438 void ChevronMenuButton::ShowOverflowMenu(bool for_drop
) {
439 // We should never try to show an overflow menu when one is already visible.
440 DCHECK(!menu_controller_
);
441 menu_controller_
.reset(new MenuController(
442 this, browser_actions_container_
, for_drop
));
443 menu_controller_
->RunMenu(GetWidget());
446 void ChevronMenuButton::MenuDone() {
447 menu_controller_
.reset();