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.
4
5#include "ui/views/controls/menu/menu_win.h"
6
7#include <string>
8
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/keycodes/keyboard_codes.h"
14#include "ui/base/l10n/l10n_util.h"
15#include "ui/base/l10n/l10n_util_win.h"
16#include "ui/base/win/window_impl.h"
17#include "ui/gfx/canvas.h"
18#include "ui/gfx/font.h"
19#include "ui/gfx/rect.h"
20
21namespace views {
22
23// The width of an icon, including the pixels between the icon and
24// the item label.
25const int kIconWidth = 23;
26// Margins between the top of the item and the label.
27const int kItemTopMargin = 3;
28// Margins between the bottom of the item and the label.
29const int kItemBottomMargin = 4;
30// Margins between the left of the item and the icon.
31const int kItemLeftMargin = 4;
32// Margins between the right of the item and the label.
33const int kItemRightMargin = 10;
34// The width for displaying the sub-menu arrow.
35const int kArrowWidth = 10;
36
37// Current active MenuHostWindow. If NULL, no menu is active.
38static MenuHostWindow* active_host_window = NULL;
39
40// The data of menu items needed to display.
41struct MenuWin::ItemData {
42  string16 label;
43  gfx::ImageSkia icon;
44  bool submenu;
45};
46
47namespace {
48
49static int ChromeGetMenuItemID(HMENU hMenu, int pos) {
50  // The built-in Windows GetMenuItemID doesn't work for submenus,
51  // so here's our own implementation.
52  MENUITEMINFO mii = {0};
53  mii.cbSize = sizeof(mii);
54  mii.fMask = MIIM_ID;
55  GetMenuItemInfo(hMenu, pos, TRUE, &mii);
56  return mii.wID;
57}
58
59// MenuHostWindow -------------------------------------------------------------
60
61// MenuHostWindow is the HWND the HMENU is parented to. MenuHostWindow is used
62// to intercept right clicks on the HMENU and notify the delegate as well as
63// for drawing icons.
64//
65class MenuHostWindow : public ui::WindowImpl {
66 public:
67  MenuHostWindow(MenuWin* menu, HWND parent_window) : menu_(menu) {
68    int extended_style = 0;
69    // If the menu needs to be created with a right-to-left UI layout, we must
70    // set the appropriate RTL flags (such as WS_EX_LAYOUTRTL) property for the
71    // underlying HWND.
72    if (menu_->delegate()->IsRightToLeftUILayout())
73      extended_style |= l10n_util::GetExtendedStyles();
74    set_window_style(WS_CHILD);
75    set_window_ex_style(extended_style);
76    Init(parent_window, gfx::Rect());
77  }
78
79  ~MenuHostWindow() {
80    DestroyWindow(hwnd());
81  }
82
83  BEGIN_MSG_MAP_EX(MenuHostWindow);
84    MSG_WM_RBUTTONUP(OnRButtonUp)
85    MSG_WM_MEASUREITEM(OnMeasureItem)
86    MSG_WM_DRAWITEM(OnDrawItem)
87  END_MSG_MAP();
88
89 private:
90  // NOTE: I really REALLY tried to use WM_MENURBUTTONUP, but I ran into
91  // two problems in using it:
92  // 1. It doesn't contain the coordinates of the mouse.
93  // 2. It isn't invoked for menuitems representing a submenu that have children
94  //   menu items (not empty).
95
96  void OnRButtonUp(UINT w_param, const CPoint& loc) {
97    int id;
98    if (menu_->delegate() && FindMenuIDByLocation(menu_, loc, &id))
99      menu_->delegate()->ShowContextMenu(menu_, id, gfx::Point(loc), true);
100  }
101
102  void OnMeasureItem(WPARAM w_param, MEASUREITEMSTRUCT* lpmis) {
103    MenuWin::ItemData* data =
104        reinterpret_cast<MenuWin::ItemData*>(lpmis->itemData);
105    if (data != NULL) {
106      gfx::Font font;
107      lpmis->itemWidth = font.GetStringWidth(data->label) + kIconWidth +
108          kItemLeftMargin + kItemRightMargin -
109          GetSystemMetrics(SM_CXMENUCHECK);
110      if (data->submenu)
111        lpmis->itemWidth += kArrowWidth;
112      // If the label contains an accelerator, make room for tab.
113      if (data->label.find(L'\t') != string16::npos)
114        lpmis->itemWidth += font.GetStringWidth(L" ");
115      lpmis->itemHeight = font.GetHeight() + kItemBottomMargin + kItemTopMargin;
116    } else {
117      // Measure separator size.
118      lpmis->itemHeight = GetSystemMetrics(SM_CYMENU) / 2;
119      lpmis->itemWidth = 0;
120    }
121  }
122
123  void OnDrawItem(UINT wParam, DRAWITEMSTRUCT* lpdis) {
124    HDC hDC = lpdis->hDC;
125    COLORREF prev_bg_color, prev_text_color;
126
127    // Set background color and text color
128    if (lpdis->itemState & ODS_SELECTED) {
129      prev_bg_color = SetBkColor(hDC, GetSysColor(COLOR_HIGHLIGHT));
130      prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_HIGHLIGHTTEXT));
131    } else {
132      prev_bg_color = SetBkColor(hDC, GetSysColor(COLOR_MENU));
133      if (lpdis->itemState & ODS_DISABLED)
134        prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_GRAYTEXT));
135      else
136        prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_MENUTEXT));
137    }
138
139    if (lpdis->itemData) {
140      MenuWin::ItemData* data =
141          reinterpret_cast<MenuWin::ItemData*>(lpdis->itemData);
142
143      // Draw the background.
144      HBRUSH hbr = CreateSolidBrush(GetBkColor(hDC));
145      FillRect(hDC, &lpdis->rcItem, hbr);
146      DeleteObject(hbr);
147
148      // Draw the label.
149      RECT rect = lpdis->rcItem;
150      rect.top += kItemTopMargin;
151      // Should we add kIconWidth only when icon.width() != 0 ?
152      rect.left += kItemLeftMargin + kIconWidth;
153      rect.right -= kItemRightMargin;
154      UINT format = DT_TOP | DT_SINGLELINE;
155      // Check whether the mnemonics should be underlined.
156      BOOL underline_mnemonics;
157      SystemParametersInfo(SPI_GETKEYBOARDCUES, 0, &underline_mnemonics, 0);
158      if (!underline_mnemonics)
159        format |= DT_HIDEPREFIX;
160      gfx::Font font;
161      HGDIOBJ old_font =
162          static_cast<HFONT>(SelectObject(hDC, font.GetNativeFont()));
163
164      // If an accelerator is specified (with a tab delimiting the rest of the
165      // label from the accelerator), we have to justify the fist part on the
166      // left and the accelerator on the right.
167      // TODO(jungshik): This will break in RTL UI. Currently, he/ar use the
168      //                 window system UI font and will not hit here.
169      string16 label = data->label;
170      string16 accel;
171      string16::size_type tab_pos = label.find(L'\t');
172      if (tab_pos != string16::npos) {
173        accel = label.substr(tab_pos);
174        label = label.substr(0, tab_pos);
175      }
176      DrawTextEx(hDC, const_cast<wchar_t*>(label.data()),
177                 static_cast<int>(label.size()), &rect, format | DT_LEFT, NULL);
178      if (!accel.empty())
179        DrawTextEx(hDC, const_cast<wchar_t*>(accel.data()),
180                   static_cast<int>(accel.size()), &rect,
181                   format | DT_RIGHT, NULL);
182      SelectObject(hDC, old_font);
183
184      // Draw the icon after the label, otherwise it would be covered
185      // by the label.
186      gfx::ImageSkiaRep icon_image_rep =
187          data->icon.GetRepresentation(ui::SCALE_FACTOR_100P);
188      if (data->icon.width() != 0 && data->icon.height() != 0) {
189        gfx::Canvas canvas(icon_image_rep, false);
190        skia::DrawToNativeContext(
191            canvas.sk_canvas(), hDC, lpdis->rcItem.left + kItemLeftMargin,
192            lpdis->rcItem.top + (lpdis->rcItem.bottom - lpdis->rcItem.top -
193                data->icon.height()) / 2, NULL);
194      }
195
196    } else {
197      // Draw the separator
198      lpdis->rcItem.top += (lpdis->rcItem.bottom - lpdis->rcItem.top) / 3;
199      DrawEdge(hDC, &lpdis->rcItem, EDGE_ETCHED, BF_TOP);
200    }
201
202    SetBkColor(hDC, prev_bg_color);
203    SetTextColor(hDC, prev_text_color);
204  }
205
206  bool FindMenuIDByLocation(MenuWin* menu, const CPoint& loc, int* id) {
207    int index = MenuItemFromPoint(NULL, menu->menu_, loc);
208    if (index != -1) {
209      *id = ChromeGetMenuItemID(menu->menu_, index);
210      return true;
211    } else {
212      for (std::vector<MenuWin*>::iterator i = menu->submenus_.begin();
213           i != menu->submenus_.end(); ++i) {
214        if (FindMenuIDByLocation(*i, loc, id))
215          return true;
216      }
217    }
218    return false;
219  }
220
221  // The menu that created us.
222  MenuWin* menu_;
223
224  DISALLOW_COPY_AND_ASSIGN(MenuHostWindow);
225};
226
227}  // namespace
228
229// static
230Menu* Menu::Create(Delegate* delegate,
231                   AnchorPoint anchor,
232                   gfx::NativeView parent) {
233  return new MenuWin(delegate, anchor, parent);
234}
235
236// static
237Menu* Menu::GetSystemMenu(gfx::NativeWindow parent) {
238  return new views::MenuWin(::GetSystemMenu(parent, FALSE));
239}
240
241MenuWin::MenuWin(Delegate* d, AnchorPoint anchor, HWND owner)
242    : Menu(d, anchor),
243      menu_(CreatePopupMenu()),
244      owner_(owner),
245      is_menu_visible_(false),
246      owner_draw_(l10n_util::NeedOverrideDefaultUIFont(NULL, NULL)) {
247  DCHECK(delegate());
248}
249
250MenuWin::MenuWin(HMENU hmenu)
251    : Menu(NULL, TOPLEFT),
252      menu_(hmenu),
253      owner_(NULL),
254      is_menu_visible_(false),
255      owner_draw_(false) {
256  DCHECK(menu_);
257}
258
259MenuWin::~MenuWin() {
260  STLDeleteContainerPointers(submenus_.begin(), submenus_.end());
261  STLDeleteContainerPointers(item_data_.begin(), item_data_.end());
262  DestroyMenu(menu_);
263}
264
265void MenuWin::AddMenuItemWithIcon(int index,
266                                  int item_id,
267                                  const string16& label,
268                                  const gfx::ImageSkia& icon) {
269  owner_draw_ = true;
270  Menu::AddMenuItemWithIcon(index, item_id, label, icon);
271}
272
273Menu* MenuWin::AddSubMenuWithIcon(int index,
274                                  int item_id,
275                                  const string16& label,
276                                  const gfx::ImageSkia& icon) {
277  MenuWin* submenu = new MenuWin(this);
278  submenus_.push_back(submenu);
279  AddMenuItemInternal(index, item_id, label, icon, submenu->menu_, NORMAL);
280  return submenu;
281}
282
283void MenuWin::AddSeparator(int index) {
284  MENUITEMINFO mii;
285  mii.cbSize = sizeof(mii);
286  mii.fMask = MIIM_FTYPE;
287  mii.fType = MFT_SEPARATOR;
288  InsertMenuItem(menu_, index, TRUE, &mii);
289}
290
291void MenuWin::EnableMenuItemByID(int item_id, bool enabled) {
292  UINT enable_flags = enabled ? MF_ENABLED : MF_DISABLED | MF_GRAYED;
293  EnableMenuItem(menu_, item_id, MF_BYCOMMAND | enable_flags);
294}
295
296void MenuWin::EnableMenuItemAt(int index, bool enabled) {
297  UINT enable_flags = enabled ? MF_ENABLED : MF_DISABLED | MF_GRAYED;
298  EnableMenuItem(menu_, index, MF_BYPOSITION | enable_flags);
299}
300
301void MenuWin::SetMenuLabel(int item_id, const string16& label) {
302  MENUITEMINFO mii = {0};
303  mii.cbSize = sizeof(mii);
304  mii.fMask = MIIM_STRING;
305  mii.dwTypeData = const_cast<wchar_t*>(label.c_str());
306  mii.cch = static_cast<UINT>(label.size());
307  SetMenuItemInfo(menu_, item_id, false, &mii);
308}
309
310bool MenuWin::SetIcon(const gfx::ImageSkia& icon, int item_id) {
311  if (!owner_draw_)
312    owner_draw_ = true;
313
314  const int num_items = GetMenuItemCount(menu_);
315  int sep_count = 0;
316  for (int i = 0; i < num_items; ++i) {
317    if (!(GetMenuState(menu_, i, MF_BYPOSITION) & MF_SEPARATOR)) {
318      if (ChromeGetMenuItemID(menu_, i) == item_id) {
319        item_data_[i - sep_count]->icon = icon;
320        // When the menu is running, we use SetMenuItemInfo to let Windows
321        // update the item information so that the icon being displayed
322        // could change immediately.
323        if (active_host_window) {
324          MENUITEMINFO mii;
325          mii.cbSize = sizeof(mii);
326          mii.fMask = MIIM_FTYPE | MIIM_DATA;
327          mii.fType = MFT_OWNERDRAW;
328          mii.dwItemData =
329              reinterpret_cast<ULONG_PTR>(item_data_[i - sep_count]);
330          SetMenuItemInfo(menu_, item_id, false, &mii);
331        }
332        return true;
333      }
334    } else {
335      ++sep_count;
336    }
337  }
338
339  // Continue searching for the item in submenus.
340  for (size_t i = 0; i < submenus_.size(); ++i) {
341    if (submenus_[i]->SetIcon(icon, item_id))
342      return true;
343  }
344
345  return false;
346}
347
348void MenuWin::RunMenuAt(int x, int y) {
349  SetMenuInfo();
350
351  delegate()->MenuWillShow();
352
353  // NOTE: we don't use TPM_RIGHTBUTTON here as it breaks selecting by way of
354  // press, drag, release. See bugs 718 and 8560.
355  UINT flags =
356      GetTPMAlignFlags() | TPM_LEFTBUTTON | TPM_RETURNCMD | TPM_RECURSE;
357  is_menu_visible_ = true;
358  DCHECK(owner_);
359  // In order for context menus on menus to work, the context menu needs to
360  // share the same window as the first menu is parented to.
361  bool created_host = false;
362  if (!active_host_window) {
363    created_host = true;
364    active_host_window = new MenuHostWindow(this, owner_);
365  }
366  UINT selected_id =
367      TrackPopupMenuEx(menu_, flags, x, y, active_host_window->hwnd(), NULL);
368  if (created_host) {
369    delete active_host_window;
370    active_host_window = NULL;
371  }
372  is_menu_visible_ = false;
373
374  // Execute the chosen command
375  if (selected_id != 0)
376    delegate()->ExecuteCommand(selected_id);
377}
378
379void MenuWin::Cancel() {
380  DCHECK(is_menu_visible_);
381  EndMenu();
382}
383
384int MenuWin::ItemCount() {
385  return GetMenuItemCount(menu_);
386}
387
388void MenuWin::AddMenuItemInternal(int index,
389                                  int item_id,
390                                  const string16& label,
391                                  const gfx::ImageSkia& icon,
392                                  MenuItemType type) {
393  AddMenuItemInternal(index, item_id, label, icon, NULL, type);
394}
395
396void MenuWin::AddMenuItemInternal(int index,
397                                  int item_id,
398                                  const string16& label,
399                                  const gfx::ImageSkia& icon,
400                                  HMENU submenu,
401                                  MenuItemType type) {
402  DCHECK(type != SEPARATOR) << "Call AddSeparator instead!";
403
404  if (!owner_draw_ && !icon.isNull())
405    owner_draw_ = true;
406
407  if (label.empty() && !delegate()) {
408    // No label and no delegate; don't add an empty menu.
409    // It appears under some circumstance we're getting an empty label
410    // (l10n_util::GetStringUTF16(IDS_TASK_MANAGER) returns ""). This shouldn't
411    // happen, but I'm working over the crash here.
412    NOTREACHED();
413    return;
414  }
415
416  MENUITEMINFO mii;
417  mii.cbSize = sizeof(mii);
418  mii.fMask = MIIM_FTYPE | MIIM_ID;
419  if (submenu) {
420    mii.fMask |= MIIM_SUBMENU;
421    mii.hSubMenu = submenu;
422  }
423
424  // Set the type and ID.
425  if (!owner_draw_) {
426    mii.fType = MFT_STRING;
427    mii.fMask |= MIIM_STRING;
428  } else {
429    mii.fType = MFT_OWNERDRAW;
430  }
431
432  if (type == RADIO)
433    mii.fType |= MFT_RADIOCHECK;
434
435  mii.wID = item_id;
436
437  // Set the item data.
438  MenuWin::ItemData* data = new ItemData;
439  item_data_.push_back(data);
440  data->submenu = submenu != NULL;
441
442  string16 actual_label(label.empty() ? delegate()->GetLabel(item_id) : label);
443
444  // Find out if there is a shortcut we need to append to the label.
445  ui::Accelerator accelerator(ui::VKEY_UNKNOWN, ui::EF_NONE);
446  if (delegate() && delegate()->GetAcceleratorInfo(item_id, &accelerator)) {
447    actual_label += L'\t';
448    actual_label += accelerator.GetShortcutText();
449  }
450  labels_.push_back(actual_label);
451
452  if (owner_draw_) {
453    if (icon.width() != 0 && icon.height() != 0)
454      data->icon = icon;
455    else
456      data->icon = delegate()->GetIcon(item_id);
457  } else {
458    mii.dwTypeData = const_cast<wchar_t*>(labels_.back().c_str());
459  }
460
461  InsertMenuItem(menu_, index, TRUE, &mii);
462}
463
464MenuWin::MenuWin(MenuWin* parent)
465    : Menu(parent->delegate(), parent->anchor()),
466      menu_(CreatePopupMenu()),
467      owner_(parent->owner_),
468      is_menu_visible_(false),
469      owner_draw_(parent->owner_draw_) {
470}
471
472void MenuWin::SetMenuInfo() {
473  const int num_items = GetMenuItemCount(menu_);
474  int sep_count = 0;
475  for (int i = 0; i < num_items; ++i) {
476    MENUITEMINFO mii_info;
477    mii_info.cbSize = sizeof(mii_info);
478    // Get the menu's original type.
479    mii_info.fMask = MIIM_FTYPE;
480    GetMenuItemInfo(menu_, i, MF_BYPOSITION, &mii_info);
481    // Set item states.
482    if (!(mii_info.fType & MF_SEPARATOR)) {
483      const int id = ChromeGetMenuItemID(menu_, i);
484
485      MENUITEMINFO mii;
486      mii.cbSize = sizeof(mii);
487      mii.fMask = MIIM_STATE | MIIM_FTYPE | MIIM_DATA | MIIM_STRING;
488      // We also need MFT_STRING for owner drawn items in order to let Windows
489      // handle the accelerators for us.
490      mii.fType = MFT_STRING;
491      if (owner_draw_)
492        mii.fType |= MFT_OWNERDRAW;
493      // If the menu originally has radiocheck type, we should follow it.
494      if (mii_info.fType & MFT_RADIOCHECK)
495        mii.fType |= MFT_RADIOCHECK;
496      mii.fState = GetStateFlagsForItemID(id);
497
498      // Validate the label. If there is a contextual label, use it, otherwise
499      // default to the static label
500      string16 label;
501      if (!delegate()->GetContextualLabel(id, &label))
502        label = labels_[i - sep_count];
503
504      if (owner_draw_) {
505        item_data_[i - sep_count]->label = label;
506        mii.dwItemData = reinterpret_cast<ULONG_PTR>(item_data_[i - sep_count]);
507      }
508      mii.dwTypeData = const_cast<wchar_t*>(label.c_str());
509      mii.cch = static_cast<UINT>(label.size());
510      SetMenuItemInfo(menu_, i, true, &mii);
511    } else {
512      // Set data for owner drawn separators. Set dwItemData NULL to indicate
513      // a separator.
514      if (owner_draw_) {
515        MENUITEMINFO mii;
516        mii.cbSize = sizeof(mii);
517        mii.fMask = MIIM_FTYPE;
518        mii.fType = MFT_SEPARATOR | MFT_OWNERDRAW;
519        mii.dwItemData = NULL;
520        SetMenuItemInfo(menu_, i, true, &mii);
521      }
522      ++sep_count;
523    }
524  }
525
526  for (size_t i = 0; i < submenus_.size(); ++i)
527    submenus_[i]->SetMenuInfo();
528}
529
530UINT MenuWin::GetStateFlagsForItemID(int item_id) const {
531  // Use the delegate to get enabled and checked state.
532  UINT flags =
533    delegate()->IsCommandEnabled(item_id) ? MFS_ENABLED : MFS_DISABLED;
534
535  if (delegate()->IsItemChecked(item_id))
536    flags |= MFS_CHECKED;
537
538  if (delegate()->IsItemDefault(item_id))
539    flags |= MFS_DEFAULT;
540
541  return flags;
542}
543
544DWORD MenuWin::GetTPMAlignFlags() const {
545  // The manner in which we handle the menu alignment depends on whether or not
546  // the menu is displayed within a mirrored view. If the UI is mirrored, the
547  // alignment needs to be fliped so that instead of aligning the menu to the
548  // right of the point, we align it to the left and vice versa.
549  DWORD align_flags = TPM_TOPALIGN;
550  switch (anchor()) {
551    case TOPLEFT:
552      if (delegate()->IsRightToLeftUILayout()) {
553        align_flags |= TPM_RIGHTALIGN;
554      } else {
555        align_flags |= TPM_LEFTALIGN;
556      }
557      break;
558
559    case TOPRIGHT:
560      if (delegate()->IsRightToLeftUILayout()) {
561        align_flags |= TPM_LEFTALIGN;
562      } else {
563        align_flags |= TPM_RIGHTALIGN;
564      }
565      break;
566
567    default:
568      NOTREACHED();
569      return 0;
570  }
571  return align_flags;
572}
573
574}  // namespace views
575