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