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