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