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/native_menu_win.h" 6 7 #include <Windowsx.h> 8 9 #include "base/bind.h" 10 #include "base/logging.h" 11 #include "base/message_loop/message_loop.h" 12 #include "base/stl_util.h" 13 #include "base/strings/string_util.h" 14 #include "base/win/wrapped_window_proc.h" 15 #include "ui/base/accelerators/accelerator.h" 16 #include "ui/base/l10n/l10n_util.h" 17 #include "ui/base/l10n/l10n_util_win.h" 18 #include "ui/base/models/menu_model.h" 19 #include "ui/events/keycodes/keyboard_codes.h" 20 #include "ui/gfx/canvas.h" 21 #include "ui/gfx/font_list.h" 22 #include "ui/gfx/geometry/rect.h" 23 #include "ui/gfx/image/image.h" 24 #include "ui/gfx/image/image_skia.h" 25 #include "ui/gfx/text_utils.h" 26 #include "ui/gfx/win/hwnd_util.h" 27 #include "ui/native_theme/native_theme.h" 28 #include "ui/native_theme/native_theme_win.h" 29 #include "ui/views/controls/menu/menu_2.h" 30 #include "ui/views/controls/menu/menu_config.h" 31 #include "ui/views/controls/menu/menu_insertion_delegate_win.h" 32 #include "ui/views/controls/menu/menu_listener.h" 33 #include "ui/views/layout/layout_constants.h" 34 35 using ui::NativeTheme; 36 37 namespace views { 38 39 // The width of an icon, including the pixels between the icon and 40 // the item label. 41 static const int kIconWidth = 23; 42 // Margins between the top of the item and the label. 43 static const int kItemTopMargin = 3; 44 // Margins between the bottom of the item and the label. 45 static const int kItemBottomMargin = 4; 46 // Margins between the left of the item and the icon. 47 static const int kItemLeftMargin = 4; 48 // The width for displaying the sub-menu arrow. 49 static const int kArrowWidth = 10; 50 51 struct NativeMenuWin::ItemData { 52 // The Windows API requires that whoever creates the menus must own the 53 // strings used for labels, and keep them around for the lifetime of the 54 // created menu. So be it. 55 base::string16 label; 56 57 // Someone needs to own submenus, it may as well be us. 58 scoped_ptr<Menu2> submenu; 59 60 // We need a pointer back to the containing menu in various circumstances. 61 NativeMenuWin* native_menu_win; 62 63 // The index of the item within the menu's model. 64 int model_index; 65 }; 66 67 // Returns the NativeMenuWin for a particular HMENU. 68 static NativeMenuWin* GetNativeMenuWinFromHMENU(HMENU hmenu) { 69 MENUINFO mi = {0}; 70 mi.cbSize = sizeof(mi); 71 mi.fMask = MIM_MENUDATA | MIM_STYLE; 72 GetMenuInfo(hmenu, &mi); 73 return reinterpret_cast<NativeMenuWin*>(mi.dwMenuData); 74 } 75 76 // A window that receives messages from Windows relevant to the native menu 77 // structure we have constructed in NativeMenuWin. 78 class NativeMenuWin::MenuHostWindow { 79 public: 80 explicit MenuHostWindow(NativeMenuWin* parent) : parent_(parent) { 81 RegisterClass(); 82 hwnd_ = CreateWindowEx(l10n_util::GetExtendedStyles(), kWindowClassName, 83 L"", 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL); 84 gfx::CheckWindowCreated(hwnd_); 85 gfx::SetWindowUserData(hwnd_, this); 86 } 87 88 ~MenuHostWindow() { 89 DestroyWindow(hwnd_); 90 } 91 92 HWND hwnd() const { return hwnd_; } 93 94 private: 95 static const wchar_t* kWindowClassName; 96 97 void RegisterClass() { 98 static bool registered = false; 99 if (registered) 100 return; 101 102 WNDCLASSEX window_class; 103 base::win::InitializeWindowClass( 104 kWindowClassName, 105 &base::win::WrappedWindowProc<MenuHostWindowProc>, 106 CS_DBLCLKS, 107 0, 108 0, 109 NULL, 110 reinterpret_cast<HBRUSH>(COLOR_WINDOW+1), 111 NULL, 112 NULL, 113 NULL, 114 &window_class); 115 ATOM clazz = RegisterClassEx(&window_class); 116 CHECK(clazz); 117 registered = true; 118 } 119 120 // Converts the WPARAM value passed to WM_MENUSELECT into an index 121 // corresponding to the menu item that was selected. 122 int GetMenuItemIndexFromWPARAM(HMENU menu, WPARAM w_param) const { 123 int count = GetMenuItemCount(menu); 124 // For normal command menu items, Windows passes a command id as the LOWORD 125 // of WPARAM for WM_MENUSELECT. We need to walk forward through the menu 126 // items to find an item with a matching ID. Ugh! 127 for (int i = 0; i < count; ++i) { 128 MENUITEMINFO mii = {0}; 129 mii.cbSize = sizeof(mii); 130 mii.fMask = MIIM_ID; 131 GetMenuItemInfo(menu, i, MF_BYPOSITION, &mii); 132 if (mii.wID == w_param) 133 return i; 134 } 135 // If we didn't find a matching command ID, this means a submenu has been 136 // selected instead, and rather than passing a command ID in 137 // LOWORD(w_param), Windows has actually passed us a position, so we just 138 // return it. 139 return w_param; 140 } 141 142 NativeMenuWin::ItemData* GetItemData(ULONG_PTR item_data) { 143 return reinterpret_cast<NativeMenuWin::ItemData*>(item_data); 144 } 145 146 // Called when the user selects a specific item. 147 void OnMenuCommand(int position, HMENU menu) { 148 NativeMenuWin* menu_win = GetNativeMenuWinFromHMENU(menu); 149 ui::MenuModel* model = menu_win->model_; 150 NativeMenuWin* root_menu = menu_win; 151 while (root_menu->parent_) 152 root_menu = root_menu->parent_; 153 154 // Only notify the model if it didn't already send out notification. 155 // See comment in MenuMessageHook for details. 156 if (root_menu->menu_action_ == MenuWrapper::MENU_ACTION_NONE) 157 model->ActivatedAt(position); 158 } 159 160 // Called as the user moves their mouse or arrows through the contents of the 161 // menu. 162 void OnMenuSelect(WPARAM w_param, HMENU menu) { 163 if (!menu) 164 return; // menu is null when closing on XP. 165 166 int position = GetMenuItemIndexFromWPARAM(menu, w_param); 167 if (position >= 0) 168 GetNativeMenuWinFromHMENU(menu)->model_->HighlightChangedTo(position); 169 } 170 171 // Called by Windows to measure the size of an owner-drawn menu item. 172 void OnMeasureItem(WPARAM w_param, MEASUREITEMSTRUCT* measure_item_struct) { 173 NativeMenuWin::ItemData* data = GetItemData(measure_item_struct->itemData); 174 if (data) { 175 gfx::FontList font_list; 176 measure_item_struct->itemWidth = 177 gfx::GetStringWidth(data->label, font_list) + 178 kIconWidth + kItemLeftMargin + views::kItemLabelSpacing - 179 GetSystemMetrics(SM_CXMENUCHECK); 180 if (data->submenu.get()) 181 measure_item_struct->itemWidth += kArrowWidth; 182 // If the label contains an accelerator, make room for tab. 183 if (data->label.find(L'\t') != base::string16::npos) 184 measure_item_struct->itemWidth += gfx::GetStringWidth(L" ", font_list); 185 measure_item_struct->itemHeight = 186 font_list.GetHeight() + kItemBottomMargin + kItemTopMargin; 187 } else { 188 // Measure separator size. 189 measure_item_struct->itemHeight = GetSystemMetrics(SM_CYMENU) / 2; 190 measure_item_struct->itemWidth = 0; 191 } 192 } 193 194 // Called by Windows to paint an owner-drawn menu item. 195 void OnDrawItem(UINT w_param, DRAWITEMSTRUCT* draw_item_struct) { 196 HDC dc = draw_item_struct->hDC; 197 COLORREF prev_bg_color, prev_text_color; 198 199 // Set background color and text color 200 if (draw_item_struct->itemState & ODS_SELECTED) { 201 prev_bg_color = SetBkColor(dc, GetSysColor(COLOR_HIGHLIGHT)); 202 prev_text_color = SetTextColor(dc, GetSysColor(COLOR_HIGHLIGHTTEXT)); 203 } else { 204 prev_bg_color = SetBkColor(dc, GetSysColor(COLOR_MENU)); 205 if (draw_item_struct->itemState & ODS_DISABLED) 206 prev_text_color = SetTextColor(dc, GetSysColor(COLOR_GRAYTEXT)); 207 else 208 prev_text_color = SetTextColor(dc, GetSysColor(COLOR_MENUTEXT)); 209 } 210 211 if (draw_item_struct->itemData) { 212 NativeMenuWin::ItemData* data = GetItemData(draw_item_struct->itemData); 213 // Draw the background. 214 HBRUSH hbr = CreateSolidBrush(GetBkColor(dc)); 215 FillRect(dc, &draw_item_struct->rcItem, hbr); 216 DeleteObject(hbr); 217 218 // Draw the label. 219 RECT rect = draw_item_struct->rcItem; 220 rect.top += kItemTopMargin; 221 // Should we add kIconWidth only when icon.width() != 0 ? 222 rect.left += kItemLeftMargin + kIconWidth; 223 rect.right -= views::kItemLabelSpacing; 224 UINT format = DT_TOP | DT_SINGLELINE; 225 // Check whether the mnemonics should be underlined. 226 BOOL underline_mnemonics; 227 SystemParametersInfo(SPI_GETKEYBOARDCUES, 0, &underline_mnemonics, 0); 228 if (!underline_mnemonics) 229 format |= DT_HIDEPREFIX; 230 gfx::FontList font_list; 231 HGDIOBJ old_font = static_cast<HFONT>( 232 SelectObject(dc, font_list.GetPrimaryFont().GetNativeFont())); 233 234 // If an accelerator is specified (with a tab delimiting the rest of the 235 // label from the accelerator), we have to justify the fist part on the 236 // left and the accelerator on the right. 237 // TODO(jungshik): This will break in RTL UI. Currently, he/ar use the 238 // window system UI font and will not hit here. 239 base::string16 label = data->label; 240 base::string16 accel; 241 base::string16::size_type tab_pos = label.find(L'\t'); 242 if (tab_pos != base::string16::npos) { 243 accel = label.substr(tab_pos); 244 label = label.substr(0, tab_pos); 245 } 246 DrawTextEx(dc, const_cast<wchar_t*>(label.data()), 247 static_cast<int>(label.size()), &rect, format | DT_LEFT, NULL); 248 if (!accel.empty()) 249 DrawTextEx(dc, const_cast<wchar_t*>(accel.data()), 250 static_cast<int>(accel.size()), &rect, 251 format | DT_RIGHT, NULL); 252 SelectObject(dc, old_font); 253 254 ui::MenuModel::ItemType type = 255 data->native_menu_win->model_->GetTypeAt(data->model_index); 256 257 // Draw the icon after the label, otherwise it would be covered 258 // by the label. 259 gfx::Image icon; 260 if (data->native_menu_win->model_->GetIconAt(data->model_index, &icon)) { 261 // We currently don't support items with both icons and checkboxes. 262 const gfx::ImageSkia* skia_icon = icon.ToImageSkia(); 263 DCHECK(type != ui::MenuModel::TYPE_CHECK); 264 gfx::Canvas canvas( 265 skia_icon->GetRepresentation(1.0f), 266 false); 267 skia::DrawToNativeContext( 268 canvas.sk_canvas(), dc, 269 draw_item_struct->rcItem.left + kItemLeftMargin, 270 draw_item_struct->rcItem.top + (draw_item_struct->rcItem.bottom - 271 draw_item_struct->rcItem.top - skia_icon->height()) / 2, NULL); 272 } else if (type == ui::MenuModel::TYPE_CHECK && 273 data->native_menu_win->model_->IsItemCheckedAt( 274 data->model_index)) { 275 // Manually render a checkbox. 276 ui::NativeThemeWin* native_theme = ui::NativeThemeWin::instance(); 277 const MenuConfig& config = MenuConfig::instance(native_theme); 278 NativeTheme::State state; 279 if (draw_item_struct->itemState & ODS_DISABLED) { 280 state = NativeTheme::kDisabled; 281 } else { 282 state = draw_item_struct->itemState & ODS_SELECTED ? 283 NativeTheme::kHovered : NativeTheme::kNormal; 284 } 285 int height = 286 draw_item_struct->rcItem.bottom - draw_item_struct->rcItem.top; 287 int icon_y = kItemTopMargin + 288 (height - kItemTopMargin - kItemBottomMargin - 289 config.check_height) / 2; 290 gfx::Canvas canvas(gfx::Size(config.check_width, config.check_height), 291 1.0f, 292 false); 293 NativeTheme::ExtraParams extra; 294 extra.menu_check.is_radio = false; 295 gfx::Rect bounds(0, 0, config.check_width, config.check_height); 296 297 // Draw the background and the check. 298 native_theme->Paint( 299 canvas.sk_canvas(), NativeTheme::kMenuCheckBackground, 300 state, bounds, extra); 301 native_theme->Paint( 302 canvas.sk_canvas(), NativeTheme::kMenuCheck, state, bounds, extra); 303 304 // Draw checkbox to menu. 305 skia::DrawToNativeContext(canvas.sk_canvas(), dc, 306 draw_item_struct->rcItem.left + kItemLeftMargin, 307 draw_item_struct->rcItem.top + (draw_item_struct->rcItem.bottom - 308 draw_item_struct->rcItem.top - config.check_height) / 2, NULL); 309 } 310 311 } else { 312 // Draw the separator 313 draw_item_struct->rcItem.top += 314 (draw_item_struct->rcItem.bottom - draw_item_struct->rcItem.top) / 3; 315 DrawEdge(dc, &draw_item_struct->rcItem, EDGE_ETCHED, BF_TOP); 316 } 317 318 SetBkColor(dc, prev_bg_color); 319 SetTextColor(dc, prev_text_color); 320 } 321 322 bool ProcessWindowMessage(HWND window, 323 UINT message, 324 WPARAM w_param, 325 LPARAM l_param, 326 LRESULT* l_result) { 327 switch (message) { 328 case WM_MENUCOMMAND: 329 OnMenuCommand(w_param, reinterpret_cast<HMENU>(l_param)); 330 *l_result = 0; 331 return true; 332 case WM_MENUSELECT: 333 OnMenuSelect(LOWORD(w_param), reinterpret_cast<HMENU>(l_param)); 334 *l_result = 0; 335 return true; 336 case WM_MEASUREITEM: 337 OnMeasureItem(w_param, reinterpret_cast<MEASUREITEMSTRUCT*>(l_param)); 338 *l_result = 0; 339 return true; 340 case WM_DRAWITEM: 341 OnDrawItem(w_param, reinterpret_cast<DRAWITEMSTRUCT*>(l_param)); 342 *l_result = 0; 343 return true; 344 // TODO(beng): bring over owner draw from old menu system. 345 } 346 return false; 347 } 348 349 static LRESULT CALLBACK MenuHostWindowProc(HWND window, 350 UINT message, 351 WPARAM w_param, 352 LPARAM l_param) { 353 MenuHostWindow* host = 354 reinterpret_cast<MenuHostWindow*>(gfx::GetWindowUserData(window)); 355 // host is null during initial construction. 356 LRESULT l_result = 0; 357 if (!host || !host->ProcessWindowMessage(window, message, w_param, l_param, 358 &l_result)) { 359 return DefWindowProc(window, message, w_param, l_param); 360 } 361 return l_result; 362 } 363 364 HWND hwnd_; 365 NativeMenuWin* parent_; 366 367 DISALLOW_COPY_AND_ASSIGN(MenuHostWindow); 368 }; 369 370 struct NativeMenuWin::HighlightedMenuItemInfo { 371 HighlightedMenuItemInfo() 372 : has_parent(false), 373 has_submenu(false), 374 menu(NULL), 375 position(-1) { 376 } 377 378 bool has_parent; 379 bool has_submenu; 380 381 // The menu and position. These are only set for non-disabled menu items. 382 NativeMenuWin* menu; 383 int position; 384 }; 385 386 // static 387 const wchar_t* NativeMenuWin::MenuHostWindow::kWindowClassName = 388 L"ViewsMenuHostWindow"; 389 390 //////////////////////////////////////////////////////////////////////////////// 391 // NativeMenuWin, public: 392 393 NativeMenuWin::NativeMenuWin(ui::MenuModel* model, HWND system_menu_for) 394 : model_(model), 395 menu_(NULL), 396 owner_draw_(l10n_util::NeedOverrideDefaultUIFont(NULL, NULL) && 397 !system_menu_for), 398 system_menu_for_(system_menu_for), 399 first_item_index_(0), 400 menu_action_(MENU_ACTION_NONE), 401 menu_to_select_(NULL), 402 position_to_select_(-1), 403 menu_to_select_factory_(this), 404 parent_(NULL), 405 destroyed_flag_(NULL) { 406 } 407 408 NativeMenuWin::~NativeMenuWin() { 409 if (destroyed_flag_) 410 *destroyed_flag_ = true; 411 STLDeleteContainerPointers(items_.begin(), items_.end()); 412 DestroyMenu(menu_); 413 } 414 415 //////////////////////////////////////////////////////////////////////////////// 416 // NativeMenuWin, MenuWrapper implementation: 417 418 void NativeMenuWin::RunMenuAt(const gfx::Point& point, int alignment) { 419 CreateHostWindow(); 420 UpdateStates(); 421 UINT flags = TPM_LEFTBUTTON | TPM_RIGHTBUTTON | TPM_RECURSE; 422 flags |= GetAlignmentFlags(alignment); 423 menu_action_ = MENU_ACTION_NONE; 424 425 // Set a hook function so we can listen for keyboard events while the 426 // menu is open, and store a pointer to this object in a static 427 // variable so the hook has access to it (ugly, but it's the 428 // only way). 429 open_native_menu_win_ = this; 430 HHOOK hhook = SetWindowsHookEx(WH_MSGFILTER, MenuMessageHook, 431 GetModuleHandle(NULL), ::GetCurrentThreadId()); 432 433 // Mark that any registered listeners have not been called for this particular 434 // opening of the menu. 435 listeners_called_ = false; 436 437 // Command dispatch is done through WM_MENUCOMMAND, handled by the host 438 // window. 439 HWND hwnd = host_window_->hwnd(); 440 menu_to_select_ = NULL; 441 position_to_select_ = -1; 442 menu_to_select_factory_.InvalidateWeakPtrs(); 443 bool destroyed = false; 444 destroyed_flag_ = &destroyed; 445 model_->MenuWillShow(); 446 TrackPopupMenu(menu_, flags, point.x(), point.y(), 0, host_window_->hwnd(), 447 NULL); 448 UnhookWindowsHookEx(hhook); 449 open_native_menu_win_ = NULL; 450 if (destroyed) 451 return; 452 destroyed_flag_ = NULL; 453 if (menu_to_select_) { 454 // Folks aren't too happy if we notify immediately. In particular, notifying 455 // the delegate can cause destruction leaving the stack in a weird 456 // state. Instead post a task, then notify. This mirrors what WM_MENUCOMMAND 457 // does. 458 menu_to_select_factory_.InvalidateWeakPtrs(); 459 base::MessageLoop::current()->PostTask( 460 FROM_HERE, 461 base::Bind(&NativeMenuWin::DelayedSelect, 462 menu_to_select_factory_.GetWeakPtr())); 463 menu_action_ = MENU_ACTION_SELECTED; 464 } 465 // Send MenuClosed after we schedule the select, otherwise MenuClosed is 466 // processed after the select (MenuClosed posts a delayed task too). 467 model_->MenuClosed(); 468 } 469 470 void NativeMenuWin::CancelMenu() { 471 EndMenu(); 472 } 473 474 void NativeMenuWin::Rebuild(MenuInsertionDelegateWin* delegate) { 475 ResetNativeMenu(); 476 items_.clear(); 477 478 owner_draw_ = model_->HasIcons() || owner_draw_; 479 first_item_index_ = delegate ? delegate->GetInsertionIndex(menu_) : 0; 480 for (int menu_index = first_item_index_; 481 menu_index < first_item_index_ + model_->GetItemCount(); ++menu_index) { 482 int model_index = menu_index - first_item_index_; 483 if (model_->GetTypeAt(model_index) == ui::MenuModel::TYPE_SEPARATOR) 484 AddSeparatorItemAt(menu_index, model_index); 485 else 486 AddMenuItemAt(menu_index, model_index); 487 } 488 } 489 490 void NativeMenuWin::UpdateStates() { 491 // A depth-first walk of the menu items, updating states. 492 int model_index = 0; 493 std::vector<ItemData*>::const_iterator it; 494 for (it = items_.begin(); it != items_.end(); ++it, ++model_index) { 495 int menu_index = model_index + first_item_index_; 496 SetMenuItemState(menu_index, model_->IsEnabledAt(model_index), 497 model_->IsItemCheckedAt(model_index), false); 498 if (model_->IsItemDynamicAt(model_index)) { 499 // TODO(atwilson): Update the icon as well (http://crbug.com/66508). 500 SetMenuItemLabel(menu_index, model_index, 501 model_->GetLabelAt(model_index)); 502 } 503 Menu2* submenu = (*it)->submenu.get(); 504 if (submenu) 505 submenu->UpdateStates(); 506 } 507 } 508 509 HMENU NativeMenuWin::GetNativeMenu() const { 510 return menu_; 511 } 512 513 NativeMenuWin::MenuAction NativeMenuWin::GetMenuAction() const { 514 return menu_action_; 515 } 516 517 void NativeMenuWin::AddMenuListener(MenuListener* listener) { 518 listeners_.AddObserver(listener); 519 } 520 521 void NativeMenuWin::RemoveMenuListener(MenuListener* listener) { 522 listeners_.RemoveObserver(listener); 523 } 524 525 void NativeMenuWin::SetMinimumWidth(int width) { 526 NOTIMPLEMENTED(); 527 } 528 529 //////////////////////////////////////////////////////////////////////////////// 530 // NativeMenuWin, private: 531 532 // static 533 NativeMenuWin* NativeMenuWin::open_native_menu_win_ = NULL; 534 535 void NativeMenuWin::DelayedSelect() { 536 if (menu_to_select_) 537 menu_to_select_->model_->ActivatedAt(position_to_select_); 538 } 539 540 // static 541 bool NativeMenuWin::GetHighlightedMenuItemInfo( 542 HMENU menu, 543 HighlightedMenuItemInfo* info) { 544 for (int i = 0; i < ::GetMenuItemCount(menu); i++) { 545 UINT state = ::GetMenuState(menu, i, MF_BYPOSITION); 546 if (state & MF_HILITE) { 547 if (state & MF_POPUP) { 548 HMENU submenu = GetSubMenu(menu, i); 549 if (GetHighlightedMenuItemInfo(submenu, info)) 550 info->has_parent = true; 551 else 552 info->has_submenu = true; 553 } else if (!(state & MF_SEPARATOR) && !(state & MF_DISABLED)) { 554 info->menu = GetNativeMenuWinFromHMENU(menu); 555 info->position = i; 556 } 557 return true; 558 } 559 } 560 return false; 561 } 562 563 // static 564 LRESULT CALLBACK NativeMenuWin::MenuMessageHook( 565 int n_code, WPARAM w_param, LPARAM l_param) { 566 LRESULT result = CallNextHookEx(NULL, n_code, w_param, l_param); 567 568 NativeMenuWin* this_ptr = open_native_menu_win_; 569 if (!this_ptr) 570 return result; 571 572 // The first time this hook is called, that means the menu has successfully 573 // opened, so call the callback function on all of our listeners. 574 if (!this_ptr->listeners_called_) { 575 FOR_EACH_OBSERVER(MenuListener, this_ptr->listeners_, OnMenuOpened()); 576 this_ptr->listeners_called_ = true; 577 } 578 579 MSG* msg = reinterpret_cast<MSG*>(l_param); 580 if (msg->message == WM_LBUTTONUP || msg->message == WM_RBUTTONUP) { 581 HighlightedMenuItemInfo info; 582 if (GetHighlightedMenuItemInfo(this_ptr->menu_, &info) && info.menu) { 583 // It appears that when running a menu by way of TrackPopupMenu(Ex) win32 584 // gets confused if the underlying window paints itself. As its very easy 585 // for the underlying window to repaint itself (especially since some menu 586 // items trigger painting of the tabstrip on mouse over) we have this 587 // workaround. When the mouse is released on a menu item we remember the 588 // menu item and end the menu. When the nested message loop returns we 589 // schedule a task to notify the model. It's still possible to get a 590 // WM_MENUCOMMAND, so we have to be careful that we don't notify the model 591 // twice. 592 this_ptr->menu_to_select_ = info.menu; 593 this_ptr->position_to_select_ = info.position; 594 EndMenu(); 595 } 596 } else if (msg->message == WM_KEYDOWN) { 597 HighlightedMenuItemInfo info; 598 if (GetHighlightedMenuItemInfo(this_ptr->menu_, &info)) { 599 if (msg->wParam == VK_LEFT && !info.has_parent) { 600 this_ptr->menu_action_ = MENU_ACTION_PREVIOUS; 601 ::EndMenu(); 602 } else if (msg->wParam == VK_RIGHT && !info.has_parent && 603 !info.has_submenu) { 604 this_ptr->menu_action_ = MENU_ACTION_NEXT; 605 ::EndMenu(); 606 } 607 } 608 } 609 610 return result; 611 } 612 613 bool NativeMenuWin::IsSeparatorItemAt(int menu_index) const { 614 MENUITEMINFO mii = {0}; 615 mii.cbSize = sizeof(mii); 616 mii.fMask = MIIM_FTYPE; 617 GetMenuItemInfo(menu_, menu_index, MF_BYPOSITION, &mii); 618 return !!(mii.fType & MF_SEPARATOR); 619 } 620 621 void NativeMenuWin::AddMenuItemAt(int menu_index, int model_index) { 622 MENUITEMINFO mii = {0}; 623 mii.cbSize = sizeof(mii); 624 mii.fMask = MIIM_FTYPE | MIIM_ID | MIIM_DATA; 625 if (!owner_draw_) 626 mii.fType = MFT_STRING; 627 else 628 mii.fType = MFT_OWNERDRAW; 629 630 ItemData* item_data = new ItemData; 631 item_data->label = base::string16(); 632 ui::MenuModel::ItemType type = model_->GetTypeAt(model_index); 633 if (type == ui::MenuModel::TYPE_SUBMENU) { 634 item_data->submenu.reset(new Menu2(model_->GetSubmenuModelAt(model_index))); 635 mii.fMask |= MIIM_SUBMENU; 636 mii.hSubMenu = item_data->submenu->GetNativeMenu(); 637 GetNativeMenuWinFromHMENU(mii.hSubMenu)->parent_ = this; 638 } else { 639 if (type == ui::MenuModel::TYPE_RADIO) 640 mii.fType |= MFT_RADIOCHECK; 641 mii.wID = model_->GetCommandIdAt(model_index); 642 } 643 item_data->native_menu_win = this; 644 item_data->model_index = model_index; 645 items_.insert(items_.begin() + model_index, item_data); 646 mii.dwItemData = reinterpret_cast<ULONG_PTR>(item_data); 647 UpdateMenuItemInfoForString(&mii, model_index, 648 model_->GetLabelAt(model_index)); 649 InsertMenuItem(menu_, menu_index, TRUE, &mii); 650 } 651 652 void NativeMenuWin::AddSeparatorItemAt(int menu_index, int model_index) { 653 MENUITEMINFO mii = {0}; 654 mii.cbSize = sizeof(mii); 655 mii.fMask = MIIM_FTYPE; 656 mii.fType = MFT_SEPARATOR; 657 // Insert a dummy entry into our label list so we can index directly into it 658 // using item indices if need be. 659 items_.insert(items_.begin() + model_index, new ItemData); 660 InsertMenuItem(menu_, menu_index, TRUE, &mii); 661 } 662 663 void NativeMenuWin::SetMenuItemState(int menu_index, bool enabled, bool checked, 664 bool is_default) { 665 if (IsSeparatorItemAt(menu_index)) 666 return; 667 668 UINT state = enabled ? MFS_ENABLED : MFS_DISABLED; 669 if (checked) 670 state |= MFS_CHECKED; 671 if (is_default) 672 state |= MFS_DEFAULT; 673 674 MENUITEMINFO mii = {0}; 675 mii.cbSize = sizeof(mii); 676 mii.fMask = MIIM_STATE; 677 mii.fState = state; 678 SetMenuItemInfo(menu_, menu_index, MF_BYPOSITION, &mii); 679 } 680 681 void NativeMenuWin::SetMenuItemLabel(int menu_index, 682 int model_index, 683 const base::string16& label) { 684 if (IsSeparatorItemAt(menu_index)) 685 return; 686 687 MENUITEMINFO mii = {0}; 688 mii.cbSize = sizeof(mii); 689 UpdateMenuItemInfoForString(&mii, model_index, label); 690 SetMenuItemInfo(menu_, menu_index, MF_BYPOSITION, &mii); 691 } 692 693 void NativeMenuWin::UpdateMenuItemInfoForString(MENUITEMINFO* mii, 694 int model_index, 695 const base::string16& label) { 696 base::string16 formatted = label; 697 ui::MenuModel::ItemType type = model_->GetTypeAt(model_index); 698 // Strip out any tabs, otherwise they get interpreted as accelerators and can 699 // lead to weird behavior. 700 ReplaceSubstringsAfterOffset(&formatted, 0, L"\t", L" "); 701 if (type != ui::MenuModel::TYPE_SUBMENU) { 702 // Add accelerator details to the label if provided. 703 ui::Accelerator accelerator(ui::VKEY_UNKNOWN, ui::EF_NONE); 704 if (model_->GetAcceleratorAt(model_index, &accelerator)) { 705 formatted += L"\t"; 706 formatted += accelerator.GetShortcutText(); 707 } 708 } 709 710 // Update the owned string, since Windows will want us to keep this new 711 // version around. 712 items_[model_index]->label = formatted; 713 714 // Give Windows a pointer to the label string. 715 mii->fMask |= MIIM_STRING; 716 mii->dwTypeData = 717 const_cast<wchar_t*>(items_[model_index]->label.c_str()); 718 } 719 720 UINT NativeMenuWin::GetAlignmentFlags(int alignment) const { 721 UINT alignment_flags = TPM_TOPALIGN; 722 if (alignment == Menu2::ALIGN_TOPLEFT) 723 alignment_flags |= TPM_LEFTALIGN; 724 else if (alignment == Menu2::ALIGN_TOPRIGHT) 725 alignment_flags |= TPM_RIGHTALIGN; 726 return alignment_flags; 727 } 728 729 void NativeMenuWin::ResetNativeMenu() { 730 if (IsWindow(system_menu_for_)) { 731 if (menu_) 732 GetSystemMenu(system_menu_for_, TRUE); 733 menu_ = GetSystemMenu(system_menu_for_, FALSE); 734 } else { 735 if (menu_) 736 DestroyMenu(menu_); 737 menu_ = CreatePopupMenu(); 738 // Rather than relying on the return value of TrackPopupMenuEx, which is 739 // always a command identifier, instead we tell the menu to notify us via 740 // our host window and the WM_MENUCOMMAND message. 741 MENUINFO mi = {0}; 742 mi.cbSize = sizeof(mi); 743 mi.fMask = MIM_STYLE | MIM_MENUDATA; 744 mi.dwStyle = MNS_NOTIFYBYPOS; 745 mi.dwMenuData = reinterpret_cast<ULONG_PTR>(this); 746 SetMenuInfo(menu_, &mi); 747 } 748 } 749 750 void NativeMenuWin::CreateHostWindow() { 751 // This only gets called from RunMenuAt, and as such there is only ever one 752 // host window per menu hierarchy, no matter how many NativeMenuWin objects 753 // exist wrapping submenus. 754 if (!host_window_.get()) 755 host_window_.reset(new MenuHostWindow(this)); 756 } 757 758 //////////////////////////////////////////////////////////////////////////////// 759 // MenuWrapper, public: 760 761 // static 762 MenuWrapper* MenuWrapper::CreateWrapper(ui::MenuModel* model) { 763 return new NativeMenuWin(model, NULL); 764 } 765 766 } // namespace views 767