Home | History | Annotate | Download | only in menu
      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 
     21 namespace views {
     22 
     23 // The width of an icon, including the pixels between the icon and
     24 // the item label.
     25 const int kIconWidth = 23;
     26 // Margins between the top of the item and the label.
     27 const int kItemTopMargin = 3;
     28 // Margins between the bottom of the item and the label.
     29 const int kItemBottomMargin = 4;
     30 // Margins between the left of the item and the icon.
     31 const int kItemLeftMargin = 4;
     32 // Margins between the right of the item and the label.
     33 const int kItemRightMargin = 10;
     34 // The width for displaying the sub-menu arrow.
     35 const int kArrowWidth = 10;
     36 
     37 // Current active MenuHostWindow. If NULL, no menu is active.
     38 static MenuHostWindow* active_host_window = NULL;
     39 
     40 // The data of menu items needed to display.
     41 struct MenuWin::ItemData {
     42   string16 label;
     43   gfx::ImageSkia icon;
     44   bool submenu;
     45 };
     46 
     47 namespace {
     48 
     49 static 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 //
     65 class 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
    230 Menu* Menu::Create(Delegate* delegate,
    231                    AnchorPoint anchor,
    232                    gfx::NativeView parent) {
    233   return new MenuWin(delegate, anchor, parent);
    234 }
    235 
    236 // static
    237 Menu* Menu::GetSystemMenu(gfx::NativeWindow parent) {
    238   return new views::MenuWin(::GetSystemMenu(parent, FALSE));
    239 }
    240 
    241 MenuWin::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 
    250 MenuWin::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 
    259 MenuWin::~MenuWin() {
    260   STLDeleteContainerPointers(submenus_.begin(), submenus_.end());
    261   STLDeleteContainerPointers(item_data_.begin(), item_data_.end());
    262   DestroyMenu(menu_);
    263 }
    264 
    265 void 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 
    273 Menu* 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 
    283 void 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 
    291 void 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 
    296 void 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 
    301 void 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 
    310 bool 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 
    348 void 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 
    379 void MenuWin::Cancel() {
    380   DCHECK(is_menu_visible_);
    381   EndMenu();
    382 }
    383 
    384 int MenuWin::ItemCount() {
    385   return GetMenuItemCount(menu_);
    386 }
    387 
    388 void 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 
    396 void 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 
    464 MenuWin::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 
    472 void 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 
    530 UINT 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 
    544 DWORD 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