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