1 // Copyright (c) 2012 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 "ui/views/controls/menu/menu_win.h"
9 #include "base/logging.h"
10 #include "base/stl_util.h"
11 #include "base/strings/string_util.h"
12 #include "ui/base/accelerators/accelerator.h"
13 #include "ui/base/l10n/l10n_util.h"
14 #include "ui/base/l10n/l10n_util_win.h"
15 #include "ui/events/keycodes/keyboard_codes.h"
16 #include "ui/gfx/canvas.h"
17 #include "ui/gfx/font.h"
18 #include "ui/gfx/rect.h"
19 #include "ui/gfx/win/window_impl.h"
20 #include "ui/views/layout/layout_constants.h"
24 // The width of an icon, including the pixels between the icon and
26 const int kIconWidth = 23;
27 // Margins between the top of the item and the label.
28 const int kItemTopMargin = 3;
29 // Margins between the bottom of the item and the label.
30 const int kItemBottomMargin = 4;
31 // Margins between the left of the item and the icon.
32 const int kItemLeftMargin = 4;
33 // The width for displaying the sub-menu arrow.
34 const int kArrowWidth = 10;
36 // Current active MenuHostWindow. If NULL, no menu is active.
37 static MenuHostWindow* active_host_window = NULL;
39 // The data of menu items needed to display.
40 struct MenuWin::ItemData {
48 static int ChromeGetMenuItemID(HMENU hMenu, int pos) {
49 // The built-in Windows GetMenuItemID doesn't work for submenus,
50 // so here's our own implementation.
51 MENUITEMINFO mii = {0};
52 mii.cbSize = sizeof(mii);
54 GetMenuItemInfo(hMenu, pos, TRUE, &mii);
58 // MenuHostWindow -------------------------------------------------------------
60 // MenuHostWindow is the HWND the HMENU is parented to. MenuHostWindow is used
61 // to intercept right clicks on the HMENU and notify the delegate as well as
64 class MenuHostWindow : public gfx::WindowImpl {
66 MenuHostWindow(MenuWin* menu, HWND parent_window) : menu_(menu) {
67 int extended_style = 0;
68 // If the menu needs to be created with a right-to-left UI layout, we must
69 // set the appropriate RTL flags (such as WS_EX_LAYOUTRTL) property for the
71 if (menu_->delegate()->IsRightToLeftUILayout())
72 extended_style |= l10n_util::GetExtendedStyles();
73 set_window_style(WS_CHILD);
74 set_window_ex_style(extended_style);
75 Init(parent_window, gfx::Rect());
79 DestroyWindow(hwnd());
82 BEGIN_MSG_MAP_EX(MenuHostWindow);
83 MSG_WM_RBUTTONUP(OnRButtonUp)
84 MSG_WM_MEASUREITEM(OnMeasureItem)
85 MSG_WM_DRAWITEM(OnDrawItem)
89 // NOTE: I really REALLY tried to use WM_MENURBUTTONUP, but I ran into
90 // two problems in using it:
91 // 1. It doesn't contain the coordinates of the mouse.
92 // 2. It isn't invoked for menuitems representing a submenu that have children
93 // menu items (not empty).
95 void OnRButtonUp(UINT w_param, const CPoint& loc) {
97 if (menu_->delegate() && FindMenuIDByLocation(menu_, loc, &id))
98 menu_->delegate()->ShowContextMenu(menu_, id, gfx::Point(loc), true);
101 void OnMeasureItem(WPARAM w_param, MEASUREITEMSTRUCT* lpmis) {
102 MenuWin::ItemData* data =
103 reinterpret_cast<MenuWin::ItemData*>(lpmis->itemData);
106 lpmis->itemWidth = font.GetStringWidth(data->label) + kIconWidth +
107 kItemLeftMargin + views::kItemLabelSpacing -
108 GetSystemMetrics(SM_CXMENUCHECK);
110 lpmis->itemWidth += kArrowWidth;
111 // If the label contains an accelerator, make room for tab.
112 if (data->label.find(L'\t') != string16::npos)
113 lpmis->itemWidth += font.GetStringWidth(L" ");
114 lpmis->itemHeight = font.GetHeight() + kItemBottomMargin + kItemTopMargin;
116 // Measure separator size.
117 lpmis->itemHeight = GetSystemMetrics(SM_CYMENU) / 2;
118 lpmis->itemWidth = 0;
122 void OnDrawItem(UINT wParam, DRAWITEMSTRUCT* lpdis) {
123 HDC hDC = lpdis->hDC;
124 COLORREF prev_bg_color, prev_text_color;
126 // Set background color and text color
127 if (lpdis->itemState & ODS_SELECTED) {
128 prev_bg_color = SetBkColor(hDC, GetSysColor(COLOR_HIGHLIGHT));
129 prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_HIGHLIGHTTEXT));
131 prev_bg_color = SetBkColor(hDC, GetSysColor(COLOR_MENU));
132 if (lpdis->itemState & ODS_DISABLED)
133 prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_GRAYTEXT));
135 prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_MENUTEXT));
138 if (lpdis->itemData) {
139 MenuWin::ItemData* data =
140 reinterpret_cast<MenuWin::ItemData*>(lpdis->itemData);
142 // Draw the background.
143 HBRUSH hbr = CreateSolidBrush(GetBkColor(hDC));
144 FillRect(hDC, &lpdis->rcItem, hbr);
148 RECT rect = lpdis->rcItem;
149 rect.top += kItemTopMargin;
150 // Should we add kIconWidth only when icon.width() != 0 ?
151 rect.left += kItemLeftMargin + kIconWidth;
152 rect.right -= views::kItemLabelSpacing;
153 UINT format = DT_TOP | DT_SINGLELINE;
154 // Check whether the mnemonics should be underlined.
155 BOOL underline_mnemonics;
156 SystemParametersInfo(SPI_GETKEYBOARDCUES, 0, &underline_mnemonics, 0);
157 if (!underline_mnemonics)
158 format |= DT_HIDEPREFIX;
161 static_cast<HFONT>(SelectObject(hDC, font.GetNativeFont()));
163 // If an accelerator is specified (with a tab delimiting the rest of the
164 // label from the accelerator), we have to justify the fist part on the
165 // left and the accelerator on the right.
166 // TODO(jungshik): This will break in RTL UI. Currently, he/ar use the
167 // window system UI font and will not hit here.
168 string16 label = data->label;
170 string16::size_type tab_pos = label.find(L'\t');
171 if (tab_pos != string16::npos) {
172 accel = label.substr(tab_pos);
173 label = label.substr(0, tab_pos);
175 DrawTextEx(hDC, const_cast<wchar_t*>(label.data()),
176 static_cast<int>(label.size()), &rect, format | DT_LEFT, NULL);
178 DrawTextEx(hDC, const_cast<wchar_t*>(accel.data()),
179 static_cast<int>(accel.size()), &rect,
180 format | DT_RIGHT, NULL);
181 SelectObject(hDC, old_font);
183 // Draw the icon after the label, otherwise it would be covered
185 gfx::ImageSkiaRep icon_image_rep = data->icon.GetRepresentation(1.0f);
186 if (data->icon.width() != 0 && data->icon.height() != 0) {
187 gfx::Canvas canvas(icon_image_rep, false);
188 skia::DrawToNativeContext(
189 canvas.sk_canvas(), hDC, lpdis->rcItem.left + kItemLeftMargin,
190 lpdis->rcItem.top + (lpdis->rcItem.bottom - lpdis->rcItem.top -
191 data->icon.height()) / 2, NULL);
195 // Draw the separator
196 lpdis->rcItem.top += (lpdis->rcItem.bottom - lpdis->rcItem.top) / 3;
197 DrawEdge(hDC, &lpdis->rcItem, EDGE_ETCHED, BF_TOP);
200 SetBkColor(hDC, prev_bg_color);
201 SetTextColor(hDC, prev_text_color);
204 bool FindMenuIDByLocation(MenuWin* menu, const CPoint& loc, int* id) {
205 int index = MenuItemFromPoint(NULL, menu->menu_, loc);
207 *id = ChromeGetMenuItemID(menu->menu_, index);
210 for (std::vector<MenuWin*>::iterator i = menu->submenus_.begin();
211 i != menu->submenus_.end(); ++i) {
212 if (FindMenuIDByLocation(*i, loc, id))
219 // The menu that created us.
222 DISALLOW_COPY_AND_ASSIGN(MenuHostWindow);
228 Menu* Menu::Create(Delegate* delegate,
230 gfx::NativeView parent) {
231 return new MenuWin(delegate, anchor, parent);
235 Menu* Menu::GetSystemMenu(gfx::NativeWindow parent) {
236 return new views::MenuWin(::GetSystemMenu(parent, FALSE));
239 MenuWin::MenuWin(Delegate* d, AnchorPoint anchor, HWND owner)
241 menu_(CreatePopupMenu()),
243 is_menu_visible_(false),
244 owner_draw_(l10n_util::NeedOverrideDefaultUIFont(NULL, NULL)) {
248 MenuWin::MenuWin(HMENU hmenu)
249 : Menu(NULL, TOPLEFT),
252 is_menu_visible_(false),
257 MenuWin::~MenuWin() {
258 STLDeleteContainerPointers(submenus_.begin(), submenus_.end());
259 STLDeleteContainerPointers(item_data_.begin(), item_data_.end());
263 void MenuWin::AddMenuItemWithIcon(int index,
265 const string16& label,
266 const gfx::ImageSkia& icon) {
268 Menu::AddMenuItemWithIcon(index, item_id, label, icon);
271 Menu* MenuWin::AddSubMenuWithIcon(int index,
273 const string16& label,
274 const gfx::ImageSkia& icon) {
275 MenuWin* submenu = new MenuWin(this);
276 submenus_.push_back(submenu);
277 AddMenuItemInternal(index, item_id, label, icon, submenu->menu_, NORMAL);
281 void MenuWin::AddSeparator(int index) {
283 mii.cbSize = sizeof(mii);
284 mii.fMask = MIIM_FTYPE;
285 mii.fType = MFT_SEPARATOR;
286 InsertMenuItem(menu_, index, TRUE, &mii);
289 void MenuWin::EnableMenuItemByID(int item_id, bool enabled) {
290 UINT enable_flags = enabled ? MF_ENABLED : MF_DISABLED | MF_GRAYED;
291 EnableMenuItem(menu_, item_id, MF_BYCOMMAND | enable_flags);
294 void MenuWin::EnableMenuItemAt(int index, bool enabled) {
295 UINT enable_flags = enabled ? MF_ENABLED : MF_DISABLED | MF_GRAYED;
296 EnableMenuItem(menu_, index, MF_BYPOSITION | enable_flags);
299 void MenuWin::SetMenuLabel(int item_id, const string16& label) {
300 MENUITEMINFO mii = {0};
301 mii.cbSize = sizeof(mii);
302 mii.fMask = MIIM_STRING;
303 mii.dwTypeData = const_cast<wchar_t*>(label.c_str());
304 mii.cch = static_cast<UINT>(label.size());
305 SetMenuItemInfo(menu_, item_id, false, &mii);
308 bool MenuWin::SetIcon(const gfx::ImageSkia& icon, int item_id) {
312 const int num_items = GetMenuItemCount(menu_);
314 for (int i = 0; i < num_items; ++i) {
315 if (!(GetMenuState(menu_, i, MF_BYPOSITION) & MF_SEPARATOR)) {
316 if (ChromeGetMenuItemID(menu_, i) == item_id) {
317 item_data_[i - sep_count]->icon = icon;
318 // When the menu is running, we use SetMenuItemInfo to let Windows
319 // update the item information so that the icon being displayed
320 // could change immediately.
321 if (active_host_window) {
323 mii.cbSize = sizeof(mii);
324 mii.fMask = MIIM_FTYPE | MIIM_DATA;
325 mii.fType = MFT_OWNERDRAW;
327 reinterpret_cast<ULONG_PTR>(item_data_[i - sep_count]);
328 SetMenuItemInfo(menu_, item_id, false, &mii);
337 // Continue searching for the item in submenus.
338 for (size_t i = 0; i < submenus_.size(); ++i) {
339 if (submenus_[i]->SetIcon(icon, item_id))
346 void MenuWin::RunMenuAt(int x, int y) {
349 delegate()->MenuWillShow();
351 // NOTE: we don't use TPM_RIGHTBUTTON here as it breaks selecting by way of
352 // press, drag, release. See bugs 718 and 8560.
354 GetTPMAlignFlags() | TPM_LEFTBUTTON | TPM_RETURNCMD | TPM_RECURSE;
355 is_menu_visible_ = true;
357 // In order for context menus on menus to work, the context menu needs to
358 // share the same window as the first menu is parented to.
359 bool created_host = false;
360 if (!active_host_window) {
362 active_host_window = new MenuHostWindow(this, owner_);
365 TrackPopupMenuEx(menu_, flags, x, y, active_host_window->hwnd(), NULL);
367 delete active_host_window;
368 active_host_window = NULL;
370 is_menu_visible_ = false;
372 // Execute the chosen command
373 if (selected_id != 0)
374 delegate()->ExecuteCommand(selected_id);
377 void MenuWin::Cancel() {
378 DCHECK(is_menu_visible_);
382 int MenuWin::ItemCount() {
383 return GetMenuItemCount(menu_);
386 void MenuWin::AddMenuItemInternal(int index,
388 const string16& label,
389 const gfx::ImageSkia& icon,
391 AddMenuItemInternal(index, item_id, label, icon, NULL, type);
394 void MenuWin::AddMenuItemInternal(int index,
396 const string16& label,
397 const gfx::ImageSkia& icon,
400 DCHECK(type != SEPARATOR) << "Call AddSeparator instead!";
402 if (!owner_draw_ && !icon.isNull())
405 if (label.empty() && !delegate()) {
406 // No label and no delegate; don't add an empty menu.
407 // It appears under some circumstance we're getting an empty label
408 // (l10n_util::GetStringUTF16(IDS_TASK_MANAGER) returns ""). This shouldn't
409 // happen, but I'm working over the crash here.
415 mii.cbSize = sizeof(mii);
416 mii.fMask = MIIM_FTYPE | MIIM_ID;
418 mii.fMask |= MIIM_SUBMENU;
419 mii.hSubMenu = submenu;
422 // Set the type and ID.
424 mii.fType = MFT_STRING;
425 mii.fMask |= MIIM_STRING;
427 mii.fType = MFT_OWNERDRAW;
431 mii.fType |= MFT_RADIOCHECK;
435 // Set the item data.
436 MenuWin::ItemData* data = new ItemData;
437 item_data_.push_back(data);
438 data->submenu = submenu != NULL;
440 string16 actual_label(label.empty() ? delegate()->GetLabel(item_id) : label);
442 // Find out if there is a shortcut we need to append to the label.
443 ui::Accelerator accelerator(ui::VKEY_UNKNOWN, ui::EF_NONE);
444 if (delegate() && delegate()->GetAcceleratorInfo(item_id, &accelerator)) {
445 actual_label += L'\t';
446 actual_label += accelerator.GetShortcutText();
448 labels_.push_back(actual_label);
451 if (icon.width() != 0 && icon.height() != 0)
454 data->icon = delegate()->GetIcon(item_id);
456 mii.dwTypeData = const_cast<wchar_t*>(labels_.back().c_str());
459 InsertMenuItem(menu_, index, TRUE, &mii);
462 MenuWin::MenuWin(MenuWin* parent)
463 : Menu(parent->delegate(), parent->anchor()),
464 menu_(CreatePopupMenu()),
465 owner_(parent->owner_),
466 is_menu_visible_(false),
467 owner_draw_(parent->owner_draw_) {
470 void MenuWin::SetMenuInfo() {
471 const int num_items = GetMenuItemCount(menu_);
473 for (int i = 0; i < num_items; ++i) {
474 MENUITEMINFO mii_info;
475 mii_info.cbSize = sizeof(mii_info);
476 // Get the menu's original type.
477 mii_info.fMask = MIIM_FTYPE;
478 GetMenuItemInfo(menu_, i, MF_BYPOSITION, &mii_info);
480 if (!(mii_info.fType & MF_SEPARATOR)) {
481 const int id = ChromeGetMenuItemID(menu_, i);
484 mii.cbSize = sizeof(mii);
485 mii.fMask = MIIM_STATE | MIIM_FTYPE | MIIM_DATA | MIIM_STRING;
486 // We also need MFT_STRING for owner drawn items in order to let Windows
487 // handle the accelerators for us.
488 mii.fType = MFT_STRING;
490 mii.fType |= MFT_OWNERDRAW;
491 // If the menu originally has radiocheck type, we should follow it.
492 if (mii_info.fType & MFT_RADIOCHECK)
493 mii.fType |= MFT_RADIOCHECK;
494 mii.fState = GetStateFlagsForItemID(id);
496 // Validate the label. If there is a contextual label, use it, otherwise
497 // default to the static label
499 if (!delegate()->GetContextualLabel(id, &label))
500 label = labels_[i - sep_count];
503 item_data_[i - sep_count]->label = label;
504 mii.dwItemData = reinterpret_cast<ULONG_PTR>(item_data_[i - sep_count]);
506 mii.dwTypeData = const_cast<wchar_t*>(label.c_str());
507 mii.cch = static_cast<UINT>(label.size());
508 SetMenuItemInfo(menu_, i, true, &mii);
510 // Set data for owner drawn separators. Set dwItemData NULL to indicate
514 mii.cbSize = sizeof(mii);
515 mii.fMask = MIIM_FTYPE;
516 mii.fType = MFT_SEPARATOR | MFT_OWNERDRAW;
517 mii.dwItemData = NULL;
518 SetMenuItemInfo(menu_, i, true, &mii);
524 for (size_t i = 0; i < submenus_.size(); ++i)
525 submenus_[i]->SetMenuInfo();
528 UINT MenuWin::GetStateFlagsForItemID(int item_id) const {
529 // Use the delegate to get enabled and checked state.
531 delegate()->IsCommandEnabled(item_id) ? MFS_ENABLED : MFS_DISABLED;
533 if (delegate()->IsItemChecked(item_id))
534 flags |= MFS_CHECKED;
536 if (delegate()->IsItemDefault(item_id))
537 flags |= MFS_DEFAULT;
542 DWORD MenuWin::GetTPMAlignFlags() const {
543 // The manner in which we handle the menu alignment depends on whether or not
544 // the menu is displayed within a mirrored view. If the UI is mirrored, the
545 // alignment needs to be fliped so that instead of aligning the menu to the
546 // right of the point, we align it to the left and vice versa.
547 DWORD align_flags = TPM_TOPALIGN;
550 if (delegate()->IsRightToLeftUILayout()) {
551 align_flags |= TPM_RIGHTALIGN;
553 align_flags |= TPM_LEFTALIGN;
558 if (delegate()->IsRightToLeftUILayout()) {
559 align_flags |= TPM_LEFTALIGN;
561 align_flags |= TPM_RIGHTALIGN;