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/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"
     21 
     22 namespace views {
     23 
     24 // The width of an icon, including the pixels between the icon and
     25 // the item label.
     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;
     35 
     36 // Current active MenuHostWindow. If NULL, no menu is active.
     37 static MenuHostWindow* active_host_window = NULL;
     38 
     39 // The data of menu items needed to display.
     40 struct MenuWin::ItemData {
     41   string16 label;
     42   gfx::ImageSkia icon;
     43   bool submenu;
     44 };
     45 
     46 namespace {
     47 
     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);
     53   mii.fMask = MIIM_ID;
     54   GetMenuItemInfo(hMenu, pos, TRUE, &mii);
     55   return mii.wID;
     56 }
     57 
     58 // MenuHostWindow -------------------------------------------------------------
     59 
     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
     62 // for drawing icons.
     63 //
     64 class MenuHostWindow : public gfx::WindowImpl {
     65  public:
     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
     70     // underlying HWND.
     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());
     76   }
     77 
     78   ~MenuHostWindow() {
     79     DestroyWindow(hwnd());
     80   }
     81 
     82   BEGIN_MSG_MAP_EX(MenuHostWindow);
     83     MSG_WM_RBUTTONUP(OnRButtonUp)
     84     MSG_WM_MEASUREITEM(OnMeasureItem)
     85     MSG_WM_DRAWITEM(OnDrawItem)
     86   END_MSG_MAP();
     87 
     88  private:
     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).
     94 
     95   void OnRButtonUp(UINT w_param, const CPoint& loc) {
     96     int id;
     97     if (menu_->delegate() && FindMenuIDByLocation(menu_, loc, &id))
     98       menu_->delegate()->ShowContextMenu(menu_, id, gfx::Point(loc), true);
     99   }
    100 
    101   void OnMeasureItem(WPARAM w_param, MEASUREITEMSTRUCT* lpmis) {
    102     MenuWin::ItemData* data =
    103         reinterpret_cast<MenuWin::ItemData*>(lpmis->itemData);
    104     if (data != NULL) {
    105       gfx::Font font;
    106       lpmis->itemWidth = font.GetStringWidth(data->label) + kIconWidth +
    107           kItemLeftMargin + views::kItemLabelSpacing -
    108           GetSystemMetrics(SM_CXMENUCHECK);
    109       if (data->submenu)
    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;
    115     } else {
    116       // Measure separator size.
    117       lpmis->itemHeight = GetSystemMetrics(SM_CYMENU) / 2;
    118       lpmis->itemWidth = 0;
    119     }
    120   }
    121 
    122   void OnDrawItem(UINT wParam, DRAWITEMSTRUCT* lpdis) {
    123     HDC hDC = lpdis->hDC;
    124     COLORREF prev_bg_color, prev_text_color;
    125 
    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));
    130     } else {
    131       prev_bg_color = SetBkColor(hDC, GetSysColor(COLOR_MENU));
    132       if (lpdis->itemState & ODS_DISABLED)
    133         prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_GRAYTEXT));
    134       else
    135         prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_MENUTEXT));
    136     }
    137 
    138     if (lpdis->itemData) {
    139       MenuWin::ItemData* data =
    140           reinterpret_cast<MenuWin::ItemData*>(lpdis->itemData);
    141 
    142       // Draw the background.
    143       HBRUSH hbr = CreateSolidBrush(GetBkColor(hDC));
    144       FillRect(hDC, &lpdis->rcItem, hbr);
    145       DeleteObject(hbr);
    146 
    147       // Draw the label.
    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;
    159       gfx::Font font;
    160       HGDIOBJ old_font =
    161           static_cast<HFONT>(SelectObject(hDC, font.GetNativeFont()));
    162 
    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;
    169       string16 accel;
    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);
    174       }
    175       DrawTextEx(hDC, const_cast<wchar_t*>(label.data()),
    176                  static_cast<int>(label.size()), &rect, format | DT_LEFT, NULL);
    177       if (!accel.empty())
    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);
    182 
    183       // Draw the icon after the label, otherwise it would be covered
    184       // by the label.
    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);
    192       }
    193 
    194     } else {
    195       // Draw the separator
    196       lpdis->rcItem.top += (lpdis->rcItem.bottom - lpdis->rcItem.top) / 3;
    197       DrawEdge(hDC, &lpdis->rcItem, EDGE_ETCHED, BF_TOP);
    198     }
    199 
    200     SetBkColor(hDC, prev_bg_color);
    201     SetTextColor(hDC, prev_text_color);
    202   }
    203 
    204   bool FindMenuIDByLocation(MenuWin* menu, const CPoint& loc, int* id) {
    205     int index = MenuItemFromPoint(NULL, menu->menu_, loc);
    206     if (index != -1) {
    207       *id = ChromeGetMenuItemID(menu->menu_, index);
    208       return true;
    209     } else {
    210       for (std::vector<MenuWin*>::iterator i = menu->submenus_.begin();
    211            i != menu->submenus_.end(); ++i) {
    212         if (FindMenuIDByLocation(*i, loc, id))
    213           return true;
    214       }
    215     }
    216     return false;
    217   }
    218 
    219   // The menu that created us.
    220   MenuWin* menu_;
    221 
    222   DISALLOW_COPY_AND_ASSIGN(MenuHostWindow);
    223 };
    224 
    225 }  // namespace
    226 
    227 // static
    228 Menu* Menu::Create(Delegate* delegate,
    229                    AnchorPoint anchor,
    230                    gfx::NativeView parent) {
    231   return new MenuWin(delegate, anchor, parent);
    232 }
    233 
    234 // static
    235 Menu* Menu::GetSystemMenu(gfx::NativeWindow parent) {
    236   return new views::MenuWin(::GetSystemMenu(parent, FALSE));
    237 }
    238 
    239 MenuWin::MenuWin(Delegate* d, AnchorPoint anchor, HWND owner)
    240     : Menu(d, anchor),
    241       menu_(CreatePopupMenu()),
    242       owner_(owner),
    243       is_menu_visible_(false),
    244       owner_draw_(l10n_util::NeedOverrideDefaultUIFont(NULL, NULL)) {
    245   DCHECK(delegate());
    246 }
    247 
    248 MenuWin::MenuWin(HMENU hmenu)
    249     : Menu(NULL, TOPLEFT),
    250       menu_(hmenu),
    251       owner_(NULL),
    252       is_menu_visible_(false),
    253       owner_draw_(false) {
    254   DCHECK(menu_);
    255 }
    256 
    257 MenuWin::~MenuWin() {
    258   STLDeleteContainerPointers(submenus_.begin(), submenus_.end());
    259   STLDeleteContainerPointers(item_data_.begin(), item_data_.end());
    260   DestroyMenu(menu_);
    261 }
    262 
    263 void MenuWin::AddMenuItemWithIcon(int index,
    264                                   int item_id,
    265                                   const string16& label,
    266                                   const gfx::ImageSkia& icon) {
    267   owner_draw_ = true;
    268   Menu::AddMenuItemWithIcon(index, item_id, label, icon);
    269 }
    270 
    271 Menu* MenuWin::AddSubMenuWithIcon(int index,
    272                                   int item_id,
    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);
    278   return submenu;
    279 }
    280 
    281 void MenuWin::AddSeparator(int index) {
    282   MENUITEMINFO mii;
    283   mii.cbSize = sizeof(mii);
    284   mii.fMask = MIIM_FTYPE;
    285   mii.fType = MFT_SEPARATOR;
    286   InsertMenuItem(menu_, index, TRUE, &mii);
    287 }
    288 
    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);
    292 }
    293 
    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);
    297 }
    298 
    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);
    306 }
    307 
    308 bool MenuWin::SetIcon(const gfx::ImageSkia& icon, int item_id) {
    309   if (!owner_draw_)
    310     owner_draw_ = true;
    311 
    312   const int num_items = GetMenuItemCount(menu_);
    313   int sep_count = 0;
    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) {
    322           MENUITEMINFO mii;
    323           mii.cbSize = sizeof(mii);
    324           mii.fMask = MIIM_FTYPE | MIIM_DATA;
    325           mii.fType = MFT_OWNERDRAW;
    326           mii.dwItemData =
    327               reinterpret_cast<ULONG_PTR>(item_data_[i - sep_count]);
    328           SetMenuItemInfo(menu_, item_id, false, &mii);
    329         }
    330         return true;
    331       }
    332     } else {
    333       ++sep_count;
    334     }
    335   }
    336 
    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))
    340       return true;
    341   }
    342 
    343   return false;
    344 }
    345 
    346 void MenuWin::RunMenuAt(int x, int y) {
    347   SetMenuInfo();
    348 
    349   delegate()->MenuWillShow();
    350 
    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.
    353   UINT flags =
    354       GetTPMAlignFlags() | TPM_LEFTBUTTON | TPM_RETURNCMD | TPM_RECURSE;
    355   is_menu_visible_ = true;
    356   DCHECK(owner_);
    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) {
    361     created_host = true;
    362     active_host_window = new MenuHostWindow(this, owner_);
    363   }
    364   UINT selected_id =
    365       TrackPopupMenuEx(menu_, flags, x, y, active_host_window->hwnd(), NULL);
    366   if (created_host) {
    367     delete active_host_window;
    368     active_host_window = NULL;
    369   }
    370   is_menu_visible_ = false;
    371 
    372   // Execute the chosen command
    373   if (selected_id != 0)
    374     delegate()->ExecuteCommand(selected_id);
    375 }
    376 
    377 void MenuWin::Cancel() {
    378   DCHECK(is_menu_visible_);
    379   EndMenu();
    380 }
    381 
    382 int MenuWin::ItemCount() {
    383   return GetMenuItemCount(menu_);
    384 }
    385 
    386 void MenuWin::AddMenuItemInternal(int index,
    387                                   int item_id,
    388                                   const string16& label,
    389                                   const gfx::ImageSkia& icon,
    390                                   MenuItemType type) {
    391   AddMenuItemInternal(index, item_id, label, icon, NULL, type);
    392 }
    393 
    394 void MenuWin::AddMenuItemInternal(int index,
    395                                   int item_id,
    396                                   const string16& label,
    397                                   const gfx::ImageSkia& icon,
    398                                   HMENU submenu,
    399                                   MenuItemType type) {
    400   DCHECK(type != SEPARATOR) << "Call AddSeparator instead!";
    401 
    402   if (!owner_draw_ && !icon.isNull())
    403     owner_draw_ = true;
    404 
    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.
    410     NOTREACHED();
    411     return;
    412   }
    413 
    414   MENUITEMINFO mii;
    415   mii.cbSize = sizeof(mii);
    416   mii.fMask = MIIM_FTYPE | MIIM_ID;
    417   if (submenu) {
    418     mii.fMask |= MIIM_SUBMENU;
    419     mii.hSubMenu = submenu;
    420   }
    421 
    422   // Set the type and ID.
    423   if (!owner_draw_) {
    424     mii.fType = MFT_STRING;
    425     mii.fMask |= MIIM_STRING;
    426   } else {
    427     mii.fType = MFT_OWNERDRAW;
    428   }
    429 
    430   if (type == RADIO)
    431     mii.fType |= MFT_RADIOCHECK;
    432 
    433   mii.wID = item_id;
    434 
    435   // Set the item data.
    436   MenuWin::ItemData* data = new ItemData;
    437   item_data_.push_back(data);
    438   data->submenu = submenu != NULL;
    439 
    440   string16 actual_label(label.empty() ? delegate()->GetLabel(item_id) : label);
    441 
    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();
    447   }
    448   labels_.push_back(actual_label);
    449 
    450   if (owner_draw_) {
    451     if (icon.width() != 0 && icon.height() != 0)
    452       data->icon = icon;
    453     else
    454       data->icon = delegate()->GetIcon(item_id);
    455   } else {
    456     mii.dwTypeData = const_cast<wchar_t*>(labels_.back().c_str());
    457   }
    458 
    459   InsertMenuItem(menu_, index, TRUE, &mii);
    460 }
    461 
    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_) {
    468 }
    469 
    470 void MenuWin::SetMenuInfo() {
    471   const int num_items = GetMenuItemCount(menu_);
    472   int sep_count = 0;
    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);
    479     // Set item states.
    480     if (!(mii_info.fType & MF_SEPARATOR)) {
    481       const int id = ChromeGetMenuItemID(menu_, i);
    482 
    483       MENUITEMINFO mii;
    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;
    489       if (owner_draw_)
    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);
    495 
    496       // Validate the label. If there is a contextual label, use it, otherwise
    497       // default to the static label
    498       string16 label;
    499       if (!delegate()->GetContextualLabel(id, &label))
    500         label = labels_[i - sep_count];
    501 
    502       if (owner_draw_) {
    503         item_data_[i - sep_count]->label = label;
    504         mii.dwItemData = reinterpret_cast<ULONG_PTR>(item_data_[i - sep_count]);
    505       }
    506       mii.dwTypeData = const_cast<wchar_t*>(label.c_str());
    507       mii.cch = static_cast<UINT>(label.size());
    508       SetMenuItemInfo(menu_, i, true, &mii);
    509     } else {
    510       // Set data for owner drawn separators. Set dwItemData NULL to indicate
    511       // a separator.
    512       if (owner_draw_) {
    513         MENUITEMINFO mii;
    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);
    519       }
    520       ++sep_count;
    521     }
    522   }
    523 
    524   for (size_t i = 0; i < submenus_.size(); ++i)
    525     submenus_[i]->SetMenuInfo();
    526 }
    527 
    528 UINT MenuWin::GetStateFlagsForItemID(int item_id) const {
    529   // Use the delegate to get enabled and checked state.
    530   UINT flags =
    531     delegate()->IsCommandEnabled(item_id) ? MFS_ENABLED : MFS_DISABLED;
    532 
    533   if (delegate()->IsItemChecked(item_id))
    534     flags |= MFS_CHECKED;
    535 
    536   if (delegate()->IsItemDefault(item_id))
    537     flags |= MFS_DEFAULT;
    538 
    539   return flags;
    540 }
    541 
    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;
    548   switch (anchor()) {
    549     case TOPLEFT:
    550       if (delegate()->IsRightToLeftUILayout()) {
    551         align_flags |= TPM_RIGHTALIGN;
    552       } else {
    553         align_flags |= TPM_LEFTALIGN;
    554       }
    555       break;
    556 
    557     case TOPRIGHT:
    558       if (delegate()->IsRightToLeftUILayout()) {
    559         align_flags |= TPM_LEFTALIGN;
    560       } else {
    561         align_flags |= TPM_RIGHTALIGN;
    562       }
    563       break;
    564 
    565     default:
    566       NOTREACHED();
    567       return 0;
    568   }
    569   return align_flags;
    570 }
    571 
    572 }  // namespace views
    573